Skip to main content

reovim_driver_command/
name_index.rs

1//! Name-based command index for ex-command resolution.
2//!
3//! Maps command names ("w", "write", "q") to `CommandId`s.
4//! Built at bootstrap from `CommandRegistry`, stored in `ServiceRegistry`.
5
6use {
7    crate::Command,
8    reovim_kernel::api::v1::{CommandId, Service},
9    std::{collections::HashMap, fmt, sync::Arc},
10};
11
12/// Error returned when a prefix matches multiple distinct commands.
13///
14/// For example, if `:s` could match both `:set` and `:split`, the user
15/// must type more characters to disambiguate.
16#[derive(Debug, Clone)]
17pub struct AmbiguousPrefix {
18    /// The prefix that was searched for.
19    pub prefix: String,
20    /// The candidate command names that matched.
21    pub candidates: Vec<String>,
22}
23
24impl fmt::Display for AmbiguousPrefix {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        write!(f, "E464: Ambiguous use of user-defined command: {}", self.prefix)
27    }
28}
29
30impl std::error::Error for AmbiguousPrefix {}
31
32/// Name-based command index for ex-command resolution.
33///
34/// Maps command names to their `CommandId` and the underlying `Command`
35/// trait object (for `complete()` delegation).
36///
37/// # Lifecycle
38///
39/// 1. Server builds this at bootstrap from `CommandRegistry`
40/// 2. Stored in `ServiceRegistry` as `Arc<CommandNameIndex>`
41/// 3. Modules query it for name→id resolution and tab-completion
42pub struct CommandNameIndex {
43    /// Name → (`CommandId`, `Command` trait object) for lookup and completion.
44    by_name: HashMap<String, (CommandId, Arc<dyn Command>)>,
45}
46
47impl CommandNameIndex {
48    /// Create an empty index.
49    #[must_use]
50    pub fn new() -> Self {
51        Self {
52            by_name: HashMap::new(),
53        }
54    }
55
56    /// Insert a command by name.
57    ///
58    /// Each name (alias) maps to the same `CommandId` and `Command`.
59    /// Last-wins if the same name is inserted twice.
60    pub fn insert(&mut self, name: String, id: CommandId, cmd: Arc<dyn Command>) {
61        self.by_name.insert(name, (id, cmd));
62    }
63
64    /// Resolve a command name to its `CommandId`.
65    #[must_use]
66    pub fn resolve(&self, name: &str) -> Option<&CommandId> {
67        self.by_name.get(name).map(|(id, _)| id)
68    }
69
70    /// Resolve a command name to its `CommandId` and `Command` trait object.
71    ///
72    /// Unlike [`resolve()`](Self::resolve), this returns the full entry so
73    /// callers can access `Command::args()` for spec-driven argument binding.
74    #[must_use]
75    pub fn resolve_entry(&self, name: &str) -> Option<(&CommandId, &dyn Command)> {
76        self.by_name.get(name).map(|(id, cmd)| (id, cmd.as_ref()))
77    }
78
79    /// Resolve a command by exact name or unambiguous prefix.
80    ///
81    /// Resolution order:
82    /// 1. Exact match — returned immediately (highest priority)
83    /// 2. Prefix search — if exactly one distinct command matches, return it
84    /// 3. Multiple aliases of the same command are deduplicated (not ambiguous)
85    /// 4. Multiple distinct commands — return [`AmbiguousPrefix`] error
86    ///
87    /// Returns `Ok(None)` for empty input or no matches.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`AmbiguousPrefix`] when the prefix matches two or more
92    /// distinct commands (e.g., `:s` matching both `:set` and `:split`).
93    pub fn resolve_prefix(
94        &self,
95        name: &str,
96    ) -> Result<Option<(&CommandId, &dyn Command)>, AmbiguousPrefix> {
97        if name.is_empty() {
98            return Ok(None);
99        }
100
101        // Exact match has highest priority
102        if let Some(entry) = self.by_name.get(name) {
103            return Ok(Some((&entry.0, entry.1.as_ref())));
104        }
105
106        // Prefix search with deduplication by CommandId
107        let matches = self.search_by_prefix(name);
108        match matches.len() {
109            0 => Ok(None),
110            1 => Ok(Some(matches[0])),
111            _ => {
112                // search_by_prefix() already deduplicates by CommandId,
113                // so 2+ results means genuinely distinct commands.
114                let mut candidates: Vec<String> = self
115                    .by_name
116                    .iter()
117                    .filter(|(n, _)| n.starts_with(name))
118                    .map(|(n, _)| n.clone())
119                    .collect();
120                candidates.sort();
121                candidates.dedup();
122                Err(AmbiguousPrefix {
123                    prefix: name.to_string(),
124                    candidates,
125                })
126            }
127        }
128    }
129
130    /// Get argument completions for a named command.
131    ///
132    /// Delegates to the command's `complete()` method.
133    #[must_use]
134    pub fn complete_args(&self, name: &str, partial: &str) -> Vec<String> {
135        self.by_name
136            .get(name)
137            .map_or_else(Vec::new, |(_, cmd)| cmd.complete(partial))
138    }
139
140    /// Search for commands whose names start with `prefix`.
141    ///
142    /// Returns deduplicated results (one entry per unique `CommandId`).
143    #[must_use]
144    pub fn search_by_prefix(&self, prefix: &str) -> Vec<(&CommandId, &dyn Command)> {
145        let mut seen = std::collections::HashSet::new();
146        let mut results = Vec::new();
147        for (name, (id, cmd)) in &self.by_name {
148            if name.starts_with(prefix) && seen.insert(id) {
149                results.push((id, cmd.as_ref()));
150            }
151        }
152        results
153    }
154
155    /// List all unique commands in the index.
156    #[must_use]
157    pub fn list_all(&self) -> Vec<(&CommandId, &dyn Command)> {
158        let mut seen = std::collections::HashSet::new();
159        self.by_name
160            .values()
161            .filter(|(id, _)| seen.insert(id))
162            .map(|(id, cmd)| (id, cmd.as_ref()))
163            .collect()
164    }
165
166    /// Get the number of unique commands (not aliases).
167    #[must_use]
168    pub fn count(&self) -> usize {
169        let seen: std::collections::HashSet<_> = self.by_name.values().map(|(id, _)| id).collect();
170        seen.len()
171    }
172}
173
174impl Default for CommandNameIndex {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180impl Service for CommandNameIndex {}
181
182impl std::fmt::Debug for CommandNameIndex {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        f.debug_struct("CommandNameIndex")
185            .field("name_count", &self.by_name.len())
186            .field("unique_commands", &self.count())
187            .finish()
188    }
189}
190
191#[cfg(test)]
192#[path = "name_index_tests.rs"]
193mod tests;