dte test coverage


Directory: ./
File: src/completion.c
Date: 2024-12-21 16:03:22
Exec Total Coverage
Lines: 459 508 90.4%
Functions: 44 48 91.7%
Branches: 221 300 73.7%

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