reovim_server/registry/command.rs
1//! Command registry for storing and executing commands.
2//!
3//! Commands are stored by their [`CommandId`] and executed through
4//! the [`CommandHandler`] trait. The registry provides lookup and
5//! execution services to the server.
6//!
7//! # Module Ownership
8//!
9//! Commands can be registered with optional module ownership via
10//! [`CommandRegistry::register_for_module`]. When a module is unloaded, all its
11//! registered commands can be removed via [`CommandRegistry::unregister_for_module`].
12
13use std::{collections::HashMap, sync::Arc};
14
15use {
16 reovim_driver_command::{
17 CommandContext, CommandHandler, CommandInfo, CommandPriority, CommandQueryService,
18 CommandResult,
19 },
20 reovim_driver_session::{
21 Session as DriverSession, SessionRuntime,
22 api::{CommandExecutor, CommandHandle},
23 },
24 reovim_driver_vfs::VfsDriver,
25 reovim_kernel::{
26 api::v1::{CommandId, KernelContext, ModuleId, Service},
27 profile_scope,
28 },
29};
30
31/// Entry in the command registry with optional ownership tracking.
32#[derive(Clone)]
33struct CommandEntry {
34 /// The command handler.
35 handler: Arc<dyn CommandHandler>,
36 /// The module that owns this command (if any).
37 owner: Option<ModuleId>,
38 /// Registration priority (#545). Higher priority wins on conflict.
39 priority: CommandPriority,
40}
41
42/// Registry for command handlers.
43///
44/// Stores [`CommandHandler`] implementations keyed by [`CommandId`].
45/// The server uses this to execute commands when keybindings match.
46///
47/// # Module Ownership
48///
49/// Commands can be registered with module ownership via [`Self::register_for_module`].
50/// This enables automatic cleanup when modules are unloaded.
51#[derive(Default, Clone)]
52pub struct CommandRegistry {
53 entries: HashMap<CommandId, CommandEntry>,
54}
55
56impl CommandRegistry {
57 /// Create a new empty command registry.
58 #[must_use]
59 pub fn new() -> Self {
60 Self::default()
61 }
62
63 /// Register a command handler (without module ownership).
64 ///
65 /// The command's ID is obtained from the handler via its `id()` method.
66 /// If a command with the same ID already exists, the higher priority
67 /// handler wins. Equal priority uses last-wins semantics (#545).
68 pub fn register(&mut self, handler: Arc<dyn CommandHandler>) {
69 let id = handler.id();
70 let new_priority = handler.priority();
71
72 // Only replace if new handler has >= priority (#545)
73 if self
74 .entries
75 .get(&id)
76 .is_some_and(|existing| new_priority < existing.priority)
77 {
78 return;
79 }
80
81 self.entries.insert(
82 id,
83 CommandEntry {
84 handler,
85 owner: None,
86 priority: new_priority,
87 },
88 );
89 }
90
91 /// Register a command handler with module ownership.
92 ///
93 /// The command's ID is obtained from the handler via its `id()` method.
94 /// If a command with the same ID already exists, the higher priority
95 /// handler wins. Equal priority uses last-wins semantics (#545).
96 ///
97 /// When the owning module is unloaded, this command will be automatically
98 /// deregistered via [`Self::unregister_for_module`].
99 pub fn register_for_module(&mut self, handler: Arc<dyn CommandHandler>, owner: ModuleId) {
100 let id = handler.id();
101 let new_priority = handler.priority();
102
103 if self
104 .entries
105 .get(&id)
106 .is_some_and(|existing| new_priority < existing.priority)
107 {
108 return;
109 }
110
111 self.entries.insert(
112 id,
113 CommandEntry {
114 handler,
115 owner: Some(owner),
116 priority: new_priority,
117 },
118 );
119 }
120
121 /// Remove all commands owned by a module.
122 ///
123 /// Called when a module is being unloaded to clean up its registrations.
124 ///
125 /// Returns the number of commands that were removed.
126 pub fn unregister_for_module(&mut self, module: &ModuleId) -> usize {
127 let before = self.entries.len();
128 self.entries
129 .retain(|_, entry| entry.owner.as_ref() != Some(module));
130 before - self.entries.len()
131 }
132
133 /// Get a command handler by ID.
134 #[must_use]
135 pub fn get(&self, id: &CommandId) -> Option<&Arc<dyn CommandHandler>> {
136 self.entries.get(id).map(|entry| &entry.handler)
137 }
138
139 /// Check if a command is registered.
140 #[must_use]
141 pub fn contains(&self, id: &CommandId) -> bool {
142 self.entries.contains_key(id)
143 }
144
145 /// Execute a command with per-client state (#471, #477).
146 ///
147 /// This uses [`SessionRuntime::new`] to ensure commands operate
148 /// on per-client mode, cursor, and extension state, enabling multi-client isolation.
149 ///
150 /// # Arguments
151 ///
152 /// * `id` - The command ID to execute
153 /// * `driver_session` - Driver session (for shared state like buffers)
154 /// * `client` - Per-client state bundle (mode, windows, extensions, registers, etc.)
155 /// * `kernel` - Kernel context (buffers, event bus, options)
156 /// * `vfs` - VFS driver for file operations
157 /// * `args` - Command arguments (count, register, etc.)
158 /// * `shared_extensions` - Optional shared extension map for cross-client state (#543)
159 #[must_use]
160 #[allow(clippy::too_many_arguments)] // bundled via ClientContext, remaining are distinct concerns
161 pub fn execute_for_client(
162 &self,
163 client_id: usize,
164 id: &CommandId,
165 driver_session: &mut DriverSession,
166 client: reovim_driver_session::ClientContext<'_>,
167 kernel: &KernelContext,
168 vfs: &Arc<dyn VfsDriver>,
169 args: &CommandContext,
170 shared_extensions: Option<&mut reovim_driver_session::ExtensionMap>,
171 ) -> Option<(
172 CommandResult,
173 reovim_driver_session::api::StateChanges,
174 Vec<reovim_driver_command_types::RuntimeSignal>,
175 )> {
176 use reovim_driver_session::{ClientId as DriverClientId, api::ChangeTracker};
177 profile_scope!("command_execute_for_client", "server::command");
178
179 self.entries.get(id).map(|entry| {
180 // Single clone point for context enrichment (Epic #415)
181 let mut ctx = args.clone();
182 // Per-client active_buffer (#471)
183 if let Some(buffer_id) = *client.active_buffer {
184 ctx.set_buffer_id(buffer_id);
185 }
186 ctx.set_vfs(Arc::clone(vfs));
187
188 // Create SessionRuntime with per-client state and real executor (#471, #477, #515, #547)
189 // The owner enables undo_mine()/redo_mine() for per-client undo
190 // Passing `self` (CommandRegistry) enables re-entrant command execution
191 let driver_client_id = DriverClientId::new(client_id);
192 let mut runtime =
193 SessionRuntime::with_owner(driver_client_id, driver_session, client, kernel, self);
194
195 // Wire up session-wide shared extensions (#543)
196 if let Some(ext) = shared_extensions {
197 runtime = runtime.with_shared_extensions(ext);
198 }
199
200 // Execute command
201 let result = entry.handler.execute(&mut runtime, &ctx);
202
203 // Take accumulated changes and signals (#547)
204 let changes = runtime.take_changes();
205 let signals = runtime.take_signals();
206
207 (result, changes, signals)
208 })
209 }
210
211 /// Get all registered command IDs.
212 pub fn ids(&self) -> impl Iterator<Item = &CommandId> {
213 self.entries.keys()
214 }
215
216 /// Get the number of registered commands.
217 #[must_use]
218 pub fn len(&self) -> usize {
219 self.entries.len()
220 }
221
222 /// Check if the registry is empty.
223 #[must_use]
224 pub fn is_empty(&self) -> bool {
225 self.entries.is_empty()
226 }
227
228 /// Build a [`CommandNameIndex`] from this registry.
229 ///
230 /// Iterates all registered handlers and maps each name alias to
231 /// the command's ID and `Command` trait object. The resulting index
232 /// is stored in `ServiceRegistry` for vim dispatch (#547).
233 #[must_use]
234 pub fn build_name_index(&self) -> reovim_driver_command::CommandNameIndex {
235 let mut index = reovim_driver_command::CommandNameIndex::new();
236 for entry in self.entries.values() {
237 let id = entry.handler.id();
238 let handler = Arc::clone(&entry.handler);
239 let cmd: Arc<dyn reovim_driver_command::Command> = handler;
240 for &name in cmd.names() {
241 index.insert(name.to_string(), id.clone(), Arc::clone(&cmd));
242 }
243 }
244 index
245 }
246
247 /// Get all command infos for query service.
248 ///
249 /// Used by [`CommandQuerySnapshot`] to capture command metadata.
250 #[must_use]
251 pub fn all_command_infos(&self) -> Vec<CommandInfo> {
252 self.entries
253 .values()
254 .map(|entry| CommandInfo::from_command(&*entry.handler))
255 .collect()
256 }
257}
258
259impl std::fmt::Debug for CommandRegistry {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 f.debug_struct("CommandRegistry")
262 .field("count", &self.entries.len())
263 .field("commands", &self.entries.keys().collect::<Vec<_>>())
264 .finish()
265 }
266}
267
268// ============================================================================
269// CommandQuerySnapshot - Query Service Implementation (#453)
270// ============================================================================
271
272/// Snapshot of command metadata for query service.
273///
274/// Captures all command info at bootstrap time for module queries.
275/// Commands are static after module loading, so snapshot is sufficient.
276pub struct CommandQuerySnapshot {
277 commands: Vec<CommandInfo>,
278}
279
280impl Service for CommandQuerySnapshot {}
281
282impl CommandQuerySnapshot {
283 /// Create snapshot from `CommandRegistry`.
284 ///
285 /// Captures all command metadata at the time of creation.
286 #[must_use]
287 pub fn from_registry(registry: &CommandRegistry) -> Self {
288 Self {
289 commands: registry.all_command_infos(),
290 }
291 }
292}
293
294impl CommandQueryService for CommandQuerySnapshot {
295 fn search_by_prefix(&self, prefix: &str) -> Vec<CommandInfo> {
296 self.commands
297 .iter()
298 .filter(|info| info.names.iter().any(|n| n.starts_with(prefix)))
299 .cloned()
300 .collect()
301 }
302
303 fn find_by_name(&self, name: &str) -> Option<CommandInfo> {
304 self.commands
305 .iter()
306 .find(|info| info.names.iter().any(|n| n == name))
307 .cloned()
308 }
309
310 fn list_user_commands(&self) -> Vec<CommandInfo> {
311 self.commands
312 .iter()
313 .filter(|info| !info.names.is_empty())
314 .cloned()
315 .collect()
316 }
317
318 fn list_all(&self) -> Vec<CommandInfo> {
319 self.commands.clone()
320 }
321
322 fn count(&self) -> usize {
323 self.commands.len()
324 }
325}
326
327// === HandlerBridge: CommandHandler -> CommandHandle (#547) ===
328
329/// Bridge from `CommandHandler` (command crate) to `CommandHandle` (session crate).
330///
331/// Wraps an `Arc<dyn CommandHandler>` so it can be returned from
332/// `CommandExecutor::get_handle()`. This breaks the session -> command
333/// dependency cycle while enabling re-entrant command execution.
334struct HandlerBridge(Arc<dyn CommandHandler>);
335
336impl CommandHandle for HandlerBridge {
337 fn execute(&self, runtime: &mut SessionRuntime<'_>, ctx: &CommandContext) -> CommandResult {
338 self.0.execute(runtime, ctx)
339 }
340}
341
342// === CommandExecutor implementation for CommandRegistry ===
343
344impl CommandExecutor for CommandRegistry {
345 fn get_handle(&self, id: &CommandId) -> Option<Arc<dyn CommandHandle>> {
346 self.entries.get(id).map(|entry| {
347 Arc::new(HandlerBridge(Arc::clone(&entry.handler))) as Arc<dyn CommandHandle>
348 })
349 }
350}
351
352#[cfg(test)]
353#[path = "command_tests.rs"]
354mod tests;