Rust · 10203 bytes Raw Blame History
1 //! Command-specific completion specifications
2 //!
3 //! This module provides a fish-style completion system where users can define
4 //! custom completions for specific commands using the `complete` builtin.
5 //!
6 //! Example usage:
7 //! ```bash
8 //! # Add subcommand completions for git
9 //! complete -c git -a "add commit push pull fetch checkout branch"
10 //!
11 //! # Add dynamic branch completions for git checkout
12 //! complete -c git -n "__rush_seen_subcommand checkout" -a "(git branch --format='%(refname:short)')"
13 //!
14 //! # Add option completions
15 //! complete -c ls -s l -d "Long format"
16 //! complete -c ls -s a -l all -d "Show hidden files"
17 //!
18 //! # Disable file completion for a command
19 //! complete -c git -f
20 //! ```
21
22 use std::collections::HashMap;
23 use std::sync::RwLock;
24
25 /// Global registry of completion specifications
26 static COMPLETION_REGISTRY: RwLock<Option<CompletionRegistry>> = RwLock::new(None);
27
28 /// A single completion specification
29 #[derive(Debug, Clone)]
30 pub struct CompletionSpec {
31 /// The command this completion applies to
32 pub command: String,
33
34 /// Condition that must be true for this completion to apply
35 /// This is a shell expression that will be evaluated
36 pub condition: Option<String>,
37
38 /// The completions to provide
39 pub source: CompletionSource,
40
41 /// Description for these completions
42 pub description: Option<String>,
43
44 /// If true, don't also complete files
45 pub no_files: bool,
46 }
47
48 /// Source of completion values
49 #[derive(Debug, Clone)]
50 pub enum CompletionSource {
51 /// Static list of completion strings
52 Static(Vec<String>),
53
54 /// Dynamic command to run (output lines become completions)
55 Dynamic(String),
56
57 /// Short option (e.g., -v)
58 ShortOption(char),
59
60 /// Long option (e.g., --verbose)
61 LongOption(String),
62
63 /// Short and long option together (e.g., -v/--verbose)
64 Option {
65 short: Option<char>,
66 long: Option<String>,
67 },
68 }
69
70 /// Registry holding all completion specifications
71 #[derive(Debug, Default)]
72 pub struct CompletionRegistry {
73 /// Completions indexed by command name
74 specs: HashMap<String, Vec<CompletionSpec>>,
75
76 /// Commands that should not have file completion
77 no_file_commands: std::collections::HashSet<String>,
78 }
79
80 impl CompletionRegistry {
81 pub fn new() -> Self {
82 Self::default()
83 }
84
85 /// Add a completion specification
86 pub fn add(&mut self, spec: CompletionSpec) {
87 if spec.no_files {
88 self.no_file_commands.insert(spec.command.clone());
89 }
90 self.specs
91 .entry(spec.command.clone())
92 .or_default()
93 .push(spec);
94 }
95
96 /// Remove all completions for a command
97 pub fn remove(&mut self, command: &str) {
98 self.specs.remove(command);
99 self.no_file_commands.remove(command);
100 }
101
102 /// Get completions for a command
103 pub fn get(&self, command: &str) -> Option<&Vec<CompletionSpec>> {
104 self.specs.get(command)
105 }
106
107 /// Check if a command has file completion disabled
108 pub fn has_no_files(&self, command: &str) -> bool {
109 self.no_file_commands.contains(command)
110 }
111
112 /// Get all registered commands
113 pub fn commands(&self) -> Vec<&String> {
114 self.specs.keys().collect()
115 }
116
117 /// Get all specs (for listing)
118 pub fn all_specs(&self) -> &HashMap<String, Vec<CompletionSpec>> {
119 &self.specs
120 }
121 }
122
123 /// Initialize the global completion registry
124 pub fn init_registry() {
125 let mut registry = COMPLETION_REGISTRY.write().unwrap();
126 if registry.is_none() {
127 let mut reg = CompletionRegistry::new();
128 // Add default completions
129 add_default_completions(&mut reg);
130 *registry = Some(reg);
131 }
132 }
133
134 /// Get a reference to the global registry for reading
135 pub fn with_registry<F, R>(f: F) -> R
136 where
137 F: FnOnce(&CompletionRegistry) -> R,
138 {
139 init_registry();
140 let registry = COMPLETION_REGISTRY.read().unwrap();
141 f(registry.as_ref().unwrap())
142 }
143
144 /// Get a mutable reference to the global registry
145 pub fn with_registry_mut<F, R>(f: F) -> R
146 where
147 F: FnOnce(&mut CompletionRegistry) -> R,
148 {
149 init_registry();
150 let mut registry = COMPLETION_REGISTRY.write().unwrap();
151 f(registry.as_mut().unwrap())
152 }
153
154 /// Add a completion spec to the global registry
155 pub fn add_completion(spec: CompletionSpec) {
156 with_registry_mut(|reg| reg.add(spec));
157 }
158
159 /// Remove completions for a command from the global registry
160 pub fn remove_completions(command: &str) {
161 with_registry_mut(|reg| reg.remove(command));
162 }
163
164 /// Add default completions for common commands
165 fn add_default_completions(registry: &mut CompletionRegistry) {
166 // Git completions
167 add_git_completions(registry);
168
169 // Cargo completions
170 add_cargo_completions(registry);
171 }
172
173 fn add_git_completions(registry: &mut CompletionRegistry) {
174 // Git subcommands
175 let git_subcommands = vec![
176 "add", "bisect", "branch", "checkout", "clone", "commit", "diff",
177 "fetch", "grep", "init", "log", "merge", "mv", "pull", "push",
178 "rebase", "reset", "restore", "rm", "show", "stash", "status",
179 "switch", "tag", "worktree",
180 ];
181
182 registry.add(CompletionSpec {
183 command: "git".to_string(),
184 condition: None,
185 source: CompletionSource::Static(git_subcommands.into_iter().map(String::from).collect()),
186 description: Some("Git subcommand".to_string()),
187 no_files: false,
188 });
189
190 // Git branch completion for checkout/switch
191 registry.add(CompletionSpec {
192 command: "git".to_string(),
193 condition: Some("__rush_git_needs_branch".to_string()),
194 source: CompletionSource::Dynamic("git branch --format='%(refname:short)' 2>/dev/null".to_string()),
195 description: Some("Branch name".to_string()),
196 no_files: true,
197 });
198
199 // Common git options
200 registry.add(CompletionSpec {
201 command: "git".to_string(),
202 condition: None,
203 source: CompletionSource::Option {
204 short: Some('v'),
205 long: Some("verbose".to_string()),
206 },
207 description: Some("Be verbose".to_string()),
208 no_files: false,
209 });
210
211 registry.add(CompletionSpec {
212 command: "git".to_string(),
213 condition: None,
214 source: CompletionSource::Option {
215 short: Some('h'),
216 long: Some("help".to_string()),
217 },
218 description: Some("Show help".to_string()),
219 no_files: false,
220 });
221 }
222
223 fn add_cargo_completions(registry: &mut CompletionRegistry) {
224 // Cargo subcommands
225 let cargo_subcommands = vec![
226 "build", "check", "clean", "doc", "new", "init", "add", "remove",
227 "run", "test", "bench", "update", "search", "publish", "install",
228 "uninstall", "fmt", "clippy", "fix", "tree", "vendor",
229 ];
230
231 registry.add(CompletionSpec {
232 command: "cargo".to_string(),
233 condition: None,
234 source: CompletionSource::Static(cargo_subcommands.into_iter().map(String::from).collect()),
235 description: Some("Cargo subcommand".to_string()),
236 no_files: false,
237 });
238
239 // Common cargo options
240 registry.add(CompletionSpec {
241 command: "cargo".to_string(),
242 condition: None,
243 source: CompletionSource::Option {
244 short: None,
245 long: Some("release".to_string()),
246 },
247 description: Some("Build in release mode".to_string()),
248 no_files: false,
249 });
250
251 registry.add(CompletionSpec {
252 command: "cargo".to_string(),
253 condition: None,
254 source: CompletionSource::Option {
255 short: None,
256 long: Some("all-features".to_string()),
257 },
258 description: Some("Enable all features".to_string()),
259 no_files: false,
260 });
261
262 registry.add(CompletionSpec {
263 command: "cargo".to_string(),
264 condition: None,
265 source: CompletionSource::Option {
266 short: Some('p'),
267 long: Some("package".to_string()),
268 },
269 description: Some("Package to build".to_string()),
270 no_files: false,
271 });
272
273 registry.add(CompletionSpec {
274 command: "cargo".to_string(),
275 condition: None,
276 source: CompletionSource::Option {
277 short: None,
278 long: Some("bin".to_string()),
279 },
280 description: Some("Binary to run".to_string()),
281 no_files: false,
282 });
283 }
284
285 /// Helper struct for building completion specs fluently
286 pub struct CompletionBuilder {
287 spec: CompletionSpec,
288 }
289
290 impl CompletionBuilder {
291 pub fn new(command: &str) -> Self {
292 Self {
293 spec: CompletionSpec {
294 command: command.to_string(),
295 condition: None,
296 source: CompletionSource::Static(vec![]),
297 description: None,
298 no_files: false,
299 },
300 }
301 }
302
303 pub fn condition(mut self, cond: &str) -> Self {
304 self.spec.condition = Some(cond.to_string());
305 self
306 }
307
308 pub fn completions(mut self, completions: Vec<String>) -> Self {
309 self.spec.source = CompletionSource::Static(completions);
310 self
311 }
312
313 pub fn dynamic(mut self, command: &str) -> Self {
314 self.spec.source = CompletionSource::Dynamic(command.to_string());
315 self
316 }
317
318 pub fn short_option(mut self, opt: char) -> Self {
319 self.spec.source = CompletionSource::ShortOption(opt);
320 self
321 }
322
323 pub fn long_option(mut self, opt: &str) -> Self {
324 self.spec.source = CompletionSource::LongOption(opt.to_string());
325 self
326 }
327
328 pub fn option(mut self, short: Option<char>, long: Option<&str>) -> Self {
329 self.spec.source = CompletionSource::Option {
330 short,
331 long: long.map(String::from),
332 };
333 self
334 }
335
336 pub fn description(mut self, desc: &str) -> Self {
337 self.spec.description = Some(desc.to_string());
338 self
339 }
340
341 pub fn no_files(mut self) -> Self {
342 self.spec.no_files = true;
343 self
344 }
345
346 pub fn build(self) -> CompletionSpec {
347 self.spec
348 }
349
350 pub fn register(self) {
351 add_completion(self.build());
352 }
353 }
354