Skip to main content

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;