Skip to main content

clash_brush_core/
completion.rs

1//! Implements programmable command completion support.
2
3use clap::ValueEnum;
4use std::{
5    borrow::Cow,
6    collections::HashMap,
7    path::{Path, PathBuf},
8};
9use strum::IntoEnumIterator;
10
11use crate::{
12    Shell, commands, env, error, escape, expansion, extensions, interfaces, jobs, namedoptions,
13    patterns,
14    sys::{self, users},
15    trace_categories, traps,
16    variables::{self, ShellValueLiteral},
17};
18use brush_parser::unquote_str;
19
20/// Type of action to take to generate completion candidates.
21#[derive(Clone, Debug, ValueEnum)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub enum CompleteAction {
24    /// Complete with valid aliases.
25    #[clap(name = "alias")]
26    Alias,
27    /// Complete with names of array shell variables.
28    #[clap(name = "arrayvar")]
29    ArrayVar,
30    /// Complete with names of key bindings.
31    #[clap(name = "binding")]
32    Binding,
33    /// Complete with names of shell builtins.
34    #[clap(name = "builtin")]
35    Builtin,
36    /// Complete with names of executable commands.
37    #[clap(name = "command")]
38    Command,
39    /// Complete with directory names.
40    #[clap(name = "directory")]
41    Directory,
42    /// Complete with names of disabled shell builtins.
43    #[clap(name = "disabled")]
44    Disabled,
45    /// Complete with names of enabled shell builtins.
46    #[clap(name = "enabled")]
47    Enabled,
48    /// Complete with names of exported shell variables.
49    #[clap(name = "export")]
50    Export,
51    /// Complete with filenames.
52    #[clap(name = "file")]
53    File,
54    /// Complete with names of shell functions.
55    #[clap(name = "function")]
56    Function,
57    /// Complete with valid user groups.
58    #[clap(name = "group")]
59    Group,
60    /// Complete with names of valid shell help topics.
61    #[clap(name = "helptopic")]
62    HelpTopic,
63    /// Complete with the system's hostname(s).
64    #[clap(name = "hostname")]
65    HostName,
66    /// Complete with the command names of shell-managed jobs.
67    #[clap(name = "job")]
68    Job,
69    /// Complete with valid shell keywords.
70    #[clap(name = "keyword")]
71    Keyword,
72    /// Complete with the command names of running shell-managed jobs.
73    #[clap(name = "running")]
74    Running,
75    /// Complete with names of system services.
76    #[clap(name = "service")]
77    Service,
78    /// Complete with the names of options settable via shopt.
79    #[clap(name = "setopt")]
80    SetOpt,
81    /// Complete with the names of options settable via set -o.
82    #[clap(name = "shopt")]
83    ShOpt,
84    /// Complete with the names of trappable signals.
85    #[clap(name = "signal")]
86    Signal,
87    /// Complete with the command names of stopped shell-managed jobs.
88    #[clap(name = "stopped")]
89    Stopped,
90    /// Complete with valid usernames.
91    #[clap(name = "user")]
92    User,
93    /// Complete with names of shell variables.
94    #[clap(name = "variable")]
95    Variable,
96}
97
98/// Options influencing how command completions are generated.
99#[derive(Clone, Debug, Eq, Hash, PartialEq, ValueEnum)]
100pub enum CompleteOption {
101    /// Perform rest of default completions if no completions are generated.
102    #[clap(name = "bashdefault")]
103    BashDefault,
104    /// Use default filename completion if no completions are generated.
105    #[clap(name = "default")]
106    Default,
107    /// Treat completions as directory names.
108    #[clap(name = "dirnames")]
109    DirNames,
110    /// Treat completions as filenames.
111    #[clap(name = "filenames")]
112    FileNames,
113    /// Suppress default auto-quotation of completions.
114    #[clap(name = "noquote")]
115    NoQuote,
116    /// Do not sort completions.
117    #[clap(name = "nosort")]
118    NoSort,
119    /// Do not append a trailing space to completions at the end of the input line.
120    #[clap(name = "nospace")]
121    NoSpace,
122    /// Also generate directory completions.
123    #[clap(name = "plusdirs")]
124    PlusDirs,
125}
126
127/// Encapsulates the shell's programmable command completion configuration.
128#[derive(Clone, Default)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub struct Config {
131    commands: HashMap<String, Spec>,
132
133    /// Optionally, a completion spec to be used as a default, when earlier
134    /// matches yield no candidates.
135    pub default: Option<Spec>,
136    /// Optionally, a completion spec to be used when the command line is empty.
137    pub empty_line: Option<Spec>,
138    /// Optionally, a completion spec to be used for the initial word of a command line.
139    pub initial_word: Option<Spec>,
140
141    /// Optionally, stores the current completion options in effect. May be mutated
142    /// while a completion generation is in-flight.
143    pub current_completion_options: Option<GenerationOptions>,
144
145    /// Fallback options to use when 'default' completions are requested (not to be
146    /// confused with the 'default' completion spec, nor 'bashdefault' completions).
147    pub fallback_options: FallbackOptions,
148}
149
150/// Options for fallback completions.
151#[derive(Clone, Debug)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153pub struct FallbackOptions {
154    /// If true, mark directory completions with a trailing slash.
155    pub mark_directories: bool,
156    /// If true, mark symlinked directory completions with a trailing slash.
157    pub mark_symlinked_directories: bool,
158}
159
160impl Default for FallbackOptions {
161    fn default() -> Self {
162        Self {
163            mark_directories: true,
164            mark_symlinked_directories: false,
165        }
166    }
167}
168
169/// Options for generating completions.
170#[derive(Clone, Debug, Default)]
171#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
172pub struct GenerationOptions {
173    //
174    // Options
175    /// Perform rest of default completions if no completions are generated.
176    pub bash_default: bool,
177    /// Use default readline-style filename completion if no completions are generated.
178    pub default: bool,
179    /// Treat completions as directory names.
180    pub dir_names: bool,
181    /// Treat completions as filenames.
182    pub file_names: bool,
183    /// Do not add usual quoting for completions.
184    pub no_quote: bool,
185    /// Do not sort completions.
186    pub no_sort: bool,
187    /// Do not append typical space to a completion at the end of the input line.
188    pub no_space: bool,
189    /// Also complete with directory names.
190    pub plus_dirs: bool,
191}
192
193/// Encapsulates a command completion specification; provides policy for how to
194/// generate completions for a given input.
195#[derive(Clone, Debug, Default)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
197pub struct Spec {
198    //
199    // Options
200    /// Options to use for completion.
201    pub options: GenerationOptions,
202
203    //
204    // Generators
205    /// Actions to take to generate completions.
206    pub actions: Vec<CompleteAction>,
207    /// Optionally, a glob pattern whose expansion will be used as completions.
208    pub glob_pattern: Option<String>,
209    /// Optionally, a list of words to use as completions.
210    pub word_list: Option<String>,
211    /// Optionally, the name of a shell function to invoke to generate completions.
212    pub function_name: Option<String>,
213    /// Optionally, the name of a command to execute to generate completions.
214    pub command: Option<String>,
215
216    //
217    // Filters
218    /// Optionally, a pattern to filter completions.
219    pub filter_pattern: Option<String>,
220    /// If true, completion candidates matching `filter_pattern` are removed;
221    /// otherwise, those not matching it are removed.
222    pub filter_pattern_excludes: bool,
223
224    //
225    // Transformers
226    /// Optionally, provides a prefix to be prepended to all completion candidates.
227    pub prefix: Option<String>,
228    /// Optionally, provides a suffix to be prepended to all completion candidates.
229    pub suffix: Option<String>,
230}
231
232/// Describes what triggered the completion process.
233#[derive(Clone, Copy, Debug, Default)]
234pub enum CompletionTrigger {
235    /// Interactive completion triggered by Tab key (normal completion).
236    #[default]
237    InteractiveComplete,
238    /// Programmatic generation via the `compgen` builtin.
239    Programmatic,
240}
241
242impl CompletionTrigger {
243    /// Returns the `COMP_TYPE` value for this trigger.
244    pub const fn comp_type(self) -> i32 {
245        match self {
246            Self::InteractiveComplete => 9, // TAB = normal completion
247            Self::Programmatic => 0,
248        }
249    }
250
251    /// Returns the `COMP_KEY` value for this trigger.
252    pub const fn comp_key(self) -> i32 {
253        match self {
254            Self::InteractiveComplete => 9, // TAB key
255            Self::Programmatic => 0,
256        }
257    }
258}
259
260/// Encapsulates context used during completion generation.
261#[derive(Debug)]
262pub struct Context<'a> {
263    /// The token to complete.
264    pub token_to_complete: &'a str,
265
266    /// If available, the name of the command being invoked.
267    pub command_name: Option<&'a str>,
268    /// If there was one, the token preceding the one being completed.
269    pub preceding_token: Option<&'a str>,
270
271    /// The 0-based index of the token to complete.
272    pub token_index: usize,
273
274    /// The input line.
275    pub input_line: &'a str,
276    /// The 0-based index of the cursor in the input line.
277    pub cursor_index: usize,
278    /// The tokens in the input line.
279    pub tokens: &'a [&'a CompletionToken<'a>],
280
281    /// What triggered the completion.
282    pub trigger: CompletionTrigger,
283}
284
285impl Spec {
286    /// Generates completion candidates using this specification.
287    ///
288    /// # Arguments
289    ///
290    /// * `shell` - The shell instance to use for completion generation.
291    /// * `context` - The context in which completion is being generated.
292    #[expect(clippy::too_many_lines)]
293    pub async fn get_completions(
294        &self,
295        shell: &mut Shell<impl extensions::ShellExtensions>,
296        context: &Context<'_>,
297    ) -> Result<Answer, crate::error::Error> {
298        // Store the current options in the shell; this is needed since the compopt
299        // built-in has the ability of modifying the options for an in-flight
300        // completion process.
301        shell.completion_config_mut().current_completion_options = Some(self.options.clone());
302
303        // Generate completions based on any provided actions (and on words).
304        let mut candidates = self.generate_action_completions(shell, context).await?;
305        if let Some(word_list) = &self.word_list {
306            let params = shell.default_exec_params();
307            // Per POSIX / bash docs, -W word list is subject to shell expansion
308            // and field splitting but NOT pathname expansion (globbing).
309            let options = crate::expansion::ExpanderOptions {
310                pathname_expand: false,
311                ..Default::default()
312            };
313            let words = crate::expansion::full_expand_and_split_word_with_options(
314                shell, &params, word_list, &options,
315            )
316            .await?;
317            for word in words {
318                if word.starts_with(context.token_to_complete) {
319                    candidates.push(word);
320                }
321            }
322        }
323
324        if let Some(glob_pattern) = &self.glob_pattern {
325            let pattern = patterns::Pattern::from(glob_pattern.as_str())
326                .set_extended_globbing(shell.options().extended_globbing)
327                .set_case_insensitive(shell.options().case_insensitive_pathname_expansion);
328
329            let expansions = pattern
330                .expand(
331                    shell.working_dir(),
332                    Some(&patterns::Pattern::accept_all_expand_filter),
333                    &patterns::FilenameExpansionOptions::default(),
334                )?
335                .into_paths();
336
337            for expansion in expansions {
338                candidates.push(expansion);
339            }
340        }
341        if let Some(function_name) = &self.function_name {
342            let call_result = self
343                .call_completion_function(shell, function_name.as_str(), context)
344                .await?;
345
346            match call_result {
347                Answer::RestartCompletionProcess => return Ok(call_result),
348                Answer::Candidates(mut new_candidates, _options) => {
349                    candidates.append(&mut new_candidates);
350                }
351            }
352        }
353        if let Some(command) = &self.command {
354            let mut new_candidates = self
355                .call_completion_command(shell, command.as_str(), context)
356                .await?;
357            candidates.append(&mut new_candidates);
358        }
359
360        // Apply filter pattern, if present. Anything the filter selects gets removed.
361        if let Some(filter_pattern) = &self.filter_pattern
362            && !filter_pattern.is_empty()
363        {
364            let mut updated = Vec::new();
365
366            for candidate in candidates {
367                let matches = completion_filter_pattern_matches(
368                    filter_pattern.as_str(),
369                    candidate.as_str(),
370                    context.token_to_complete,
371                    shell,
372                )?;
373
374                if self.filter_pattern_excludes != matches {
375                    updated.push(candidate);
376                }
377            }
378
379            candidates = updated;
380        }
381
382        // Add prefix and/or suffix, if present.
383        if self.prefix.is_some() || self.suffix.is_some() {
384            let empty = String::new();
385            let prefix = self.prefix.as_ref().unwrap_or(&empty);
386            let suffix = self.suffix.as_ref().unwrap_or(&empty);
387
388            let mut updated = Vec::new();
389            for candidate in candidates {
390                updated.push(std::format!("{prefix}{candidate}{suffix}"));
391            }
392
393            candidates = updated;
394        }
395
396        //
397        // Now apply options
398        //
399
400        let options = if let Some(options) = &shell.completion_config().current_completion_options {
401            options
402        } else {
403            &self.options
404        };
405
406        let mut processing_options = ProcessingOptions {
407            treat_as_filenames: options.file_names,
408            no_autoquote_filenames: options.no_quote,
409            no_trailing_space_at_end_of_line: options.no_space,
410        };
411
412        if options.plus_dirs || options.dir_names {
413            // Also add dir name completion.
414            let mut dir_candidates = get_file_completions(
415                shell,
416                context.token_to_complete,
417                /* must_be_dir */ true,
418            )
419            .await;
420            candidates.append(&mut dir_candidates);
421        }
422
423        // If we still have no candidates, and bashdefault completions were requested, then generate
424        // those.
425        if candidates.is_empty() && options.bash_default {
426            // TODO(completions): it's not clear what default "bash" completions means. From basic
427            // testing, this doesn't seem to include basic file and directory name
428            // completion.
429            tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -o bashdefault");
430        }
431
432        // If we still have no candidates, and default completions were requested, then generate
433        // those.
434        if candidates.is_empty() && options.default {
435            // N.B. We approximate "default" readline completion behavior by getting file and
436            // dir completions.
437            let must_be_dir = options.dir_names;
438
439            let mut default_candidates =
440                get_file_completions(shell, context.token_to_complete, must_be_dir).await;
441            candidates.append(&mut default_candidates);
442
443            if shell.completion_config().fallback_options.mark_directories {
444                processing_options.treat_as_filenames = true;
445            }
446        }
447
448        // Sort, unless blocked by options.
449        if !self.options.no_sort {
450            candidates.sort();
451        }
452
453        Ok(Answer::Candidates(candidates, processing_options))
454    }
455
456    #[expect(clippy::too_many_lines)]
457    async fn generate_action_completions(
458        &self,
459        shell: &Shell<impl extensions::ShellExtensions>,
460        context: &Context<'_>,
461    ) -> Result<Vec<String>, error::Error> {
462        let mut candidates = Vec::new();
463
464        let token = context.token_to_complete;
465
466        for action in &self.actions {
467            match action {
468                CompleteAction::Alias => {
469                    for name in shell.aliases().keys() {
470                        if name.starts_with(token) {
471                            candidates.push(name.clone());
472                        }
473                    }
474                }
475                CompleteAction::ArrayVar => {
476                    for (name, var) in shell.env().iter() {
477                        if var.value().is_array() && name.starts_with(token) {
478                            candidates.push(name.to_owned());
479                        }
480                    }
481                }
482                CompleteAction::Binding => {
483                    for input_func in interfaces::InputFunction::iter() {
484                        let name: &'static str = input_func.into();
485                        if name.starts_with(token) {
486                            candidates.push(name.to_string());
487                        }
488                    }
489                }
490                CompleteAction::Builtin => {
491                    for name in shell.builtins().keys() {
492                        if name.starts_with(token) {
493                            candidates.push(name.to_owned());
494                        }
495                    }
496                }
497                CompleteAction::Command => {
498                    let mut command_completions =
499                        get_external_command_completions(shell, context.token_to_complete);
500                    candidates.append(&mut command_completions);
501                }
502                CompleteAction::Directory => {
503                    let mut file_completions =
504                        get_file_completions(shell, context.token_to_complete, true).await;
505                    candidates.append(&mut file_completions);
506                }
507                CompleteAction::Disabled => {
508                    for (name, registration) in shell.builtins() {
509                        if registration.disabled && name.starts_with(token) {
510                            candidates.push(name.to_owned());
511                        }
512                    }
513                }
514                CompleteAction::Enabled => {
515                    for (name, registration) in shell.builtins() {
516                        if !registration.disabled && name.starts_with(token) {
517                            candidates.push(name.to_owned());
518                        }
519                    }
520                }
521                CompleteAction::Export => {
522                    for (key, value) in shell.env().iter() {
523                        if value.is_exported() && key.starts_with(token) {
524                            candidates.push(key.to_owned());
525                        }
526                    }
527                }
528                CompleteAction::File => {
529                    let mut file_completions =
530                        get_file_completions(shell, context.token_to_complete, false).await;
531                    candidates.append(&mut file_completions);
532                }
533                CompleteAction::Function => {
534                    for (name, _) in shell.funcs().iter() {
535                        candidates.push(name.to_owned());
536                    }
537                }
538                CompleteAction::Group => {
539                    for group_name in users::get_all_groups()? {
540                        if group_name.starts_with(token) {
541                            candidates.push(group_name);
542                        }
543                    }
544                }
545                CompleteAction::HelpTopic => {
546                    // For now, we only have help topics for built-in commands.
547                    for name in shell.builtins().keys() {
548                        if name.starts_with(token) {
549                            candidates.push(name.to_owned());
550                        }
551                    }
552                }
553                CompleteAction::HostName => {
554                    // N.B. We only retrieve one hostname.
555                    if let Ok(name) = sys::network::get_hostname() {
556                        let name = name.to_string_lossy();
557                        if name.starts_with(token) {
558                            candidates.push(name.to_string());
559                        }
560                    }
561                }
562                CompleteAction::Job => {
563                    for job in &shell.jobs().jobs {
564                        let command_name = job.command_name();
565                        if command_name.starts_with(token) {
566                            candidates.push(command_name.to_owned());
567                        }
568                    }
569                }
570                CompleteAction::Keyword => {
571                    for keyword in shell.get_keywords() {
572                        if keyword.starts_with(token) {
573                            candidates.push(keyword.to_string());
574                        }
575                    }
576                }
577                CompleteAction::Running => {
578                    for job in &shell.jobs().jobs {
579                        if matches!(job.state, jobs::JobState::Running) {
580                            let command_name = job.command_name();
581                            if command_name.starts_with(token) {
582                                candidates.push(command_name.to_owned());
583                            }
584                        }
585                    }
586                }
587                CompleteAction::Service => {
588                    tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
589                }
590                CompleteAction::SetOpt => {
591                    for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter()
592                    {
593                        if option.name.starts_with(token) {
594                            candidates.push(option.name.to_owned());
595                        }
596                    }
597                }
598                CompleteAction::ShOpt => {
599                    for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter()
600                    {
601                        if option.name.starts_with(token) {
602                            candidates.push(option.name.to_owned());
603                        }
604                    }
605                }
606                CompleteAction::Signal => {
607                    for signal in traps::TrapSignal::iterator() {
608                        if signal.as_str().starts_with(token) {
609                            candidates.push(signal.as_str().to_string());
610                        }
611                    }
612                }
613                CompleteAction::Stopped => {
614                    for job in &shell.jobs().jobs {
615                        if matches!(job.state, jobs::JobState::Stopped) {
616                            let command_name = job.command_name();
617                            if command_name.starts_with(token) {
618                                candidates.push(job.command_name().to_owned());
619                            }
620                        }
621                    }
622                }
623                CompleteAction::User => {
624                    for user_name in users::get_all_users()? {
625                        if user_name.starts_with(token) {
626                            candidates.push(user_name);
627                        }
628                    }
629                }
630                CompleteAction::Variable => {
631                    for (key, _) in shell.env().iter() {
632                        if key.starts_with(token) {
633                            candidates.push(key.to_owned());
634                        }
635                    }
636                }
637            }
638        }
639
640        Ok(candidates)
641    }
642
643    async fn call_completion_command(
644        &self,
645        shell: &Shell<impl extensions::ShellExtensions>,
646        command_name: &str,
647        context: &Context<'_>,
648    ) -> Result<Vec<String>, error::Error> {
649        // Move to a subshell so we can start filling out variables.
650        let mut shell = shell.clone();
651
652        let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
653            ("COMP_LINE", context.input_line.into()),
654            ("COMP_POINT", context.cursor_index.to_string().into()),
655            ("COMP_KEY", context.trigger.comp_key().to_string().into()),
656            ("COMP_TYPE", context.trigger.comp_type().to_string().into()),
657        ];
658
659        // Fill out variables.
660        for (var, value) in vars_and_values {
661            shell.env_mut().update_or_add(
662                var,
663                value,
664                |v| {
665                    v.export();
666                    Ok(())
667                },
668                env::EnvironmentLookup::Anywhere,
669                env::EnvironmentScope::Global,
670            )?;
671        }
672
673        // Compute args.
674        let mut args = vec![
675            context.command_name.unwrap_or(""),
676            context.token_to_complete,
677        ];
678        if let Some(preceding_token) = context.preceding_token {
679            args.push(preceding_token);
680        }
681
682        // Compose the full command line.
683        let mut command_line = command_name.to_owned();
684        for arg in args {
685            command_line.push(' ');
686
687            let escaped_arg = escape::quote_if_needed(arg, escape::QuoteMode::SingleQuote);
688            command_line.push_str(escaped_arg.as_ref());
689        }
690
691        // Run the command.
692        let params = shell.default_exec_params();
693        let output =
694            commands::invoke_command_in_subshell_and_get_output(&mut shell, &params, command_line)
695                .await?;
696
697        // Split results.
698        let mut candidates = Vec::new();
699        for line in output.lines() {
700            candidates.push(line.to_owned());
701        }
702
703        Ok(candidates)
704    }
705
706    async fn call_completion_function(
707        &self,
708        shell: &mut Shell<impl extensions::ShellExtensions>,
709        function_name: &str,
710        context: &Context<'_>,
711    ) -> Result<Answer, error::Error> {
712        // TODO(completions): Don't pollute the persistent environment with these?
713        let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
714            ("COMP_LINE", context.input_line.into()),
715            ("COMP_POINT", context.cursor_index.to_string().into()),
716            ("COMP_KEY", context.trigger.comp_key().to_string().into()),
717            ("COMP_TYPE", context.trigger.comp_type().to_string().into()),
718            (
719                "COMP_WORDS",
720                context
721                    .tokens
722                    .iter()
723                    .map(|t| t.text)
724                    .collect::<Vec<_>>()
725                    .into(),
726            ),
727            ("COMP_CWORD", context.token_index.to_string().into()),
728        ];
729
730        tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
731            vars_and_values.iter().map(|(k, v)| std::format!("{k}={v}")).collect::<Vec<String>>().join(" "));
732
733        let mut vars_to_remove = Vec::with_capacity(vars_and_values.len());
734        for (var, value) in vars_and_values {
735            shell.env_mut().update_or_add(
736                var,
737                value,
738                |_| Ok(()),
739                env::EnvironmentLookup::Anywhere,
740                env::EnvironmentScope::Global,
741            )?;
742
743            vars_to_remove.push(var);
744        }
745
746        let mut args = vec![
747            context.command_name.unwrap_or(""),
748            context.token_to_complete,
749        ];
750        if let Some(preceding_token) = context.preceding_token {
751            args.push(preceding_token);
752        }
753
754        // TODO(completions): Find a more appropriate interlock here. For now we use the existing
755        // handler depth count to suppress any debug traps.
756        shell.enter_trap_handler(None);
757
758        let params = shell.default_exec_params();
759        let invoke_result = shell
760            .invoke_function(function_name, args.iter(), &params)
761            .await;
762        tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
763
764        shell.leave_trap_handler();
765
766        // Make a best-effort attempt to unset the temporary variables.
767        for var_name in vars_to_remove {
768            let _ = shell.env_mut().unset(var_name);
769        }
770
771        let result = invoke_result.unwrap_or_else(|e| {
772            tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
773            1 // Report back a non-zero exit code.
774        });
775
776        // When the function returns the special value 124, then it's a request
777        // for us to restart the completion process.
778        if result == 124 {
779            Ok(Answer::RestartCompletionProcess)
780        } else {
781            if let Some(reply) = shell.env_mut().unset("COMPREPLY")? {
782                tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
783
784                match reply.value() {
785                    variables::ShellValue::IndexedArray(values) => {
786                        return Ok(Answer::Candidates(
787                            values.values().map(|v| v.to_owned()).collect(),
788                            ProcessingOptions::default(),
789                        ));
790                    }
791                    variables::ShellValue::String(s) => {
792                        let candidates = vec![s.to_owned()];
793                        return Ok(Answer::Candidates(candidates, ProcessingOptions::default()));
794                    }
795                    _ => (),
796                }
797            }
798
799            Ok(Answer::Candidates(Vec::new(), ProcessingOptions::default()))
800        }
801    }
802}
803
804/// Represents a set of generated command completions.
805#[derive(Debug, Default)]
806pub struct Completions {
807    /// The index in the input line where the completions should be inserted. Represented
808    /// as a byte offset into the input line; must be at a clean character boundary.
809    pub insertion_index: usize,
810    /// The number of elements in the input line that should be removed before insertion.
811    /// Represented as a byte count; must capture an exact character boundary.
812    pub delete_count: usize,
813    /// The ordered set of completions.
814    pub candidates: Vec<String>,
815    /// Options for processing the candidates.
816    pub options: ProcessingOptions,
817}
818
819/// Options governing how command completion candidates are processed after being generated.
820#[derive(Debug)]
821pub struct ProcessingOptions {
822    /// Treat completions as file names.
823    pub treat_as_filenames: bool,
824    /// Don't auto-quote completions that are file names.
825    pub no_autoquote_filenames: bool,
826    /// Don't append a trailing space to completions at the end of the input line.
827    pub no_trailing_space_at_end_of_line: bool,
828}
829
830/// Represents a token in the input line being completed.
831#[derive(Debug, Clone, Copy)]
832pub struct CompletionToken<'a> {
833    /// The text of the token.
834    pub text: &'a str,
835    /// The start of the token, expressed as a byte offset into the input line.
836    pub start: usize,
837}
838
839impl CompletionToken<'_> {
840    /// Returns the length of the token, expressed as a byte count.
841    pub const fn length(&self) -> usize {
842        self.text.len()
843    }
844
845    /// Returns the end of the token, expressed as a byte offset into the input line.
846    pub const fn end(&self) -> usize {
847        self.start + self.length()
848    }
849}
850
851impl Default for ProcessingOptions {
852    fn default() -> Self {
853        Self {
854            treat_as_filenames: true,
855            no_autoquote_filenames: false,
856            no_trailing_space_at_end_of_line: false,
857        }
858    }
859}
860
861/// Encapsulates a completion answer.
862pub enum Answer {
863    /// The completion process generated a set of candidates along with options
864    /// controlling how to process them.
865    Candidates(Vec<String>, ProcessingOptions),
866    /// The completion process needs to be restarted.
867    RestartCompletionProcess,
868}
869
870const EMPTY_COMMAND: &str = "_EmptycmD_";
871const DEFAULT_COMMAND: &str = "_DefaultCmD_";
872const INITIAL_WORD: &str = "_InitialWorD_";
873
874impl Config {
875    /// Removes all registered completion specs.
876    pub fn clear(&mut self) {
877        self.commands.clear();
878        self.empty_line = None;
879        self.default = None;
880        self.initial_word = None;
881    }
882
883    /// Ensures the named completion spec is no longer registered; returns whether a
884    /// removal operation was required.
885    ///
886    /// # Arguments
887    ///
888    /// * `name` - The name of the completion spec to remove.
889    pub fn remove(&mut self, name: &str) -> bool {
890        match name {
891            EMPTY_COMMAND => {
892                let result = self.empty_line.is_some();
893                self.empty_line = None;
894                result
895            }
896            DEFAULT_COMMAND => {
897                let result = self.default.is_some();
898                self.default = None;
899                result
900            }
901            INITIAL_WORD => {
902                let result = self.initial_word.is_some();
903                self.initial_word = None;
904                result
905            }
906            _ => self.commands.remove(name).is_some(),
907        }
908    }
909
910    /// Returns an iterator over the completion specs.
911    pub fn iter(&self) -> impl Iterator<Item = (&String, &Spec)> {
912        self.commands.iter()
913    }
914
915    /// If present, returns the completion spec for the command of the given name.
916    ///
917    /// # Arguments
918    ///
919    /// * `name` - The name of the command.
920    pub fn get(&self, name: &str) -> Option<&Spec> {
921        match name {
922            EMPTY_COMMAND => self.empty_line.as_ref(),
923            DEFAULT_COMMAND => self.default.as_ref(),
924            INITIAL_WORD => self.initial_word.as_ref(),
925            _ => self.commands.get(name),
926        }
927    }
928
929    /// If present, sets the provided completion spec to be associated with the
930    /// command of the given name.
931    ///
932    /// # Arguments
933    ///
934    /// * `name` - The name of the command.
935    /// * `spec` - The completion spec to associate with the command.
936    pub fn set(&mut self, name: &str, spec: Spec) {
937        match name {
938            EMPTY_COMMAND => {
939                self.empty_line = Some(spec);
940            }
941            DEFAULT_COMMAND => {
942                self.default = Some(spec);
943            }
944            INITIAL_WORD => {
945                self.initial_word = Some(spec);
946            }
947            _ => {
948                self.commands.insert(name.to_owned(), spec);
949            }
950        }
951    }
952
953    /// Returns a mutable reference to the completion spec for the command of the
954    /// given name; if the command already was associated with a spec, returns
955    /// a reference to that existing spec. Otherwise registers a new default
956    /// spec and returns a mutable reference to it.
957    ///
958    /// # Arguments
959    ///
960    /// * `name` - The name of the command.
961    #[allow(
962        clippy::missing_panics_doc,
963        clippy::unwrap_used,
964        reason = "these unwrap calls should not fail"
965    )]
966    pub fn get_or_add_mut(&mut self, name: &str) -> &mut Spec {
967        match name {
968            EMPTY_COMMAND => {
969                if self.empty_line.is_none() {
970                    self.empty_line = Some(Spec::default());
971                }
972                self.empty_line.as_mut().unwrap()
973            }
974            DEFAULT_COMMAND => {
975                if self.default.is_none() {
976                    self.default = Some(Spec::default());
977                }
978                self.default.as_mut().unwrap()
979            }
980            INITIAL_WORD => {
981                if self.initial_word.is_none() {
982                    self.initial_word = Some(Spec::default());
983                }
984                self.initial_word.as_mut().unwrap()
985            }
986            _ => self.commands.entry(name.to_owned()).or_default(),
987        }
988    }
989
990    /// Generates completions for the given input line and cursor position.
991    ///
992    /// # Arguments
993    ///
994    /// * `shell` - The shell instance to use for completion generation.
995    /// * `input` - The input line for which completions are being generated.
996    /// * `position` - The 0-based index of the cursor in the input line.
997    #[expect(clippy::string_slice)]
998    pub async fn get_completions(
999        &self,
1000        shell: &mut Shell<impl extensions::ShellExtensions>,
1001        input: &str,
1002        position: usize,
1003    ) -> Result<Completions, error::Error> {
1004        const MAX_RESTARTS: u32 = 10;
1005
1006        // Make a best-effort attempt to tokenize.
1007        let tokens = Self::tokenize_input_for_completion(shell, input);
1008
1009        let cursor = position;
1010        let mut preceding_token = None;
1011        let mut completion_prefix = "";
1012        let mut insertion_index = cursor;
1013        let mut completion_token_index = tokens.len();
1014
1015        // Copy a set of references to the tokens; we will adjust this list as
1016        // we find we need to insert an empty token.
1017        let mut adjusted_tokens: Vec<&CompletionToken<'_>> = tokens.iter().collect();
1018
1019        // Try to find which token we are in.
1020        for (i, token) in tokens.iter().enumerate() {
1021            // If the cursor is before the start of the token, then it's between
1022            // this token and the one that preceded it (or it's before the first
1023            // token if this is the first token).
1024            if cursor < token.start {
1025                // TODO(completions): Should insert an empty token here; the position looks to have
1026                // been between this token and the preceding one.
1027                completion_token_index = i;
1028                break;
1029            }
1030            // If the cursor is anywhere from the first char of the token up to
1031            // (and including) the first char after the token, then this we need
1032            // to generate completions to replace/update this token. We'll pay
1033            // attention to the position to figure out the prefix that we should
1034            // be completing.
1035            else if cursor >= token.start && cursor <= token.end() {
1036                // Update insertion index.
1037                insertion_index = token.start;
1038
1039                // Update prefix.
1040                let offset_into_token = cursor - insertion_index;
1041                let token_str = token.text;
1042                completion_prefix = &token_str[..offset_into_token];
1043
1044                // Update token index.
1045                completion_token_index = i;
1046
1047                break;
1048            }
1049
1050            // Otherwise, we need to keep looking. Update what we think the
1051            // preceding token may be.
1052            preceding_token = Some(token);
1053        }
1054
1055        // If the position is after the last token, then we need to insert an empty
1056        // token for the new token to be generated.
1057        let empty_token = CompletionToken {
1058            text: "",
1059            start: input.len(),
1060        };
1061        if completion_token_index == tokens.len() {
1062            adjusted_tokens.push(&empty_token);
1063        }
1064
1065        // Get the completions.
1066        let mut result = Answer::RestartCompletionProcess;
1067        let mut restart_count = 0;
1068        while matches!(result, Answer::RestartCompletionProcess) {
1069            if restart_count > MAX_RESTARTS {
1070                tracing::warn!("possible infinite loop detected in completion process");
1071                break;
1072            }
1073
1074            let completion_context = Context {
1075                token_to_complete: completion_prefix,
1076                preceding_token: preceding_token.map(|t| t.text),
1077                command_name: adjusted_tokens.first().map(|token| token.text),
1078                input_line: input,
1079                token_index: completion_token_index,
1080                tokens: adjusted_tokens.as_slice(),
1081                cursor_index: position,
1082                trigger: CompletionTrigger::InteractiveComplete,
1083            };
1084
1085            result = self
1086                .get_completions_for_token(shell, completion_context)
1087                .await;
1088
1089            restart_count += 1;
1090        }
1091
1092        match result {
1093            Answer::Candidates(candidates, options) => Ok(Completions {
1094                insertion_index,
1095                delete_count: completion_prefix.len(),
1096                candidates,
1097                options,
1098            }),
1099            Answer::RestartCompletionProcess => Ok(Completions {
1100                insertion_index,
1101                delete_count: 0,
1102                candidates: Vec::new(),
1103                options: ProcessingOptions::default(),
1104            }),
1105        }
1106    }
1107
1108    fn tokenize_input_for_completion<'a>(
1109        shell: &Shell<impl extensions::ShellExtensions>,
1110        input: &'a str,
1111    ) -> Vec<CompletionToken<'a>> {
1112        const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
1113
1114        let delimiter_str = shell
1115            .env_str("COMP_WORDBREAKS")
1116            .unwrap_or_else(|| FALLBACK.into());
1117
1118        let delimiters: Vec<_> = delimiter_str.chars().collect();
1119
1120        simple_tokenize_by_delimiters(input, delimiters.as_slice())
1121    }
1122
1123    async fn get_completions_for_token(
1124        &self,
1125        shell: &mut Shell<impl extensions::ShellExtensions>,
1126        context: Context<'_>,
1127    ) -> Answer {
1128        // See if we can find a completion spec matching the current command.
1129        let mut found_spec: Option<&Spec> = None;
1130
1131        if let Some(command_name) = context.command_name {
1132            if context.token_index == 0 {
1133                if let Some(spec) = &self.initial_word {
1134                    found_spec = Some(spec);
1135                }
1136            } else {
1137                if let Some(spec) = shell.completion_config().commands.get(command_name) {
1138                    found_spec = Some(spec);
1139                } else if let Some(file_name) = PathBuf::from(command_name).file_name()
1140                    && let Some(spec) = shell
1141                        .completion_config()
1142                        .commands
1143                        .get(&file_name.to_string_lossy().to_string())
1144                {
1145                    found_spec = Some(spec);
1146                }
1147
1148                if found_spec.is_none()
1149                    && let Some(spec) = &self.default
1150                {
1151                    found_spec = Some(spec);
1152                }
1153            }
1154        } else {
1155            if let Some(spec) = &self.empty_line {
1156                found_spec = Some(spec);
1157            }
1158        }
1159
1160        // Try to generate completions.
1161        if let Some(spec) = found_spec {
1162            spec.to_owned()
1163                .get_completions(shell, &context)
1164                .await
1165                .unwrap_or_else(|_err| Answer::Candidates(Vec::new(), ProcessingOptions::default()))
1166        } else {
1167            // If we didn't find a spec, then fall back to basic completion.
1168            get_completions_using_basic_lookup(shell, &context).await
1169        }
1170    }
1171}
1172
1173async fn get_file_completions(
1174    shell: &Shell<impl extensions::ShellExtensions>,
1175    token_to_complete: &str,
1176    must_be_dir: bool,
1177) -> Vec<String> {
1178    // Basic-expand the token-to-be-completed; it won't have been expanded to this point.
1179    let mut throwaway_shell = shell.clone();
1180    let params = throwaway_shell.default_exec_params();
1181    let options = expansion::ExpanderOptions {
1182        execute_command_substitutions: false,
1183        ..Default::default()
1184    };
1185    let expanded_token = expansion::basic_expand_word_with_options(
1186        &mut throwaway_shell,
1187        &params,
1188        &unquote_str(token_to_complete),
1189        &options,
1190    )
1191    .await
1192    .unwrap_or_else(|_err| token_to_complete.to_owned());
1193
1194    let glob = std::format!("{expanded_token}*");
1195
1196    let path_filter = |path: &Path| !must_be_dir || shell.absolute_path(path).is_dir();
1197
1198    let pattern = patterns::Pattern::from(glob)
1199        .set_extended_globbing(shell.options().extended_globbing)
1200        .set_case_insensitive(shell.options().case_insensitive_pathname_expansion);
1201
1202    pattern
1203        .expand(
1204            shell.working_dir(),
1205            Some(&path_filter),
1206            &patterns::FilenameExpansionOptions::default(),
1207        )
1208        .unwrap_or_default()
1209        .into_paths()
1210        .into_iter()
1211        .collect()
1212}
1213
1214fn get_external_command_completions(
1215    shell: &Shell<impl extensions::ShellExtensions>,
1216    prefix: &str,
1217) -> Vec<String> {
1218    let mut candidates = Vec::new();
1219
1220    // Look for external commands.
1221    for path in shell.find_executables_in_path_with_prefix(
1222        prefix,
1223        shell.options().case_insensitive_pathname_expansion,
1224    ) {
1225        if let Some(file_name) = path.file_name() {
1226            candidates.push(file_name.to_string_lossy().to_string());
1227        }
1228    }
1229
1230    candidates.into_iter().collect()
1231}
1232
1233/// Attempts to complete a variable name from the given token.
1234/// Returns `Some(Answer)` if the token looks like a variable reference being typed,
1235/// or `None` if file/command completion should be used instead.
1236///
1237/// # Arguments
1238///
1239/// * `shell` - The shell instance to use for variable lookup.
1240/// * `token` - The token being completed. May be empty.
1241fn try_get_variable_completions(
1242    shell: &Shell<impl extensions::ShellExtensions>,
1243    token: &str,
1244) -> Option<Answer> {
1245    // Determine if this is a braced or unbraced variable reference
1246    let (var_prefix, use_braces) = if let Some(prefix) = token.strip_prefix("${") {
1247        // For braced: only complete if brace isn't closed yet
1248        if prefix.contains('}') {
1249            return None;
1250        }
1251        (prefix, true)
1252    } else if let Some(prefix) = token.strip_prefix('$') {
1253        (prefix, false)
1254    } else {
1255        return None;
1256    };
1257
1258    // If there's a path separator, this is a path like $HOME/foo, not a variable to complete
1259    if var_prefix.contains(std::path::MAIN_SEPARATOR) {
1260        return None;
1261    }
1262
1263    // Find matching variables
1264    let mut candidates: Vec<String> = shell
1265        .env()
1266        .iter()
1267        .filter(|(key, _)| key.starts_with(var_prefix))
1268        .map(|(key, _)| {
1269            if use_braces {
1270                format!("${{{key}}}")
1271            } else {
1272                format!("${key}")
1273            }
1274        })
1275        .collect();
1276    candidates.sort();
1277
1278    // Variable completions should not be treated as filenames (no escaping needed)
1279    let options = ProcessingOptions {
1280        treat_as_filenames: false,
1281        ..ProcessingOptions::default()
1282    };
1283
1284    Some(Answer::Candidates(candidates, options))
1285}
1286
1287/// Adds command-position completions to candidates.
1288/// This includes external commands, builtins, functions, aliases, and keywords.
1289fn add_command_completions(
1290    shell: &Shell<impl extensions::ShellExtensions>,
1291    prefix: &str,
1292    candidates: &mut Vec<String>,
1293) {
1294    // Add external commands.
1295    let mut command_completions = get_external_command_completions(shell, prefix);
1296    candidates.append(&mut command_completions);
1297
1298    // Add built-in commands.
1299    for (name, registration) in shell.builtins() {
1300        if !registration.disabled && name.starts_with(prefix) {
1301            candidates.push(name.to_owned());
1302        }
1303    }
1304
1305    // Add shell functions.
1306    for (name, _) in shell.funcs().iter() {
1307        if name.starts_with(prefix) {
1308            candidates.push(name.to_owned());
1309        }
1310    }
1311
1312    // Add aliases.
1313    for name in shell.aliases().keys() {
1314        if name.starts_with(prefix) {
1315            candidates.push(name.to_owned());
1316        }
1317    }
1318
1319    // Add keywords.
1320    for keyword in shell.get_keywords() {
1321        if keyword.starts_with(prefix) {
1322            candidates.push(keyword.to_string());
1323        }
1324    }
1325}
1326
1327async fn get_completions_using_basic_lookup(
1328    shell: &Shell<impl extensions::ShellExtensions>,
1329    context: &Context<'_>,
1330) -> Answer {
1331    let token = context.token_to_complete;
1332
1333    // Try variable completion first (e.g., $HO -> $HOME, ${HO -> ${HOME})
1334    if let Some(answer) = try_get_variable_completions(shell, token) {
1335        return answer;
1336    }
1337
1338    // File completions
1339    let mut candidates = get_file_completions(shell, token, false).await;
1340
1341    // If this appears to be the command token (and if there's *some* prefix without
1342    // a path separator) then also consider whether we should search the path for
1343    // completions too.
1344    // TODO(completions): Do a better job than just checking if index == 0.
1345    let is_command_position =
1346        context.token_index == 0 && !token.is_empty() && !token.contains(std::path::MAIN_SEPARATOR);
1347
1348    if is_command_position {
1349        add_command_completions(shell, token, &mut candidates);
1350        candidates.sort();
1351    }
1352
1353    #[cfg(windows)]
1354    {
1355        candidates = candidates
1356            .into_iter()
1357            .map(|c| c.replace('\\', "/"))
1358            .collect();
1359    }
1360
1361    Answer::Candidates(candidates, ProcessingOptions::default())
1362}
1363
1364/// Tokenizes input by splitting on delimiter characters. Words (non-delimiter sequences)
1365/// are emitted as tokens. Consecutive non-whitespace delimiters are grouped into a single
1366/// token. Whitespace delimiters separate tokens but are not emitted themselves.
1367#[allow(clippy::string_slice, reason = "used indices come from char_indices")]
1368fn simple_tokenize_by_delimiters<'a>(
1369    input: &'a str,
1370    delimiters: &[char],
1371) -> Vec<CompletionToken<'a>> {
1372    let mut tokens = vec![];
1373    let mut word_start = None;
1374    let mut word_is_delimiters = false;
1375    let mut quote_char: Option<char> = None;
1376    let mut escaped = false;
1377
1378    for (i, c) in input.char_indices() {
1379        let mut is_active_delimiter = false;
1380        if escaped {
1381            escaped = false;
1382        } else if let Some(q) = quote_char {
1383            if c == '\\' && q == '"' {
1384                // an escape in double-quoted string works as an escape.
1385                escaped = true;
1386            } else if c == q {
1387                // end of quote.
1388                quote_char = None;
1389            }
1390        } else {
1391            if c == '\\' {
1392                escaped = true;
1393            } else if word_start.is_none() && (c == '\'' || c == '\"') {
1394                // start a new quote.
1395                quote_char = Some(c);
1396            } else {
1397                is_active_delimiter = delimiters.contains(&c);
1398            }
1399        }
1400
1401        if is_active_delimiter {
1402            // If we were building a regular word and this is a delimiter, then finish it.
1403            // Similarly, if this is a whitespace delimiter, finish any delimiter sequence.
1404            if let Some(start) = word_start {
1405                if !word_is_delimiters || c.is_ascii_whitespace() {
1406                    tokens.push(CompletionToken {
1407                        text: &input[start..i],
1408                        start,
1409                    });
1410                    word_start = None;
1411                    word_is_delimiters = false;
1412                }
1413
1414                if !c.is_ascii_whitespace() && word_start.is_none() {
1415                    word_start = Some(i);
1416                    word_is_delimiters = true;
1417                }
1418            } else if !c.is_ascii_whitespace() {
1419                // Non-whitespace delimiter: start or continue delimiter sequence
1420                if word_start.is_none() {
1421                    word_start = Some(i);
1422                    word_is_delimiters = true;
1423                }
1424            }
1425        } else {
1426            // Regular character (not a delimiter). Finish any delimiter sequence.
1427            if word_is_delimiters && let Some(start) = word_start {
1428                tokens.push(CompletionToken {
1429                    text: &input[start..i],
1430                    start,
1431                });
1432                word_start = None;
1433                word_is_delimiters = false;
1434            }
1435
1436            // Start or continue a word
1437            if word_start.is_none() {
1438                word_start = Some(i);
1439            }
1440        }
1441    }
1442
1443    // Add any remaining delimiter sequence
1444    if let Some(start) = word_start {
1445        tokens.push(CompletionToken {
1446            text: &input[start..],
1447            start,
1448        });
1449    }
1450
1451    tokens
1452}
1453
1454fn completion_filter_pattern_matches(
1455    pattern: &str,
1456    candidate: &str,
1457    token_being_completed: &str,
1458    shell: &Shell<impl extensions::ShellExtensions>,
1459) -> Result<bool, error::Error> {
1460    let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
1461
1462    //
1463    // TODO(completions): Replace unescaped '&' with the word being completed.
1464    //
1465
1466    let pattern = patterns::Pattern::from(pattern.as_ref())
1467        .set_extended_globbing(shell.options().extended_globbing)
1468        .set_case_insensitive(shell.options().case_insensitive_pathname_expansion);
1469
1470    let matches = pattern.exactly_matches(candidate)?;
1471
1472    Ok(matches)
1473}
1474
1475fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
1476    let mut in_escape = false;
1477    let mut insertion_points = vec![];
1478
1479    for (i, c) in pattern.char_indices() {
1480        if !in_escape && c == '&' {
1481            insertion_points.push(i);
1482        }
1483        in_escape = !in_escape && c == '\\';
1484    }
1485
1486    if insertion_points.is_empty() {
1487        return pattern.into();
1488    }
1489
1490    let mut result = pattern.to_owned();
1491    for i in insertion_points.iter().rev() {
1492        result.replace_range(*i..=*i, replacement);
1493    }
1494
1495    result.into()
1496}
1497
1498#[cfg(test)]
1499mod tests {
1500    use super::*;
1501    use pretty_assertions::assert_matches;
1502
1503    #[test]
1504    #[allow(clippy::too_many_lines)]
1505    fn completion_tokenization() {
1506        assert_matches!(
1507            simple_tokenize_by_delimiters("one two", &[' ']).as_slice(),
1508            [
1509                CompletionToken {
1510                    text: "one",
1511                    start: 0,
1512                },
1513                CompletionToken {
1514                    text: "two",
1515                    start: 4,
1516                }
1517            ]
1518        );
1519
1520        assert_matches!(
1521            simple_tokenize_by_delimiters("one \t two", &[' ', '\t']).as_slice(),
1522            [
1523                CompletionToken {
1524                    text: "one",
1525                    start: 0,
1526                },
1527                CompletionToken {
1528                    text: "two",
1529                    start: 6,
1530                }
1531            ]
1532        );
1533
1534        assert_matches!(simple_tokenize_by_delimiters("    ", &[' ']).as_slice(), []);
1535
1536        assert_matches!(
1537            simple_tokenize_by_delimiters(":", &[':']).as_slice(),
1538            [CompletionToken {
1539                text: ":",
1540                start: 0,
1541            }]
1542        );
1543
1544        assert_matches!(
1545            simple_tokenize_by_delimiters("a:::b", &[':', ' ']).as_slice(),
1546            [
1547                CompletionToken {
1548                    text: "a",
1549                    start: 0,
1550                },
1551                CompletionToken {
1552                    text: ":::",
1553                    start: 1,
1554                },
1555                CompletionToken {
1556                    text: "b",
1557                    start: 4,
1558                }
1559            ]
1560        );
1561
1562        assert_matches!(
1563            simple_tokenize_by_delimiters("a: : :b", &[':', ' ']).as_slice(),
1564            [
1565                CompletionToken {
1566                    text: "a",
1567                    start: 0,
1568                },
1569                CompletionToken {
1570                    text: ":",
1571                    start: 1,
1572                },
1573                CompletionToken {
1574                    text: ":",
1575                    start: 3,
1576                },
1577                CompletionToken {
1578                    text: ":",
1579                    start: 5,
1580                },
1581                CompletionToken {
1582                    text: "b",
1583                    start: 6,
1584                }
1585            ]
1586        );
1587
1588        assert_matches!(
1589            simple_tokenize_by_delimiters("one two:three", &[':', ' ']).as_slice(),
1590            [
1591                CompletionToken {
1592                    text: "one",
1593                    start: 0,
1594                },
1595                CompletionToken {
1596                    text: "two",
1597                    start: 4,
1598                },
1599                CompletionToken {
1600                    text: ":",
1601                    start: 7,
1602                },
1603                CompletionToken {
1604                    text: "three",
1605                    start: 8,
1606                }
1607            ]
1608        );
1609
1610        assert_matches!(
1611            simple_tokenize_by_delimiters("one'two", &['\'']).as_slice(),
1612            [
1613                CompletionToken {
1614                    text: "one",
1615                    start: 0,
1616                },
1617                CompletionToken {
1618                    text: "'",
1619                    start: 3,
1620                },
1621                CompletionToken {
1622                    text: "two",
1623                    start: 4,
1624                },
1625            ]
1626        );
1627
1628        assert_matches!(
1629            simple_tokenize_by_delimiters("one 'two:three'", &[':', ' ']).as_slice(),
1630            [
1631                CompletionToken {
1632                    text: "one",
1633                    start: 0,
1634                },
1635                CompletionToken {
1636                    text: "'two:three'",
1637                    start: 4,
1638                },
1639            ]
1640        );
1641
1642        assert_matches!(
1643            simple_tokenize_by_delimiters("one \\'two \"two four\"", &[':', ' ']).as_slice(),
1644            [
1645                CompletionToken {
1646                    text: "one",
1647                    start: 0,
1648                },
1649                CompletionToken {
1650                    text: "\\'two",
1651                    start: 4,
1652                },
1653                CompletionToken {
1654                    text: "\"two four\"",
1655                    start: 10,
1656                },
1657            ]
1658        );
1659    }
1660}