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