dte test coverage


Directory: ./
File: src/completion.c
Date: 2025-05-08 15:05:54
Exec Total Coverage
Lines: 467 524 89.1%
Functions: 44 48 91.7%
Branches: 223 306 72.9%

Line Branch Exec Source
1 #include <fcntl.h>
2 #include <stdbool.h>
3 #include <stdlib.h>
4 #include <string.h>
5 #include <sys/stat.h>
6 #include <unistd.h>
7 #include "completion.h"
8 #include "bind.h"
9 #include "command/alias.h"
10 #include "command/args.h"
11 #include "command/parse.h"
12 #include "command/run.h"
13 #include "command/serialize.h"
14 #include "commands.h"
15 #include "compiler.h"
16 #include "config.h"
17 #include "exec.h"
18 #include "filetype.h"
19 #include "mode.h"
20 #include "options.h"
21 #include "show.h"
22 #include "syntax/color.h"
23 #include "tag.h"
24 #include "terminal/cursor.h"
25 #include "terminal/key.h"
26 #include "terminal/style.h"
27 #include "util/arith.h"
28 #include "util/array.h"
29 #include "util/ascii.h"
30 #include "util/bit.h"
31 #include "util/bsearch.h"
32 #include "util/debug.h"
33 #include "util/intmap.h"
34 #include "util/log.h"
35 #include "util/numtostr.h"
36 #include "util/path.h"
37 #include "util/str-array.h"
38 #include "util/str-util.h"
39 #include "util/string-view.h"
40 #include "util/string.h"
41 #include "util/xdirent.h"
42 #include "util/xmalloc.h"
43 #include "vars.h"
44
45 // NOLINTNEXTLINE(*-avoid-non-const-global-variables)
46 extern char **environ;
47
48 typedef enum {
49 COLLECT_ALL, // (directories and files)
50 COLLECT_EXECUTABLES, // (directories and executable files)
51 COLLECT_DIRS_ONLY,
52 } FileCollectionType;
53
54 static bool is_executable(int dir_fd, const char *filename)
55 {
56 return faccessat(dir_fd, filename, X_OK, 0) == 0;
57 }
58
59 10 static bool do_collect_files (
60 PointerArray *array,
61 const char *dirname,
62 const char *dirprefix,
63 const char *fileprefix,
64 FileCollectionType type
65 ) {
66 10 DIR *const dir = xopendir(dirname);
67
1/2
✓ Branch 0 (3→4) taken 10 times.
✗ Branch 1 (3→45) not taken.
10 if (!dir) {
68 return false;
69 }
70
71 10 const int dir_fd = dirfd(dir);
72
1/2
✗ Branch 0 (5→6) not taken.
✓ Branch 1 (5→10) taken 10 times.
10 if (unlikely(dir_fd < 0)) {
73 LOG_ERRNO("dirfd");
74 xclosedir(dir);
75 return false;
76 }
77
78 10 size_t dlen = strlen(dirprefix);
79 10 size_t flen = strlen(fileprefix);
80 10 const struct dirent *de;
81
82
2/2
✓ Branch 0 (42→11) taken 299 times.
✓ Branch 1 (42→43) taken 10 times.
309 while ((de = xreaddir(dir))) {
83 299 const char *name = de->d_name;
84
5/6
✓ Branch 0 (11→12) taken 289 times.
✓ Branch 1 (11→14) taken 10 times.
✓ Branch 2 (12→13) taken 279 times.
✓ Branch 3 (12→14) taken 10 times.
✗ Branch 4 (13→14) not taken.
✓ Branch 5 (13→15) taken 279 times.
299 if (streq(name, ".") || streq(name, "..") || unlikely(streq(name, ""))) {
85 247 continue;
86 }
87
88 // TODO: add a global option to allow dotfiles to be included
89 // even when there's no prefix
90
4/4
✓ Branch 0 (15→16) taken 229 times.
✓ Branch 1 (15→17) taken 50 times.
✓ Branch 2 (18→19) taken 227 times.
✓ Branch 3 (18→20) taken 52 times.
279 if (flen ? strncmp(name, fileprefix, flen) != 0 : name[0] == '.') {
91 227 continue;
92 }
93
94 52 struct stat st;
95
1/2
✗ Branch 0 (21→22) not taken.
✓ Branch 1 (21→23) taken 52 times.
52 if (fstatat(dir_fd, name, &st, AT_SYMLINK_NOFOLLOW)) {
96 continue;
97 }
98
99 52 bool is_dir = S_ISDIR(st.st_mode);
100
1/2
✗ Branch 0 (23→24) not taken.
✓ Branch 1 (23→27) taken 52 times.
52 if (S_ISLNK(st.st_mode)) {
101 if (!fstatat(dir_fd, name, &st, 0)) {
102 is_dir = S_ISDIR(st.st_mode);
103 }
104 }
105
106
2/2
✓ Branch 0 (27→28) taken 51 times.
✓ Branch 1 (27→36) taken 1 times.
52 if (!is_dir) {
107
1/4
✗ Branch 0 (28→29) not taken.
✗ Branch 1 (28→30) not taken.
✗ Branch 2 (28→35) not taken.
✓ Branch 3 (28→36) taken 51 times.
51 switch (type) {
108 case COLLECT_DIRS_ONLY:
109 continue;
110 case COLLECT_ALL:
111 break;
112 case COLLECT_EXECUTABLES:
113 if (!is_executable(dir_fd, name)) {
114 continue;
115 }
116 if (!dlen) {
117 dirprefix = "./";
118 dlen = 2;
119 }
120 break;
121 default:
122 BUG("unhandled FileCollectionType value");
123 }
124 }
125
126 52 ptr_array_append(array, path_joinx(dirprefix, name, is_dir));
127 }
128
129 10 xclosedir(dir);
130 10 return true;
131 }
132
133 10 static void collect_files(EditorState *e, CompletionState *cs, FileCollectionType type)
134 {
135 10 StringView esc = cs->escaped;
136
2/2
✓ Branch 0 (3→4) taken 2 times.
✓ Branch 1 (3→10) taken 8 times.
10 if (strview_has_prefix(&esc, "~/")) {
137 2 CommandRunner runner = normal_mode_cmdrunner(e);
138 2 runner.expand_tilde_slash = false;
139 2 char *str = parse_command_arg(&runner, esc.data, esc.length);
140 2 const char *slash = xstrrchr(str, '/');
141 2 cs->tilde_expanded = true;
142 2 char *dir = path_dirname(cs->parsed);
143 2 char *dirprefix = path_dirname(str);
144 2 do_collect_files(&cs->completions, dir, dirprefix, slash + 1, type);
145 2 free(dirprefix);
146 2 free(dir);
147 2 free(str);
148 } else {
149 8 const char *slash = strrchr(cs->parsed, '/');
150
2/2
✓ Branch 0 (10→11) taken 5 times.
✓ Branch 1 (10→12) taken 3 times.
8 if (!slash) {
151 5 do_collect_files(&cs->completions, ".", "", cs->parsed, type);
152 } else {
153 3 char *dir = path_dirname(cs->parsed);
154 3 do_collect_files(&cs->completions, dir, dir, slash + 1, type);
155 3 free(dir);
156 }
157 }
158
159
2/2
✓ Branch 0 (15→16) taken 4 times.
✓ Branch 1 (15→18) taken 6 times.
10 if (cs->completions.count == 1) {
160 // Add space if completed string is not a directory
161 4 const char *s = cs->completions.ptrs[0];
162 4 size_t len = strlen(s);
163
1/2
✓ Branch 0 (16→17) taken 4 times.
✗ Branch 1 (16→18) not taken.
4 if (len > 0) {
164 4 cs->add_space_after_single_match = s[len - 1] != '/';
165 }
166 }
167 10 }
168
169 5 void collect_normal_aliases(EditorState *e, PointerArray *a, const char *prefix)
170 {
171 5 collect_hashmap_keys(&e->aliases, a, prefix);
172 5 }
173
174 5 static void collect_bound_keys(const IntMap *bindings, PointerArray *a, const char *prefix)
175 {
176 5 size_t prefix_len = strlen(prefix);
177 5 char keystr[KEYCODE_STR_BUFSIZE];
178
2/2
✓ Branch 0 (9→3) taken 282 times.
✓ Branch 1 (9→10) taken 5 times.
292 for (IntMapIter it = intmap_iter(bindings); intmap_next(&it); ) {
179 282 size_t keylen = keycode_to_string(it.entry->key, keystr);
180
2/2
✓ Branch 0 (4→5) taken 124 times.
✓ Branch 1 (4→8) taken 158 times.
282 if (str_has_strn_prefix(keystr, prefix, prefix_len)) {
181 124 ptr_array_append(a, xmemdup(keystr, keylen + 1));
182 }
183 }
184 5 }
185
186 1 void collect_bound_normal_keys(EditorState *e, PointerArray *a, const char *prefix)
187 {
188 1 collect_bound_keys(&e->normal_mode->key_bindings, a, prefix);
189 1 }
190
191 1 void collect_hl_styles(EditorState *e, PointerArray *a, const char *prefix)
192 {
193 1 const char *dot = strchr(prefix, '.');
194
1/4
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→4) taken 1 times.
✗ Branch 2 (3→4) not taken.
✗ Branch 3 (3→6) not taken.
1 if (!dot || dot == prefix || (dot - prefix) > FILETYPE_NAME_MAX) {
195 // No dot found in prefix, or found at offset 0, or buffer too small;
196 // just collect matching highlight names added by the `hi` command
197 1 collect_builtin_styles(a, prefix);
198 1 collect_hashmap_keys(&e->styles.other, a, prefix);
199 2 return;
200 }
201
202 // Copy and null-terminate the filetype part of `prefix` (before the dot)
203 char filetype[FILETYPE_NAME_MAX + 1];
204 size_t ftlen = dot - prefix;
205 memcpy(filetype, prefix, ftlen);
206 filetype[ftlen] = '\0';
207
208 // Find or load the Syntax for `filetype`
209 const Syntax *syn = find_syntax(&e->syntaxes, filetype);
210 if (!syn) {
211 syn = load_syntax_by_filetype(e, filetype);
212 if (!syn) {
213 return;
214 }
215 }
216
217 // Collect all emit names from `syn` that start with the string after
218 // the dot
219 collect_syntax_emit_names(syn, a, dot + 1);
220 }
221
222 3 void collect_compilers(EditorState *e, PointerArray *a, const char *prefix)
223 {
224 3 collect_hashmap_keys(&e->compilers, a, prefix);
225 3 }
226
227 2 void collect_env(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix)
228 {
229 2 size_t prefix_len = strlen(prefix);
230
1/2
✓ Branch 0 (2→8) taken 2 times.
✗ Branch 1 (2→9) not taken.
2 if (memchr(prefix, '=', prefix_len)) {
231 return;
232 }
233
234
2/2
✓ Branch 0 (8→3) taken 330 times.
✓ Branch 1 (8→9) taken 2 times.
332 for (size_t i = 0; environ[i]; i++) {
235 330 const char *var = environ[i];
236
2/2
✓ Branch 0 (3→4) taken 2 times.
✓ Branch 1 (3→7) taken 328 times.
330 if (str_has_strn_prefix(var, prefix, prefix_len)) {
237 2 const char *delim = strchr(var, '=');
238
1/2
✓ Branch 0 (4→5) taken 2 times.
✗ Branch 1 (4→7) not taken.
2 if (likely(delim && delim != var)) {
239 2 ptr_array_append(a, xstrcut(var, delim - var));
240 }
241 }
242 }
243 }
244
245 2 static void complete_alias(EditorState *e, const CommandArgs *a)
246 {
247 2 CompletionState *cs = &e->cmdline.completion;
248
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 1 times.
2 if (a->nr_args == 0) {
249 1 collect_normal_aliases(e, &cs->completions, cs->parsed);
250
2/4
✓ Branch 0 (4→5) taken 1 times.
✗ Branch 1 (4→10) not taken.
✓ Branch 2 (5→6) taken 1 times.
✗ Branch 3 (5→10) not taken.
1 } else if (a->nr_args == 1 && cs->parsed[0] == '\0') {
251 1 const char *cmd = find_alias(&e->aliases, a->args[0]);
252
1/2
✓ Branch 0 (7→8) taken 1 times.
✗ Branch 1 (7→10) not taken.
1 if (cmd) {
253 1 ptr_array_append(&cs->completions, xstrdup(cmd));
254 }
255 }
256 2 }
257
258 10 static void complete_bind(EditorState *e, const CommandArgs *a)
259 {
260 // Mask of flags that determine modes (excludes -q)
261 10 CommandFlagSet modemask = cmdargs_flagset_from_str("cnsT");
262
263
3/4
✓ Branch 0 (3→4) taken 8 times.
✓ Branch 1 (3→31) taken 2 times.
✓ Branch 2 (4→5) taken 8 times.
✗ Branch 3 (4→31) not taken.
10 if (u64_popcount(a->flag_set & modemask) > 1 || a->nr_flag_args > 1) {
264 // Don't complete bindings for multiple modes
265 return;
266 }
267
268 8 const ModeHandler *mode;
269
2/2
✓ Branch 0 (6→7) taken 1 times.
✓ Branch 1 (6→8) taken 7 times.
8 if (cmdargs_has_flag(a, 'c')) {
270 1 mode = e->command_mode;
271
2/2
✓ Branch 0 (9→10) taken 3 times.
✓ Branch 1 (9→11) taken 4 times.
7 } else if (cmdargs_has_flag(a, 's')) {
272 3 mode = e->search_mode;
273
2/2
✓ Branch 0 (12→13) taken 1 times.
✓ Branch 1 (12→19) taken 3 times.
4 } else if (cmdargs_has_flag(a, 'T')) {
274 1 BUG_ON(a->nr_flag_args != 1);
275 1 BUG_ON(a->flags[0] != 'T');
276 1 mode = get_mode_handler(&e->modes, a->args[0]);
277
1/2
✓ Branch 0 (18→20) taken 1 times.
✗ Branch 1 (18→31) not taken.
1 if (!mode) {
278 return;
279 }
280 } else {
281 3 mode = e->normal_mode;
282 }
283
284 8 const IntMap *key_bindings = &mode->key_bindings;
285 8 CompletionState *cs = &e->cmdline.completion;
286
2/2
✓ Branch 0 (20→21) taken 4 times.
✓ Branch 1 (20→23) taken 4 times.
8 if (a->nr_args == 0) {
287 4 collect_bound_keys(key_bindings, &cs->completions, cs->parsed);
288 4 return;
289 }
290
291
2/4
✓ Branch 0 (23→24) taken 4 times.
✗ Branch 1 (23→31) not taken.
✓ Branch 2 (24→25) taken 4 times.
✗ Branch 3 (24→31) not taken.
4 if (a->nr_args != 1 || cs->parsed[0] != '\0') {
292 return;
293 }
294
295 4 KeyCode key = parse_key_string(a->args[a->nr_flag_args]);
296
1/2
✓ Branch 0 (26→27) taken 4 times.
✗ Branch 1 (26→31) not taken.
4 if (key == KEY_NONE) {
297 return;
298 }
299
300 4 const CachedCommand *cmd = lookup_binding(key_bindings, key);
301
1/2
✓ Branch 0 (28→29) taken 4 times.
✗ Branch 1 (28→31) not taken.
4 if (!cmd) {
302 return;
303 }
304
305 4 ptr_array_append(&cs->completions, xstrdup(cmd->cmd_str));
306 }
307
308 static void complete_cd(EditorState *e, const CommandArgs* UNUSED_ARG(a))
309 {
310 CompletionState *cs = &e->cmdline.completion;
311 collect_files(e, cs, COLLECT_DIRS_ONLY);
312 if (str_has_prefix("-", cs->parsed)) {
313 if (likely(xgetenv("OLDPWD"))) {
314 ptr_array_append(&cs->completions, xstrdup("-"));
315 }
316 }
317 }
318
319 4 static void complete_exec(EditorState *e, const CommandArgs *a)
320 {
321 // TODO: add completion for [-ioe] option arguments
322 4 CompletionState *cs = &e->cmdline.completion;
323 4 collect_files(e, cs, a->nr_args == 0 ? COLLECT_EXECUTABLES : COLLECT_ALL);
324 4 }
325
326 1 static void complete_compile(EditorState *e, const CommandArgs *a)
327 {
328 1 CompletionState *cs = &e->cmdline.completion;
329 1 size_t n = a->nr_args;
330
1/2
✓ Branch 0 (2→3) taken 1 times.
✗ Branch 1 (2→4) not taken.
1 if (n == 0) {
331 1 collect_compilers(e, &cs->completions, cs->parsed);
332 } else {
333 collect_files(e, cs, n == 1 ? COLLECT_EXECUTABLES : COLLECT_ALL);
334 }
335 1 }
336
337 4 static void complete_cursor(EditorState *e, const CommandArgs *a)
338 {
339 4 CompletionState *cs = &e->cmdline.completion;
340 4 size_t n = a->nr_args;
341
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 3 times.
4 if (n == 0) {
342 1 collect_cursor_modes(&cs->completions, cs->parsed);
343
2/2
✓ Branch 0 (4→5) taken 2 times.
✓ Branch 1 (4→6) taken 1 times.
3 } else if (n == 1) {
344 2 collect_cursor_types(&cs->completions, cs->parsed);
345
1/2
✓ Branch 0 (6→7) taken 1 times.
✗ Branch 1 (6→11) not taken.
1 } else if (n == 2) {
346 1 collect_cursor_colors(&cs->completions, cs->parsed);
347 // Add an example #rrggbb color, to make things more discoverable
348 1 static const char rgb_example[] = "#22AABB";
349
1/2
✓ Branch 0 (8→9) taken 1 times.
✗ Branch 1 (8→11) not taken.
1 if (str_has_prefix(rgb_example, cs->parsed)) {
350 1 ptr_array_append(&cs->completions, xstrdup(rgb_example));
351 }
352 }
353 4 }
354
355 3 static void complete_def_mode(EditorState *e, const CommandArgs *a)
356 {
357
2/2
✓ Branch 0 (2→3) taken 2 times.
✓ Branch 1 (2→16) taken 1 times.
3 if (a->nr_args == 0) {
358 return;
359 }
360
361 2 CompletionState *cs = &e->cmdline.completion;
362 2 PointerArray *completions = &cs->completions;
363 2 const char *prefix = cs->parsed;
364 2 size_t prefix_len = strlen(prefix);
365
366
2/2
✓ Branch 0 (14→4) taken 6 times.
✓ Branch 1 (14→15) taken 2 times.
8 for (HashMapIter it = hashmap_iter(&e->modes); hashmap_next(&it); ) {
367 6 const char *name = it.entry->key;
368 6 const ModeHandler *mode = it.entry->value;
369
1/2
✗ Branch 0 (4→5) not taken.
✓ Branch 1 (4→6) taken 6 times.
6 if (!str_has_strn_prefix(name, prefix, prefix_len)) {
370 continue;
371 }
372
2/2
✓ Branch 0 (6→7) taken 4 times.
✓ Branch 1 (6→8) taken 2 times.
6 if (mode->cmds != &normal_commands) {
373 // Exclude command/search mode
374 4 continue;
375 }
376
2/2
✓ Branch 0 (8→9) taken 1 times.
✓ Branch 1 (8→10) taken 1 times.
2 if (string_array_contains_str(a->args + 1 + a->nr_flag_args, name)) {
377 // Exclude modes already specified in a previous argument
378 1 continue;
379 }
380 1 ptr_array_append(completions, xstrdup(name));
381 }
382 }
383
384 3 static void complete_errorfmt(EditorState *e, const CommandArgs *a)
385 {
386 3 CompletionState *cs = &e->cmdline.completion;
387
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 2 times.
3 if (a->nr_args == 0) {
388 1 collect_compilers(e, &cs->completions, cs->parsed);
389
2/4
✓ Branch 0 (4→5) taken 2 times.
✗ Branch 1 (4→8) not taken.
✓ Branch 2 (6→7) taken 2 times.
✗ Branch 3 (6→8) not taken.
2 } else if (a->nr_args >= 2 && !cmdargs_has_flag(a, 'i')) {
390 2 collect_errorfmt_capture_names(&cs->completions, cs->parsed);
391 }
392 3 }
393
394 1 static void complete_ft(EditorState *e, const CommandArgs *a)
395 {
396 1 CompletionState *cs = &e->cmdline.completion;
397
1/2
✓ Branch 0 (2→3) taken 1 times.
✗ Branch 1 (2→4) not taken.
1 if (a->nr_args == 0) {
398 1 collect_ft(&e->filetypes, &cs->completions, cs->parsed);
399 }
400 1 }
401
402 2 static void complete_hi(EditorState *e, const CommandArgs *a)
403 {
404 2 CompletionState *cs = &e->cmdline.completion;
405
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 1 times.
2 if (a->nr_args == 0) {
406 1 collect_hl_styles(e, &cs->completions, cs->parsed);
407 } else {
408 // TODO: Take into account previous arguments and don't
409 // suggest repeat attributes or excess colors
410 1 collect_colors_and_attributes(&cs->completions, cs->parsed);
411 }
412 2 }
413
414 1 static void complete_include(EditorState *e, const CommandArgs *a)
415 {
416 1 CompletionState *cs = &e->cmdline.completion;
417
1/2
✓ Branch 0 (2→3) taken 1 times.
✗ Branch 1 (2→7) not taken.
1 if (a->nr_args == 0) {
418
1/2
✓ Branch 0 (4→5) taken 1 times.
✗ Branch 1 (4→6) not taken.
1 if (cmdargs_has_flag(a, 'b')) {
419 1 collect_builtin_includes(&cs->completions, cs->parsed);
420 } else {
421 collect_files(e, cs, COLLECT_ALL);
422 }
423 }
424 1 }
425
426 1 static void complete_macro(EditorState *e, const CommandArgs *a)
427 {
428 1 static const char verbs[][8] = {
429 "cancel",
430 "play",
431 "record",
432 "stop",
433 "toggle",
434 };
435
436
1/2
✓ Branch 0 (2→3) taken 1 times.
✗ Branch 1 (2→4) not taken.
1 if (a->nr_args != 0) {
437 return;
438 }
439
440 1 CompletionState *cs = &e->cmdline.completion;
441 1 COLLECT_STRINGS(verbs, &cs->completions, cs->parsed);
442 }
443
444 1 static void complete_mode(EditorState *e, const CommandArgs *a)
445 {
446
1/2
✓ Branch 0 (2→3) taken 1 times.
✗ Branch 1 (2→4) not taken.
1 if (a->nr_args != 0) {
447 return;
448 }
449
450 1 CompletionState *cs = &e->cmdline.completion;
451 1 collect_hashmap_keys(&e->modes, &cs->completions, cs->parsed);
452 }
453
454 1 static void complete_move_tab(EditorState *e, const CommandArgs *a)
455 {
456
1/2
✓ Branch 0 (2→3) taken 1 times.
✗ Branch 1 (2→4) not taken.
1 if (a->nr_args != 0) {
457 return;
458 }
459
460 1 static const char words[][8] = {"left", "right"};
461 1 CompletionState *cs = &e->cmdline.completion;
462 1 COLLECT_STRINGS(words, &cs->completions, cs->parsed);
463 }
464
465 4 static void complete_open(EditorState *e, const CommandArgs *a)
466 {
467
1/2
✓ Branch 0 (3→4) taken 4 times.
✗ Branch 1 (3→5) not taken.
4 if (!cmdargs_has_flag(a, 't')) {
468 4 collect_files(e, &e->cmdline.completion, COLLECT_ALL);
469 }
470 4 }
471
472 3 static void complete_option(EditorState *e, const CommandArgs *a)
473 {
474 3 CompletionState *cs = &e->cmdline.completion;
475
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→6) taken 2 times.
3 if (a->nr_args == 0) {
476
1/2
✓ Branch 0 (4→5) taken 1 times.
✗ Branch 1 (4→9) not taken.
1 if (!cmdargs_has_flag(a, 'r')) {
477 1 collect_ft(&e->filetypes, &cs->completions, cs->parsed);
478 }
479
2/2
✓ Branch 0 (6→7) taken 1 times.
✓ Branch 1 (6→8) taken 1 times.
2 } else if (a->nr_args & 1) {
480 1 collect_auto_options(&cs->completions, cs->parsed);
481 } else {
482 1 collect_option_values(e, &cs->completions, a->args[a->nr_args - 1], cs->parsed);
483 }
484 3 }
485
486 1 static void complete_save(EditorState *e, const CommandArgs* UNUSED_ARG(a))
487 {
488 1 collect_files(e, &e->cmdline.completion, COLLECT_ALL);
489 1 }
490
491 1 static void complete_quit(EditorState *e, const CommandArgs* UNUSED_ARG(a))
492 {
493 1 CompletionState *cs = &e->cmdline.completion;
494
1/2
✓ Branch 0 (2→3) taken 1 times.
✗ Branch 1 (2→5) not taken.
1 if (str_has_prefix("0", cs->parsed)) {
495 1 ptr_array_append(&cs->completions, xstrdup("0"));
496 }
497
1/2
✓ Branch 0 (5→6) taken 1 times.
✗ Branch 1 (5→8) not taken.
1 if (str_has_prefix("1", cs->parsed)) {
498 1 ptr_array_append(&cs->completions, xstrdup("1"));
499 }
500 1 }
501
502 static void complete_redo(EditorState *e, const CommandArgs* UNUSED_ARG(a))
503 {
504 const Change *change = e->buffer->cur_change;
505 CompletionState *cs = &e->cmdline.completion;
506 for (unsigned long i = 1, n = change->nr_prev; i <= n; i++) {
507 ptr_array_append(&cs->completions, xstrdup(ulong_to_str(i)));
508 }
509 }
510
511 7 static void complete_set(EditorState *e, const CommandArgs *a)
512 {
513 7 CompletionState *cs = &e->cmdline.completion;
514
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→6) taken 6 times.
7 if ((a->nr_args + 1) & 1) {
515 1 bool local = cmdargs_has_flag(a, 'l');
516 1 bool global = cmdargs_has_flag(a, 'g');
517 1 collect_options(&cs->completions, cs->parsed, local, global);
518 } else {
519 6 collect_option_values(e, &cs->completions, a->args[a->nr_args - 1], cs->parsed);
520 }
521 7 }
522
523 2 static void complete_setenv(EditorState *e, const CommandArgs *a)
524 {
525 2 CompletionState *cs = &e->cmdline.completion;
526
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 1 times.
2 if (a->nr_args == 0) {
527 1 collect_env(e, &cs->completions, cs->parsed);
528
2/4
✓ Branch 0 (4→5) taken 1 times.
✗ Branch 1 (4→12) not taken.
✓ Branch 2 (5→6) taken 1 times.
✗ Branch 3 (5→12) not taken.
1 } else if (a->nr_args == 1 && cs->parsed[0] == '\0') {
529 1 BUG_ON(!a->args[0]);
530 1 const char *value = getenv(a->args[0]);
531
1/2
✓ Branch 0 (9→10) taken 1 times.
✗ Branch 1 (9→12) not taken.
1 if (value) {
532 1 ptr_array_append(&cs->completions, xstrdup(value));
533 }
534 }
535 2 }
536
537 6 static void complete_show(EditorState *e, const CommandArgs *a)
538 {
539 6 CompletionState *cs = &e->cmdline.completion;
540
2/2
✓ Branch 0 (2→3) taken 2 times.
✓ Branch 1 (2→4) taken 4 times.
6 if (a->nr_args == 0) {
541 2 collect_show_subcommands(&cs->completions, cs->parsed);
542
1/2
✓ Branch 0 (4→5) taken 4 times.
✗ Branch 1 (4→8) not taken.
4 } else if (a->nr_args == 1) {
543 4 BUG_ON(!a->args[0]);
544 4 collect_show_subcommand_args(e, &cs->completions, a->args[0], cs->parsed);
545 }
546 6 }
547
548 static void complete_tag(EditorState *e, const CommandArgs *a)
549 {
550 CompletionState *cs = &e->cmdline.completion;
551 if (!cmdargs_has_flag(a, 'r')) {
552 BUG_ON(!cs->parsed);
553 StringView prefix = strview_from_cstring(cs->parsed);
554 collect_tags(&e->tagfile, &cs->completions, &prefix);
555 }
556 }
557
558 1 static void complete_toggle(EditorState *e, const CommandArgs *a)
559 {
560 1 CompletionState *cs = &e->cmdline.completion;
561
1/2
✓ Branch 0 (2→3) taken 1 times.
✗ Branch 1 (2→5) not taken.
1 if (a->nr_args == 0) {
562 1 bool global = cmdargs_has_flag(a, 'g');
563 1 collect_toggleable_options(&cs->completions, cs->parsed, global);
564 }
565 1 }
566
567 1 static void complete_wsplit(EditorState *e, const CommandArgs *a)
568 {
569 1 CompletionState *cs = &e->cmdline.completion;
570
2/4
✓ Branch 0 (3→4) taken 1 times.
✗ Branch 1 (3→7) not taken.
✓ Branch 2 (5→6) taken 1 times.
✗ Branch 3 (5→7) not taken.
1 if (!cmdargs_has_flag(a, 't') && !cmdargs_has_flag(a, 'n')) {
571 1 collect_files(e, cs, COLLECT_ALL);
572 }
573 1 }
574
575 typedef struct {
576 char cmd_name[12];
577 void (*complete)(EditorState *e, const CommandArgs *a);
578 } CompletionHandler;
579
580 static const CompletionHandler completion_handlers[] = {
581 {"alias", complete_alias},
582 {"bind", complete_bind},
583 {"cd", complete_cd},
584 {"compile", complete_compile},
585 {"cursor", complete_cursor},
586 {"def-mode", complete_def_mode},
587 {"errorfmt", complete_errorfmt},
588 {"exec", complete_exec},
589 {"ft", complete_ft},
590 {"hi", complete_hi},
591 {"include", complete_include},
592 {"macro", complete_macro},
593 {"mode", complete_mode},
594 {"move-tab", complete_move_tab},
595 {"open", complete_open},
596 {"option", complete_option},
597 {"quit", complete_quit},
598 {"redo", complete_redo},
599 {"save", complete_save},
600 {"set", complete_set},
601 {"setenv", complete_setenv},
602 {"show", complete_show},
603 {"tag", complete_tag},
604 {"toggle", complete_toggle},
605 {"wsplit", complete_wsplit},
606 };
607
608 22 UNITTEST {
609 22 CHECK_BSEARCH_ARRAY(completion_handlers, cmd_name, strcmp);
610 // Ensure handlers are kept in sync with renamed/removed commands
611
2/2
✓ Branch 0 (8→4) taken 550 times.
✓ Branch 1 (8→9) taken 22 times.
572 for (size_t i = 0; i < ARRAYLEN(completion_handlers); i++) {
612 550 const char *name = completion_handlers[i].cmd_name;
613
1/2
✗ Branch 0 (5→6) not taken.
✓ Branch 1 (5→7) taken 550 times.
550 if (!find_normal_command(name)) {
614 BUG("completion handler for non-existent command: \"%s\"", name);
615 }
616 }
617 22 }
618
619 20 static bool can_collect_flags (
620 char **args,
621 size_t argc,
622 size_t nr_flag_args,
623 bool allow_flags_after_nonflags
624 ) {
625
2/2
✓ Branch 0 (2→5) taken 12 times.
✓ Branch 1 (2→11) taken 8 times.
20 if (allow_flags_after_nonflags) {
626
2/2
✓ Branch 0 (5→3) taken 6 times.
✓ Branch 1 (5→12) taken 10 times.
16 for (size_t i = 0; i < argc; i++) {
627
2/2
✓ Branch 0 (3→4) taken 4 times.
✓ Branch 1 (3→12) taken 2 times.
6 if (streq(args[i], "--")) {
628 return false;
629 }
630 }
631 return true;
632 }
633
634
2/2
✓ Branch 0 (11→6) taken 14 times.
✓ Branch 1 (11→12) taken 4 times.
18 for (size_t i = 0, nonflag = 0; i < argc; i++) {
635
2/2
✓ Branch 0 (6→7) taken 6 times.
✓ Branch 1 (6→9) taken 8 times.
14 if (args[i][0] != '-') {
636
2/2
✓ Branch 0 (7→8) taken 3 times.
✓ Branch 1 (7→12) taken 3 times.
6 if (++nonflag > nr_flag_args) {
637 return false;
638 }
639 3 continue;
640 }
641
2/2
✓ Branch 0 (9→10) taken 7 times.
✓ Branch 1 (9→12) taken 1 times.
8 if (streq(args[i], "--")) {
642 return false;
643 }
644 }
645
646 return true;
647 }
648
649 20 static bool collect_command_flags (
650 PointerArray *array,
651 char **args,
652 size_t argc,
653 const Command *cmd,
654 const CommandArgs *a,
655 const char *prefix
656 ) {
657 20 BUG_ON(prefix[0] != '-');
658 20 bool flags_after_nonflags = !(cmd->cmdopts & CMDOPT_NO_FLAGS_AFTER_ARGS);
659
2/2
✓ Branch 0 (4→5) taken 14 times.
✓ Branch 1 (4→21) taken 6 times.
20 if (!can_collect_flags(args, argc, a->nr_flag_args, flags_after_nonflags)) {
660 return false;
661 }
662
663 14 const char *flags = cmd->flags;
664
4/4
✓ Branch 0 (5→6) taken 3 times.
✓ Branch 1 (5→11) taken 11 times.
✓ Branch 2 (6→7) taken 2 times.
✓ Branch 3 (6→11) taken 1 times.
14 if (ascii_isalnum(prefix[1]) && prefix[2] == '\0') {
665
1/2
✓ Branch 0 (7→8) taken 2 times.
✗ Branch 1 (7→10) not taken.
2 if (strchr(flags, prefix[1])) {
666 2 ptr_array_append(array, xmemdup(prefix, 3));
667 }
668 2 return true;
669 }
670
671
2/2
✓ Branch 0 (11→12) taken 11 times.
✓ Branch 1 (11→21) taken 1 times.
12 if (prefix[1] != '\0') {
672 return true;
673 }
674
675 11 char buf[3] = "-";
676
2/2
✓ Branch 0 (20→13) taken 53 times.
✓ Branch 1 (20→21) taken 11 times.
64 for (size_t i = 0; flags[i]; i++) {
677
4/4
✓ Branch 0 (13→14) taken 47 times.
✓ Branch 1 (13→16) taken 6 times.
✓ Branch 2 (15→16) taken 13 times.
✓ Branch 3 (15→17) taken 34 times.
53 if (!ascii_isalnum(flags[i]) || cmdargs_has_flag(a, flags[i])) {
678 19 continue;
679 }
680 34 buf[1] = flags[i];
681 34 ptr_array_append(array, xmemdup(buf, 3));
682 }
683
684 return true;
685 }
686
687 3 static void collect_command_flag_args (
688 EditorState *e,
689 PointerArray *array,
690 const char *prefix,
691 const char *cmd,
692 const CommandArgs *a
693 ) {
694 3 char flag = a->flags[0];
695
2/2
✓ Branch 0 (2→3) taken 2 times.
✓ Branch 1 (2→6) taken 1 times.
3 if (streq(cmd, "bind")) {
696
1/2
✗ Branch 0 (3→4) not taken.
✓ Branch 1 (3→5) taken 2 times.
2 WARN_ON(flag != 'T');
697 2 collect_hashmap_keys(&e->modes, array, prefix);
698
1/2
✓ Branch 0 (6→7) taken 1 times.
✗ Branch 1 (6→13) not taken.
1 } else if (streq(cmd, "exec")) {
699
2/4
✓ Branch 0 (7→8) taken 1 times.
✗ Branch 1 (7→10) not taken.
✗ Branch 2 (8→9) not taken.
✓ Branch 3 (8→10) taken 1 times.
1 int fd = (flag == 'i') ? 0 : (flag == 'o' ? 1 : 2);
700
1/2
✗ Branch 0 (10→11) not taken.
✓ Branch 1 (10→12) taken 1 times.
1 WARN_ON(fd == 2 && flag != 'e');
701 1 collect_exec_actions(array, prefix, fd);
702 }
703 // TODO: Completions for `open -e` and `save -e`
704 3 }
705
706 // Only recurses for cmdname="repeat" and typically not more than once
707 // NOLINTNEXTLINE(misc-no-recursion)
708 87 static void collect_completions(EditorState *e, char **args, size_t argc)
709 {
710 87 CompletionState *cs = &e->cmdline.completion;
711 87 PointerArray *arr = &cs->completions;
712 87 const char *prefix = cs->parsed;
713
2/2
✓ Branch 0 (2→3) taken 4 times.
✓ Branch 1 (2→7) taken 83 times.
87 if (!argc) {
714 4 collect_normal_commands(arr, prefix);
715 4 collect_normal_aliases(e, arr, prefix);
716 8 return;
717 }
718
719
2/2
✓ Branch 0 (7→5) taken 160 times.
✓ Branch 1 (7→8) taken 83 times.
243 for (size_t i = 0; i < argc; i++) {
720
1/2
✓ Branch 0 (5→6) taken 160 times.
✗ Branch 1 (5→39) not taken.
160 if (!args[i]) {
721 // Embedded NULLs indicate there are multiple commands.
722 // Just return early here and avoid handling this case.
723 return;
724 }
725 }
726
727 83 const char *cmdname = args[0];
728 83 const Command *cmd = find_normal_command(cmdname);
729
1/2
✓ Branch 0 (9→10) taken 83 times.
✗ Branch 1 (9→39) not taken.
83 if (!cmd) {
730 return;
731 }
732
733 83 char **args_copy = copy_string_array(args + 1, argc - 1);
734 83 CommandArgs a = cmdargs_new(args_copy);
735 83 ArgParseError err = do_parse_args(cmd, &a);
736
737
2/2
✓ Branch 0 (12→13) taken 3 times.
✓ Branch 1 (12→15) taken 80 times.
83 if (err == ARGERR_OPTION_ARGUMENT_MISSING) {
738 3 collect_command_flag_args(e, arr, prefix, cmdname, &a);
739 3 goto out;
740 }
741
742 80 bool dash = (prefix[0] == '-');
743 80 if (
744
2/2
✓ Branch 0 (15→16) taken 79 times.
✓ Branch 1 (15→19) taken 1 times.
80 (err != ARGERR_NONE && err != ARGERR_TOO_FEW_ARGUMENTS)
745
5/6
✓ Branch 0 (16→17) taken 6 times.
✓ Branch 1 (16→20) taken 73 times.
✓ Branch 2 (17→18) taken 6 times.
✗ Branch 3 (17→20) not taken.
✓ Branch 4 (18→19) taken 1 times.
✓ Branch 5 (18→21) taken 5 times.
79 || (a.nr_args >= cmd->max_args && cmd->max_args != 0xFF && !dash)
746 ) {
747 2 goto out;
748 }
749
750
4/4
✓ Branch 0 (20→21) taken 15 times.
✓ Branch 1 (20→24) taken 58 times.
✓ Branch 2 (22→23) taken 14 times.
✓ Branch 3 (22→24) taken 6 times.
78 if (dash && collect_command_flags(arr, args + 1, argc - 1, cmd, &a, prefix)) {
751 14 goto out;
752 }
753
754
2/2
✓ Branch 0 (24→25) taken 1 times.
✓ Branch 1 (24→26) taken 63 times.
64 if (cmd->max_args == 0) {
755 1 goto out;
756 }
757
758 63 const CompletionHandler *h = BSEARCH(cmdname, completion_handlers, vstrcmp);
759
2/2
✓ Branch 0 (27→28) taken 60 times.
✓ Branch 1 (27→29) taken 3 times.
63 if (h) {
760 60 h->complete(e, &a);
761
2/2
✓ Branch 0 (29→30) taken 1 times.
✓ Branch 1 (29→31) taken 2 times.
3 } else if (streq(cmdname, "repeat")) {
762
2/2
✓ Branch 0 (31→32) taken 1 times.
✓ Branch 1 (31→33) taken 1 times.
2 if (a.nr_args == 1) {
763 1 collect_normal_commands(arr, prefix);
764
1/2
✗ Branch 0 (33→34) not taken.
✓ Branch 1 (33→35) taken 1 times.
1 } else if (a.nr_args >= 2) {
765 1 collect_completions(e, args + 2, argc - 2);
766 }
767 }
768
769 1 out:
770 83 free_string_array(args_copy);
771 }
772
773 395 static bool is_var(const char *str, size_t len)
774 {
775
4/4
✓ Branch 0 (2→3) taken 341 times.
✓ Branch 1 (2→9) taken 54 times.
✓ Branch 2 (3→4) taken 265 times.
✓ Branch 3 (3→9) taken 76 times.
395 if (len == 0 || str[0] != '$') {
776 return false;
777 }
778
2/2
✓ Branch 0 (4→5) taken 243 times.
✓ Branch 1 (4→9) taken 22 times.
265 if (len == 1) {
779 return true;
780 }
781
2/2
✓ Branch 0 (5→8) taken 155 times.
✓ Branch 1 (5→9) taken 88 times.
243 if (!is_alpha_or_underscore(str[1])) {
782 return false;
783 }
784
2/2
✓ Branch 0 (8→6) taken 384 times.
✓ Branch 1 (8→9) taken 133 times.
517 for (size_t i = 2; i < len; i++) {
785
2/2
✓ Branch 0 (6→7) taken 362 times.
✓ Branch 1 (6→9) taken 22 times.
384 if (!is_alnum_or_underscore(str[i])) {
786 return false;
787 }
788 }
789 return true;
790 }
791
792 22 UNITTEST {
793 // NOLINTBEGIN(bugprone-assert-side-effect)
794 22 BUG_ON(!is_var(STRN("$VAR")));
795 22 BUG_ON(!is_var(STRN("$xy_190")));
796 22 BUG_ON(!is_var(STRN("$__x_y_z")));
797 22 BUG_ON(!is_var(STRN("$x")));
798 22 BUG_ON(!is_var(STRN("$A")));
799 22 BUG_ON(!is_var(STRN("$_0")));
800 22 BUG_ON(!is_var(STRN("$")));
801 22 BUG_ON(is_var(STRN("")));
802 22 BUG_ON(is_var(STRN("A")));
803 22 BUG_ON(is_var(STRN("$.a")));
804 22 BUG_ON(is_var(STRN("$xyz!")));
805 22 BUG_ON(is_var(STRN("$1")));
806 22 BUG_ON(is_var(STRN("$09")));
807 22 BUG_ON(is_var(STRN("$1a")));
808 // NOLINTEND(bugprone-assert-side-effect)
809 22 }
810
811 2073 static int strptrcmp(const void *v1, const void *v2)
812 {
813 2073 const char *const *s1 = v1;
814 2073 const char *const *s2 = v2;
815 2073 return strcmp(*s1, *s2);
816 }
817
818 87 static void init_completion(EditorState *e, const CommandLine *cmdline)
819 {
820 87 CompletionState *cs = &e->cmdline.completion;
821 87 const CommandRunner runner = normal_mode_cmdrunner(e);
822 87 BUG_ON(cs->orig);
823 87 BUG_ON(runner.e != e);
824 87 BUG_ON(!runner.lookup_alias);
825
826 87 const size_t cmdline_pos = cmdline->pos;
827 87 char *const cmd = string_clone_cstring(&cmdline->buf);
828 87 PointerArray array = PTR_ARRAY_INIT;
829 87 ssize_t semicolon = -1;
830 87 ssize_t completion_pos = -1;
831
832 87 for (size_t pos = 0; true; ) {
833
2/2
✓ Branch 0 (12→10) taken 164 times.
✓ Branch 1 (12→13) taken 254 times.
418 while (ascii_isspace(cmd[pos])) {
834 164 pos++;
835 }
836
837
2/2
✓ Branch 0 (13→14) taken 32 times.
✓ Branch 1 (13→15) taken 222 times.
254 if (pos >= cmdline_pos) {
838 32 completion_pos = cmdline_pos;
839 32 break;
840 }
841
842
1/2
✓ Branch 0 (15→16) taken 222 times.
✗ Branch 1 (15→41) not taken.
222 if (!cmd[pos]) {
843 break;
844 }
845
846
2/2
✓ Branch 0 (16→17) taken 3 times.
✓ Branch 1 (16→19) taken 219 times.
222 if (cmd[pos] == ';') {
847 3 semicolon = array.count;
848 3 ptr_array_append(&array, NULL);
849 3 pos++;
850 3 continue;
851 }
852
853 219 CommandParseError err;
854 219 size_t end = find_end(cmd, pos, &err);
855
3/4
✓ Branch 0 (20→21) taken 219 times.
✗ Branch 1 (20→22) not taken.
✓ Branch 2 (21→22) taken 55 times.
✓ Branch 3 (21→23) taken 164 times.
219 if (err != CMDERR_NONE || end >= cmdline_pos) {
856 55 completion_pos = pos;
857 55 break;
858 }
859
860
2/2
✓ Branch 0 (23→24) taken 86 times.
✓ Branch 1 (23→38) taken 78 times.
164 if (semicolon + 1 == array.count) {
861 86 char *name = xstrslice(cmd, pos, end);
862 86 const char *value = runner.lookup_alias(runner.e, name);
863
1/2
✗ Branch 0 (26→27) not taken.
✓ Branch 1 (26→35) taken 86 times.
86 if (value) {
864 size_t save = array.count;
865 if (parse_commands(&runner, &array, value) != CMDERR_NONE) {
866 for (size_t i = save, n = array.count; i < n; i++) {
867 free(array.ptrs[i]);
868 array.ptrs[i] = NULL;
869 }
870 array.count = save;
871 ptr_array_append(&array, parse_command_arg(&runner, name, end - pos));
872 } else {
873 // Remove NULL
874 array.count--;
875 }
876 } else {
877 86 ptr_array_append(&array, parse_command_arg(&runner, name, end - pos));
878 }
879 86 free(name);
880 } else {
881 78 ptr_array_append(&array, parse_command_arg(&runner, cmd + pos, end - pos));
882 }
883 164 pos = end;
884 }
885
886 87 const char *str = cmd + completion_pos;
887 87 size_t len = cmdline_pos - completion_pos;
888
2/2
✓ Branch 0 (41→42) taken 1 times.
✓ Branch 1 (41→46) taken 86 times.
87 if (is_var(str, len)) {
889 1 char *name = xstrslice(str, 1, len);
890 1 completion_pos++;
891 1 collect_env(e, &cs->completions, name);
892 1 collect_normal_vars(&cs->completions, name);
893 1 free(name);
894 } else {
895 86 cs->escaped = string_view(str, len);
896 86 cs->parsed = parse_command_arg(&runner, str, len);
897 86 cs->add_space_after_single_match = true;
898 86 size_t count = array.count;
899
2/2
✓ Branch 0 (47→48) taken 83 times.
✓ Branch 1 (47→49) taken 3 times.
86 char **args = count ? (char**)array.ptrs + 1 + semicolon : NULL;
900 83 size_t argc = count ? count - semicolon - 1 : 0;
901 86 collect_completions(e, args, argc);
902 }
903
904 87 ptr_array_free(&array);
905 87 ptr_array_sort(&cs->completions, strptrcmp);
906 87 cs->orig = cmd; // (takes ownership)
907 87 cs->tail = strview_from_cstring(cmd + cmdline_pos);
908 87 cs->head_len = completion_pos;
909 87 }
910
911 93 static void do_complete_command(CommandLine *cmdline)
912 {
913 93 const CompletionState *cs = &cmdline->completion;
914 93 const PointerArray *arr = &cs->completions;
915 93 const StringView middle = strview_from_cstring(arr->ptrs[cs->idx]);
916 93 const StringView tail = cs->tail;
917 93 const size_t head_length = cs->head_len;
918
919 93 String buf = string_new(head_length + tail.length + middle.length + 16);
920 93 string_append_buf(&buf, cs->orig, head_length);
921 93 string_append_escaped_arg_sv(&buf, middle, !cs->tilde_expanded);
922
923 93 bool single_completion = (arr->count == 1);
924
4/4
✓ Branch 0 (5→6) taken 42 times.
✓ Branch 1 (5→8) taken 51 times.
✓ Branch 2 (6→7) taken 41 times.
✓ Branch 3 (6→8) taken 1 times.
93 if (single_completion && cs->add_space_after_single_match) {
925 41 string_append_byte(&buf, ' ');
926 }
927
928 93 size_t pos = buf.len;
929 93 string_append_strview(&buf, &tail);
930 93 cmdline_set_text(cmdline, string_borrow_cstring(&buf));
931 93 cmdline->pos = pos;
932 93 string_free(&buf);
933
934
2/2
✓ Branch 0 (12→13) taken 42 times.
✓ Branch 1 (12→14) taken 51 times.
93 if (single_completion) {
935 42 reset_completion(cmdline);
936 }
937 93 }
938
939 106 void complete_command_next(EditorState *e)
940 {
941 106 CompletionState *cs = &e->cmdline.completion;
942 106 const bool init = !cs->orig;
943
2/2
✓ Branch 0 (2→3) taken 86 times.
✓ Branch 1 (2→4) taken 20 times.
106 if (init) {
944 86 init_completion(e, &e->cmdline);
945 }
946 106 size_t count = cs->completions.count;
947
2/2
✓ Branch 0 (4→5) taken 88 times.
✓ Branch 1 (4→9) taken 18 times.
106 if (!count) {
948 return;
949 }
950
2/2
✓ Branch 0 (5→6) taken 20 times.
✓ Branch 1 (5→8) taken 68 times.
88 if (!init) {
951 20 cs->idx = size_increment_wrapped(cs->idx, count);
952 }
953 88 do_complete_command(&e->cmdline);
954 }
955
956 5 void complete_command_prev(EditorState *e)
957 {
958 5 CompletionState *cs = &e->cmdline.completion;
959 5 const bool init = !cs->orig;
960
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 4 times.
5 if (init) {
961 1 init_completion(e, &e->cmdline);
962 }
963 5 size_t count = cs->completions.count;
964
1/2
✓ Branch 0 (4→5) taken 5 times.
✗ Branch 1 (4→9) not taken.
5 if (!count) {
965 return;
966 }
967
2/2
✓ Branch 0 (5→6) taken 4 times.
✓ Branch 1 (5→8) taken 1 times.
5 if (!init) {
968 4 cs->idx = size_decrement_wrapped(cs->idx, count);
969 }
970 5 do_complete_command(&e->cmdline);
971 }
972
973 127 void reset_completion(CommandLine *cmdline)
974 {
975 127 CompletionState *cs = &cmdline->completion;
976 127 free(cs->parsed);
977 127 free(cs->orig);
978 127 ptr_array_free(&cs->completions);
979 127 *cs = (CompletionState){.orig = NULL};
980 127 }
981
982 12 void collect_hashmap_keys(const HashMap *map, PointerArray *a, const char *prefix)
983 {
984 12 size_t prefix_len = strlen(prefix);
985
2/2
✓ Branch 0 (8→3) taken 42 times.
✓ Branch 1 (8→9) taken 12 times.
66 for (HashMapIter it = hashmap_iter(map); hashmap_next(&it); ) {
986 42 const char *name = it.entry->key;
987
2/2
✓ Branch 0 (3→4) taken 11 times.
✓ Branch 1 (3→7) taken 31 times.
42 if (str_has_strn_prefix(name, prefix, prefix_len)) {
988 11 ptr_array_append(a, xstrdup(name));
989 }
990 }
991 12 }
992