dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 89.9% 464 / 2 / 518
Functions: 91.8% 45 / 0 / 49
Branches: 71.4% 217 / 20 / 324

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