dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 90.2% 480 / 2 / 534
Functions: 91.8% 45 / 0 / 49
Branches: 72.7% 218 / 20 / 320

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