Skip to main content

zsh/extensions/
bash_complete.rs

1//! Rust-original types for the bash-style `complete` builtin extension.
2//!
3//! These types back the `complete` / `compopt` / `compgen` builtins
4//! that `src/extensions/ext_builtins.rs` exposes. They are NOT ports
5//! of any zsh C source — zsh's native `compdef` / `compctl` / `compsys`
6//! uses completely different types (`Cmatch` at comp_h.rs:334,
7//! `Cmgroup` at comp_h.rs:269, `Compctl` at compctl_h.rs:198).
8//!
9//! Previous home was `src/ported/zle/computil.rs` which violated the
10//! "no Rust-only types in ported files" invariant; moved here so the
11//! ported zle/ tree stays a faithful C-source mirror.
12
13/// `complete` builtin per-command spec (bash-style).
14#[derive(Debug, Clone, Default)]
15pub struct CompSpec {
16    pub actions: Vec<String>,     // -a, -b, -c, etc.
17    pub wordlist: Option<String>, // -W wordlist
18    pub function: Option<String>, // -F function
19    pub command: Option<String>,  // -C command
20    pub globpat: Option<String>,  // -G glob
21    pub prefix: Option<String>,   // -P prefix
22    pub suffix: Option<String>,   // -S suffix
23}
24
25/// One completion-match candidate surfaced by the `complete` builtin.
26#[derive(Debug, Clone, Default)]
27pub struct CompMatch {
28    pub word: String,                   // The actual completion word
29    pub display: Option<String>,        // Display string (-d)
30    pub prefix: Option<String>,         // -P prefix (inserted but not part of match)
31    pub suffix: Option<String>,         // -S suffix (inserted but not part of match)
32    pub hidden_prefix: Option<String>,  // -p hidden prefix
33    pub hidden_suffix: Option<String>,  // -s hidden suffix
34    pub ignored_prefix: Option<String>, // -i ignored prefix
35    pub ignored_suffix: Option<String>, // -I ignored suffix
36    pub group: Option<String>,          // -J/-V group name
37    pub description: Option<String>,    // -X explanation
38    pub remove_suffix: Option<String>,  // -r remove chars
39    pub file_match: bool,               // -f flag
40    pub quote_match: bool,              // -q flag
41}
42
43/// Completion group bundling multiple match candidates.
44#[derive(Debug, Clone, Default)]
45pub struct CompGroup {
46    /// `name` field.
47    pub name: String,
48    /// `matches` field.
49    pub matches: Vec<CompMatch>,
50    /// `explanation` field.
51    pub explanation: Option<String>,
52    /// `sorted` field.
53    pub sorted: bool,
54}
55
56/// Per-completion state tracked across `complete` builtin invocations.
57#[derive(Debug, Clone, Default)]
58pub struct CompState {
59    pub context: String,               // completion context
60    pub exact: String,                 // exact match handling
61    pub exact_string: String,          // the exact string if matched
62    pub ignored: i32,                  // number of ignored matches
63    pub insert: String,                // what to insert
64    pub insert_positions: String,      // cursor positions after insert
65    pub last_prompt: String,           // whether to return to last prompt
66    pub list: String,                  // listing style
67    pub list_lines: i32,               // number of lines for listing
68    pub list_max: i32,                 // max matches to list
69    pub nmatches: i32,                 // number of matches
70    pub old_insert: String,            // previous insert value
71    pub old_list: String,              // previous list value
72    pub parameter: String,             // parameter being completed
73    pub pattern_insert: String,        // pattern insert mode
74    pub matchpat: String,              // pattern matching mode
75    pub bslashquote: String,           // quoting type
76    pub quoting: String,               // current quoting
77    pub redirect: String,              // redirection type
78    pub restore: String,               // restore mode
79    pub to_end: String,                // move to end mode
80    pub unambiguous: String,           // unambiguous prefix
81    pub unambiguous_cursor: i32,       // cursor pos in unambiguous
82    pub unambiguous_positions: String, // positions in unambiguous
83    pub vared: String,                 // vared context
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    // === CompSpec: bash-style `complete` builtin spec defaults ===
91
92    #[test]
93    fn comp_spec_default_has_empty_actions_and_no_callbacks() {
94        let s = CompSpec::default();
95        assert!(
96            s.actions.is_empty(),
97            "default actions must be empty (no -a/-b/-c flags applied yet)"
98        );
99        assert!(s.wordlist.is_none(), "no -W wordlist by default");
100        assert!(s.function.is_none(), "no -F function by default");
101        assert!(s.command.is_none(), "no -C command by default");
102        assert!(s.globpat.is_none(), "no -G glob by default");
103        assert!(s.prefix.is_none(), "no -P prefix by default");
104        assert!(s.suffix.is_none(), "no -S suffix by default");
105    }
106
107    #[test]
108    fn comp_spec_clone_is_deep_independent() {
109        // CompSpec derives Clone — mutating the clone must not bleed
110        // back into the original. Catches a future move to `Rc` /
111        // `Arc` field types that would share state.
112        let mut a = CompSpec::default();
113        a.actions.push("file".to_string());
114        a.function = Some("__git_complete".to_string());
115        let b = a.clone();
116        a.actions.push("dir".to_string());
117        a.function = Some("__docker_complete".to_string());
118        assert_eq!(b.actions, vec!["file"]);
119        assert_eq!(b.function.as_deref(), Some("__git_complete"));
120    }
121
122    #[test]
123    fn comp_spec_actions_preserve_insertion_order() {
124        // Bash `complete -A file -A directory -A user` records each
125        // action in flag-order — CompSpec uses Vec to honor that.
126        let mut s = CompSpec::default();
127        for a in ["file", "directory", "user"] {
128            s.actions.push(a.to_string());
129        }
130        assert_eq!(s.actions, vec!["file", "directory", "user"]);
131    }
132
133    // === CompMatch: per-candidate flags + override defaults ===
134
135    #[test]
136    fn comp_match_default_empty_word_and_no_flags() {
137        let m = CompMatch::default();
138        assert_eq!(m.word, "");
139        assert!(m.display.is_none());
140        assert!(m.prefix.is_none());
141        assert!(m.suffix.is_none());
142        assert!(m.hidden_prefix.is_none());
143        assert!(m.hidden_suffix.is_none());
144        assert!(m.ignored_prefix.is_none());
145        assert!(m.ignored_suffix.is_none());
146        assert!(m.group.is_none());
147        assert!(m.description.is_none());
148        assert!(m.remove_suffix.is_none());
149        assert!(
150            !m.file_match,
151            "file_match must default to false (no -f flag)"
152        );
153        assert!(
154            !m.quote_match,
155            "quote_match must default to false (no -q flag)"
156        );
157    }
158
159    #[test]
160    fn comp_match_word_round_trip_preserves_value() {
161        let m = CompMatch {
162            word: "git-status".to_string(),
163            description: Some("show working tree".to_string()),
164            file_match: false,
165            quote_match: true,
166            ..Default::default()
167        };
168        assert_eq!(m.word, "git-status");
169        assert_eq!(m.description.as_deref(), Some("show working tree"));
170        assert!(m.quote_match);
171    }
172
173    // === CompGroup: ordering / grouping container defaults ===
174
175    #[test]
176    fn comp_group_default_has_no_name_no_matches_unsorted() {
177        let g = CompGroup::default();
178        assert_eq!(g.name, "");
179        assert!(g.matches.is_empty());
180        assert!(g.explanation.is_none());
181        assert!(
182            !g.sorted,
183            "default groups are unsorted (callers opt in via -J)"
184        );
185    }
186
187    #[test]
188    fn comp_group_can_carry_multiple_matches() {
189        let g = CompGroup {
190            name: "files".to_string(),
191            matches: vec![
192                CompMatch {
193                    word: "a.txt".to_string(),
194                    file_match: true,
195                    ..Default::default()
196                },
197                CompMatch {
198                    word: "b.txt".to_string(),
199                    file_match: true,
200                    ..Default::default()
201                },
202            ],
203            explanation: Some("text files".to_string()),
204            sorted: true,
205        };
206        assert_eq!(g.matches.len(), 2);
207        assert_eq!(g.matches[0].word, "a.txt");
208        assert!(g.matches.iter().all(|m| m.file_match));
209        assert!(g.sorted);
210    }
211
212    // === CompState: per-completion mutable state defaults ===
213
214    #[test]
215    fn comp_state_default_all_strings_empty_all_ints_zero() {
216        let s = CompState::default();
217        // Every String field defaults to "" — verify a representative
218        // subset across param/list/quoting/etc. families.
219        assert_eq!(s.context, "");
220        assert_eq!(s.exact, "");
221        assert_eq!(s.insert, "");
222        assert_eq!(s.list, "");
223        assert_eq!(s.parameter, "");
224        assert_eq!(s.quoting, "");
225        assert_eq!(s.unambiguous, "");
226        assert_eq!(s.vared, "");
227        // Every i32 field defaults to 0.
228        assert_eq!(s.ignored, 0);
229        assert_eq!(s.list_lines, 0);
230        assert_eq!(s.list_max, 0);
231        assert_eq!(s.nmatches, 0);
232        assert_eq!(s.unambiguous_cursor, 0);
233    }
234
235    #[test]
236    fn comp_state_clone_independence() {
237        // Same independence guarantee as CompSpec — protects against
238        // a future regression to a shared-state field type.
239        let mut a = CompState::default();
240        a.nmatches = 5;
241        a.insert = "foo".to_string();
242        let b = a.clone();
243        a.nmatches = 99;
244        a.insert = "bar".to_string();
245        assert_eq!(b.nmatches, 5);
246        assert_eq!(b.insert, "foo");
247    }
248
249    #[test]
250    fn comp_state_int_fields_round_trip_negative_values() {
251        // Some compstate ints are signed by design (e.g.
252        // unambiguous_cursor can carry -1 for "none"). Verify the
253        // field type carries the sign through.
254        let s = CompState {
255            unambiguous_cursor: -1,
256            nmatches: -42,
257            ..Default::default()
258        };
259        assert_eq!(s.unambiguous_cursor, -1);
260        assert_eq!(s.nmatches, -42);
261    }
262}