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;