1use std::sync::Arc;
20
21use lattice_grammar::source::SourceLocation;
22use lattice_grammar::{CommandInvocation, CommandRegistry};
23use lattice_keymap::{BindingMode, KeymapCapability, KeymapHandle, KeymapLayer};
24
25use crate::{
26 Component, PluginBudget, PluginHost, PluginHostError, PluginId, PluginManifest, TrustTier,
27 arm_store, classify_trap,
28};
29
30pub(crate) mod bindings {
31 wasmtime::component::bindgen!({
32 world: "keymap-plugin",
33 path: "../lattice-wit/wit",
34 exports: { default: async },
39 });
40}
41
42#[derive(Debug, Clone)]
46pub struct KeymapBindingToken {
47 pub mode: BindingMode,
48 pub chord: String,
49}
50
51pub(crate) struct KeymapBindCtx {
54 pub keymap: KeymapHandle,
55 pub commands: Arc<CommandRegistry>,
56 pub plugin_id: PluginId,
57}
58
59pub(crate) fn bind_user_keybinding(
67 keymap: &KeymapHandle,
68 commands: &CommandRegistry,
69 plugin_id: u32,
70 mode: BindingMode,
71 chord: &str,
72 command: &str,
73) -> bool {
74 let Some(command_id) = commands.id_by_name(command) else {
75 tracing::warn!(
76 command,
77 chord,
78 "user keybinding skipped: command not registered"
79 );
80 return false;
81 };
82 match keymap.try_bind_chord_string(
83 KeymapCapability::User,
84 KeymapLayer::User,
85 mode,
86 chord,
87 CommandInvocation::of(command_id),
88 SourceLocation::plugin(plugin_id),
89 ) {
90 Ok(()) => true,
91 Err(error) => {
92 tracing::warn!(chord, command, %error, "user keybinding skipped");
93 false
94 }
95 }
96}
97
98impl PluginHost {
99 pub async fn spawn_keymap_plugin(
110 &self,
111 component: &Component,
112 manifest: &PluginManifest,
113 tier: TrustTier,
114 budget: PluginBudget,
115 keymap: &KeymapHandle,
116 commands: &Arc<CommandRegistry>,
117 ) -> Result<(PluginId, Vec<KeymapBindingToken>), PluginHostError> {
118 let (wasi, outcome, _data_dir) = self.build_plugin_wasi(manifest, tier);
119 for denied in &outcome.denied {
120 tracing::warn!(
121 plugin = %manifest.id,
122 capability = ?denied,
123 "keymap plugin loaded with a withheld capability (reduced function)"
124 );
125 }
126 let mut store = self.new_store(wasi, outcome.grant, budget, Some(&manifest.id))?;
127 let bindings =
128 bindings::KeymapPlugin::instantiate_async(&mut store, component, &self.linker)
129 .await
130 .map_err(|e| PluginHostError::Instantiate(e.into()))?;
131 let plugin_id = self.alloc_id();
132
133 store.data_mut().keymap_ctx = Some(KeymapBindCtx {
136 keymap: keymap.clone(),
137 commands: Arc::clone(commands),
138 plugin_id,
139 });
140 store.data_mut().log_ctx = self.log_ctx_for(plugin_id);
143
144 arm_store(&mut store, budget)?;
145 bindings
146 .call_register_keymap(&mut store)
147 .await
148 .map_err(|source| PluginHostError::Trap {
149 func: "register-keymap",
150 kind: classify_trap(&source),
151 source: source.into(),
152 })?;
153
154 let tokens = store.data_mut().keymap_contributions.drain(..).collect();
155 Ok((plugin_id, tokens))
156 }
157}
158
159pub(crate) fn project_binding_mode(
162 wit: bindings::lattice::plugin_host::keymap::BindingMode,
163) -> BindingMode {
164 use bindings::lattice::plugin_host::keymap::BindingMode as Wit;
165 match wit {
166 Wit::Normal => BindingMode::Normal,
167 Wit::Insert => BindingMode::Insert,
168 Wit::Visual => BindingMode::Visual,
169 Wit::Select => BindingMode::Select,
170 Wit::Replace => BindingMode::Replace,
171 Wit::Command => BindingMode::Command,
172 Wit::Search => BindingMode::Search,
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 #![allow(clippy::unwrap_used, clippy::panic)]
179
180 use super::*;
181 use lattice_grammar::registry::CommandRegistry;
182
183 fn registry_with_write() -> CommandRegistry {
185 let mut r = CommandRegistry::new();
186 let _ = lattice_grammar::ex_commands::populate(&mut r);
187 r
188 }
189
190 #[test]
191 fn binds_a_user_keybinding_into_the_user_layer() {
192 let commands = registry_with_write();
193 let keymap = KeymapHandle::new();
194 let bound = bind_user_keybinding(
196 &keymap,
197 &commands,
198 7,
199 BindingMode::Normal,
200 "<C-s>",
201 "ex:write",
202 );
203 assert!(bound, "a well-formed binding to a real command lands");
204 assert!(
205 keymap.binding_count() >= 1,
206 "the User-layer binding is live"
207 );
208 }
209
210 #[test]
211 fn unknown_command_binds_nothing() {
212 let commands = registry_with_write();
213 let keymap = KeymapHandle::new();
214 assert!(
215 !bind_user_keybinding(
216 &keymap,
217 &commands,
218 7,
219 BindingMode::Normal,
220 "<C-s>",
221 "nope:nope"
222 ),
223 "an unregistered command binds nothing"
224 );
225 assert_eq!(keymap.binding_count(), 0, "no binding leaked");
226 }
227
228 #[test]
229 fn unparseable_chord_binds_nothing() {
230 let commands = registry_with_write();
231 let keymap = KeymapHandle::new();
232 assert!(
233 !bind_user_keybinding(
234 &keymap,
235 &commands,
236 7,
237 BindingMode::Normal,
238 "<not-a-chord",
239 "ex:write"
240 ),
241 "a malformed chord binds nothing"
242 );
243 assert_eq!(keymap.binding_count(), 0);
244 }
245}