1#[derive(Debug, Clone, Default)]
15pub struct CompSpec {
16 pub actions: Vec<String>, pub wordlist: Option<String>, pub function: Option<String>, pub command: Option<String>, pub globpat: Option<String>, pub prefix: Option<String>, pub suffix: Option<String>, }
24
25#[derive(Debug, Clone, Default)]
27pub struct CompMatch {
28 pub word: String, pub display: Option<String>, pub prefix: Option<String>, pub suffix: Option<String>, pub hidden_prefix: Option<String>, pub hidden_suffix: Option<String>, pub ignored_prefix: Option<String>, pub ignored_suffix: Option<String>, pub group: Option<String>, pub description: Option<String>, pub remove_suffix: Option<String>, pub file_match: bool, pub quote_match: bool, }
42
43#[derive(Debug, Clone, Default)]
45pub struct CompGroup {
46 pub name: String,
48 pub matches: Vec<CompMatch>,
50 pub explanation: Option<String>,
52 pub sorted: bool,
54}
55
56#[derive(Debug, Clone, Default)]
58pub struct CompState {
59 pub context: String, pub exact: String, pub exact_string: String, pub ignored: i32, pub insert: String, pub insert_positions: String, pub last_prompt: String, pub list: String, pub list_lines: i32, pub list_max: i32, pub nmatches: i32, pub old_insert: String, pub old_list: String, pub parameter: String, pub pattern_insert: String, pub matchpat: String, pub bslashquote: String, pub quoting: String, pub redirect: String, pub restore: String, pub to_end: String, pub unambiguous: String, pub unambiguous_cursor: i32, pub unambiguous_positions: String, pub vared: String, }
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[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 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 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 #[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 #[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 #[test]
215 fn comp_state_default_all_strings_empty_all_ints_zero() {
216 let s = CompState::default();
217 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 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 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 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}