dte test coverage


Directory: ./
File: src/completion.c
Date: 2025-07-03 15:44:24
Exec Total Coverage
Lines: 469 529 88.7%
Functions: 44 48 91.7%
Branches: 226 314 72.0%

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