dte test coverage


Directory: ./
File: src/cmdline.c
Date: 2026-01-09 16:07:09
Coverage Exec Excl Total
Lines: 51.7% 140 0 271
Functions: 56.8% 21 0 37
Branches: 39.4% 41 0 104

Line Branch Exec Source
1 #include <stdlib.h>
2 #include <string.h>
3 #include "cmdline.h"
4 #include "command/args.h"
5 #include "command/macro.h"
6 #include "commands.h"
7 #include "completion.h"
8 #include "copy.h"
9 #include "editor.h"
10 #include "history.h"
11 #include "options.h"
12 #include "regexp.h"
13 #include "search.h"
14 #include "selection.h"
15 #include "terminal/osc52.h"
16 #include "util/arith.h"
17 #include "util/ascii.h"
18 #include "util/bsearch.h"
19 #include "util/debug.h"
20 #include "util/log.h"
21 #include "util/utf8.h"
22
23 4 static void cmdline_delete(CommandLine *c)
24 {
25 4 size_t pos = c->pos;
26
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 3 times.
4 if (pos == c->buf.len) {
27 1 return;
28 }
29
30 3 u_get_char(c->buf.buffer, c->buf.len, &pos);
31 3 string_remove(&c->buf, c->pos, pos - c->pos);
32 }
33
34 13 void cmdline_clear(CommandLine *c)
35 {
36 13 string_clear(&c->buf);
37 13 c->pos = 0;
38 13 c->search_pos = NULL;
39 13 }
40
41 11 void cmdline_free(CommandLine *c)
42 {
43 11 cmdline_clear(c);
44 11 string_free(&c->buf);
45 11 free(c->search_text);
46 11 reset_completion(c);
47 11 }
48
49 172 static void set_text(CommandLine *c, const char *text)
50 {
51 172 size_t text_len = strlen(text);
52 172 c->pos = text_len;
53 172 string_clear(&c->buf);
54 172 string_append_buf(&c->buf, text, text_len);
55 172 }
56
57 172 void cmdline_set_text(CommandLine *c, const char *text)
58 {
59 172 c->search_pos = NULL;
60 172 set_text(c, text);
61 172 }
62
63 // Reset command completion and history search state (after cursor
64 // position or buffer is changed)
65 20 static bool cmdline_soft_reset(CommandLine *c)
66 {
67 20 c->search_pos = NULL;
68 20 maybe_reset_completion(c);
69 20 return true;
70 }
71
72 2 static bool cmd_bol(EditorState *e, const CommandArgs *a)
73 {
74 2 BUG_ON(a->nr_args);
75 2 e->cmdline.pos = 0;
76 2 return cmdline_soft_reset(&e->cmdline);
77 }
78
79 1 static bool cmd_cancel(EditorState *e, const CommandArgs *a)
80 {
81 1 BUG_ON(a->nr_args);
82 1 CommandLine *c = &e->cmdline;
83 1 cmdline_clear(c);
84 1 pop_input_mode(e);
85 1 reset_completion(c);
86 1 return true;
87 }
88
89 static bool cmd_clear(EditorState *e, const CommandArgs *a)
90 {
91 BUG_ON(a->nr_args);
92 cmdline_clear(&e->cmdline);
93 return true;
94 }
95
96 static bool cmd_copy(EditorState *e, const CommandArgs *a)
97 {
98 static const FlagMapping map[] = {
99 {'b', TCOPY_CLIPBOARD},
100 {'p', TCOPY_PRIMARY},
101 };
102
103 Terminal *term = &e->terminal;
104 TermCopyFlags flags = cmdargs_convert_flags(a, map, ARRAYLEN(map));
105 bool internal = cmdargs_has_flag(a, 'i') || flags == 0;
106 bool osc52 = flags && (term->features & TFLAG_OSC52_COPY);
107 String *buf = &e->cmdline.buf;
108 size_t len = buf->len;
109
110 if (internal) {
111 char *str = string_clone_cstring(buf);
112 record_copy(&e->clipboard, str, len, false);
113 }
114
115 if (osc52) {
116 bool ok = term_osc52_copy(&term->obuf, strview_from_string(buf), flags);
117 LOG_ERRNO_ON(!ok, "term_osc52_copy");
118 }
119
120 return true;
121 }
122
123 2 static bool cmd_delete(EditorState *e, const CommandArgs *a)
124 {
125 2 BUG_ON(a->nr_args);
126 2 CommandLine *c = &e->cmdline;
127 2 cmdline_delete(c);
128 2 return cmdline_soft_reset(c);
129 }
130
131 1 static bool cmd_delete_eol(EditorState *e, const CommandArgs *a)
132 {
133 1 BUG_ON(a->nr_args);
134 1 CommandLine *c = &e->cmdline;
135 1 c->buf.len = c->pos;
136 1 return cmdline_soft_reset(c);
137 }
138
139 1 static bool cmd_delete_word(EditorState *e, const CommandArgs *a)
140 {
141 1 BUG_ON(a->nr_args);
142 1 CommandLine *c = &e->cmdline;
143 1 const char *buf = c->buf.buffer;
144 1 const size_t len = c->buf.len;
145 1 size_t i = c->pos;
146
147
1/2
✓ Branch 4 → 6 taken 1 time.
✗ Branch 4 → 14 not taken.
1 if (i == len) {
148 return true;
149 }
150
151
3/4
✓ Branch 6 → 7 taken 6 times.
✗ Branch 6 → 8 not taken.
✓ Branch 7 → 5 taken 5 times.
✓ Branch 7 → 8 taken 1 time.
6 while (i < len && is_word_byte(buf[i])) {
152 5 i++;
153 }
154
155
3/4
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 12 not taken.
✓ Branch 11 → 9 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
2 while (i < len && !is_word_byte(buf[i])) {
156 1 i++;
157 }
158
159 1 string_remove(&c->buf, c->pos, i - c->pos);
160 1 return cmdline_soft_reset(c);
161 }
162
163 2 static bool cmd_eol(EditorState *e, const CommandArgs *a)
164 {
165 2 BUG_ON(a->nr_args);
166 2 CommandLine *c = &e->cmdline;
167 2 c->pos = c->buf.len;
168 2 return cmdline_soft_reset(c);
169 }
170
171 2 static bool cmd_erase(EditorState *e, const CommandArgs *a)
172 {
173 2 BUG_ON(a->nr_args);
174 2 CommandLine *c = &e->cmdline;
175
1/2
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 7 not taken.
2 if (c->pos > 0) {
176 2 u_prev_char(c->buf.buffer, &c->pos);
177 2 cmdline_delete(c);
178 }
179 2 return cmdline_soft_reset(c);
180 }
181
182 1 static bool cmd_erase_bol(EditorState *e, const CommandArgs *a)
183 {
184 1 BUG_ON(a->nr_args);
185 1 CommandLine *c = &e->cmdline;
186 1 string_remove(&c->buf, 0, c->pos);
187 1 c->pos = 0;
188 1 return cmdline_soft_reset(c);
189 }
190
191 2 static bool cmd_erase_word(EditorState *e, const CommandArgs *a)
192 {
193 2 BUG_ON(a->nr_args);
194 2 CommandLine *c = &e->cmdline;
195 2 size_t i = c->pos;
196
1/2
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 15 not taken.
2 if (i == 0) {
197 return true;
198 }
199
200 // open /path/to/file^W => open /path/to/
201
202 // erase whitespace
203
3/4
✓ Branch 5 → 6 taken 3 times.
✗ Branch 5 → 7 not taken.
✓ Branch 6 → 5 taken 1 time.
✓ Branch 6 → 7 taken 2 times.
3 while (i && ascii_isspace(c->buf.buffer[i - 1])) {
204 i--;
205 }
206
207 // erase non-word bytes
208
2/4
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 10 not taken.
✗ Branch 9 → 8 not taken.
✓ Branch 9 → 10 taken 2 times.
2 while (i && !is_word_byte(c->buf.buffer[i - 1])) {
209 i--;
210 }
211
212 // erase word bytes
213
3/4
✓ Branch 11 → 12 taken 12 times.
✗ Branch 11 → 13 not taken.
✓ Branch 12 → 11 taken 10 times.
✓ Branch 12 → 13 taken 2 times.
12 while (i && is_word_byte(c->buf.buffer[i - 1])) {
214 i--;
215 }
216
217 2 string_remove(&c->buf, i, c->pos - i);
218 2 c->pos = i;
219 2 return cmdline_soft_reset(c);
220 }
221
222 static bool cmd_insert(EditorState *e, const CommandArgs *a)
223 {
224 const char *str = a->args[0];
225 size_t len = strlen(str);
226 CommandLine *c = &e->cmdline;
227 string_insert_buf(&c->buf, c->pos, str, len);
228 c->pos += cmdargs_has_flag(a, 'm') ? len : 0;
229 c->search_pos = NULL;
230 return true;
231 }
232
233 static bool do_history_prev(const History *hist, CommandLine *c, bool prefix_search)
234 {
235 if (!c->search_pos) {
236 free(c->search_text);
237 c->search_text = string_clone_cstring(&c->buf);
238 }
239
240 const char *search_text = prefix_search ? c->search_text : "";
241 if (history_search_forward(hist, &c->search_pos, search_text)) {
242 BUG_ON(!c->search_pos);
243 set_text(c, c->search_pos->text);
244 }
245
246 maybe_reset_completion(c);
247 return true;
248 }
249
250 static bool do_history_next(const History *hist, CommandLine *c, bool prefix_search)
251 {
252 if (!c->search_pos) {
253 goto out;
254 }
255
256 const char *search_text = prefix_search ? c->search_text : "";
257 if (history_search_backward(hist, &c->search_pos, search_text)) {
258 BUG_ON(!c->search_pos);
259 set_text(c, c->search_pos->text);
260 } else {
261 set_text(c, c->search_text);
262 c->search_pos = NULL;
263 }
264
265 out:
266 maybe_reset_completion(c);
267 return true;
268 }
269
270 static bool cmd_search_history_next(EditorState *e, const CommandArgs *a)
271 {
272 BUG_ON(a->nr_args);
273 bool prefix_search = !cmdargs_has_flag(a, 'S');
274 return do_history_next(&e->search_history, &e->cmdline, prefix_search);
275 }
276
277 static bool cmd_search_history_prev(EditorState *e, const CommandArgs *a)
278 {
279 BUG_ON(a->nr_args);
280 bool prefix_search = !cmdargs_has_flag(a, 'S');
281 return do_history_prev(&e->search_history, &e->cmdline, prefix_search);
282 }
283
284 static bool cmd_command_history_next(EditorState *e, const CommandArgs *a)
285 {
286 BUG_ON(a->nr_args);
287 bool prefix_search = !cmdargs_has_flag(a, 'S');
288 return do_history_next(&e->command_history, &e->cmdline, prefix_search);
289 }
290
291 static bool cmd_command_history_prev(EditorState *e, const CommandArgs *a)
292 {
293 BUG_ON(a->nr_args);
294 bool prefix_search = !cmdargs_has_flag(a, 'S');
295 return do_history_prev(&e->command_history, &e->cmdline, prefix_search);
296 }
297
298 2 static bool cmd_left(EditorState *e, const CommandArgs *a)
299 {
300 2 BUG_ON(a->nr_args);
301 2 CommandLine *c = &e->cmdline;
302
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 1 time.
2 if (c->pos) {
303 1 u_prev_char(c->buf.buffer, &c->pos);
304 }
305 2 return cmdline_soft_reset(c);
306 }
307
308 static bool cmd_paste(EditorState *e, const CommandArgs *a)
309 {
310 const Clipboard *clip = &e->clipboard;
311 const size_t len = clip->len;
312 if (len == 0) {
313 return true;
314 }
315
316 CommandLine *c = &e->cmdline;
317 char newline_replacement = cmdargs_has_flag(a, 'n') ? ';' : ' ';
318 string_insert_buf(&c->buf, c->pos, clip->buf, len);
319 strn_replace_byte(c->buf.buffer + c->pos, len, '\n', newline_replacement);
320 c->pos += cmdargs_has_flag(a, 'm') ? len : 0;
321 return cmdline_soft_reset(c);
322 }
323
324 2 static bool cmd_right(EditorState *e, const CommandArgs *a)
325 {
326 2 BUG_ON(a->nr_args);
327 2 CommandLine *c = &e->cmdline;
328
1/2
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 6 not taken.
2 if (c->pos < c->buf.len) {
329 2 u_get_char(c->buf.buffer, c->buf.len, &c->pos);
330 }
331 2 return cmdline_soft_reset(c);
332 }
333
334 static bool cmd_toggle(EditorState *e, const CommandArgs *a)
335 {
336 const char *option_name = a->args[0];
337 bool global = cmdargs_has_flag(a, 'g');
338 size_t nr_values = a->nr_args - 1;
339 if (nr_values == 0) {
340 return toggle_option(e, option_name, global, false);
341 }
342
343 char **values = a->args + 1;
344 return toggle_option_values(e, option_name, global, false, values, nr_values);
345 }
346
347 2 static bool cmd_word_bwd(EditorState *e, const CommandArgs *a)
348 {
349 2 BUG_ON(a->nr_args);
350 2 CommandLine *c = &e->cmdline;
351
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 1 time.
2 if (c->pos <= 1) {
352 1 c->pos = 0;
353 1 return cmdline_soft_reset(c);
354 }
355
356 1 const char *const buf = c->buf.buffer;
357 1 size_t i = c->pos - 1;
358
359
3/4
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 10 not taken.
✓ Branch 9 → 7 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
2 while (i > 0 && !is_word_byte(buf[i])) {
360 1 i--;
361 }
362
363
3/4
✓ Branch 12 → 13 taken 6 times.
✗ Branch 12 → 14 not taken.
✓ Branch 13 → 11 taken 5 times.
✓ Branch 13 → 14 taken 1 time.
6 while (i > 0 && is_word_byte(buf[i])) {
364 5 i--;
365 }
366
367
1/2
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 16 not taken.
1 if (i > 0) {
368 1 i++;
369 }
370
371 1 c->pos = i;
372 1 return cmdline_soft_reset(c);
373 }
374
375 1 static bool cmd_word_fwd(EditorState *e, const CommandArgs *a)
376 {
377 1 BUG_ON(a->nr_args);
378 1 CommandLine *c = &e->cmdline;
379 1 const char *buf = c->buf.buffer;
380 1 const size_t len = c->buf.len;
381 1 size_t i = c->pos;
382
383
3/4
✓ Branch 6 → 7 taken 6 times.
✗ Branch 6 → 8 not taken.
✓ Branch 7 → 5 taken 5 times.
✓ Branch 7 → 8 taken 1 time.
6 while (i < len && is_word_byte(buf[i])) {
384 5 i++;
385 }
386
387
3/4
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 11 → 9 taken 1 time.
✗ Branch 11 → 12 not taken.
2 while (i < len && !is_word_byte(buf[i])) {
388 1 i++;
389 }
390
391 1 c->pos = i;
392 1 return cmdline_soft_reset(c);
393 }
394
395 static bool cmd_complete_next(EditorState *e, const CommandArgs *a)
396 {
397 BUG_ON(a->nr_args);
398 complete_command_next(e);
399 return true;
400 }
401
402 static bool cmd_complete_prev(EditorState *e, const CommandArgs *a)
403 {
404 BUG_ON(a->nr_args);
405 complete_command_prev(e);
406 return true;
407 }
408
409 static bool cmd_direction(EditorState *e, const CommandArgs *a)
410 {
411 BUG_ON(a->nr_args);
412 toggle_search_direction(&e->search);
413 return true;
414 }
415
416 static bool cmd_command_mode_accept(EditorState *e, const CommandArgs *a)
417 {
418 CommandLine *c = &e->cmdline;
419 reset_completion(c);
420 pop_input_mode(e);
421
422 const char *str = string_borrow_cstring(&c->buf);
423 cmdline_clear(c);
424 if (!cmdargs_has_flag(a, 'H') && str[0] != ' ') {
425 // This is done before handle_command() because "command [text]"
426 // can modify the contents of the command-line
427 history_append(&e->command_history, str);
428 }
429
430 e->err.command_name = NULL;
431 return handle_normal_command(e, str, true);
432 }
433
434 static bool cmd_search_mode_accept(EditorState *e, const CommandArgs *a)
435 {
436 CommandLine *c = &e->cmdline;
437 bool add_to_history = !cmdargs_has_flag(a, 'H');
438 const char *pat = NULL;
439
440 if (c->buf.len > 0) {
441 String *s = &c->buf;
442 if (cmdargs_has_flag(a, 'e')) {
443 // Escape the regex; to match as plain text
444 char *original = string_clone_cstring(s);
445 size_t origlen = string_clear(s);
446 size_t bufsize = xmul(2, origlen) + 1;
447 char *buf = string_reserve_space(s, bufsize);
448 s->len = regexp_escapeb(buf, bufsize, original, origlen);
449 BUG_ON(s->len < origlen);
450 free(original);
451 }
452
453 pat = string_borrow_cstring(s);
454 search_set_regexp(&e->search, pat);
455 if (add_to_history) {
456 history_append(&e->search_history, pat);
457 }
458 }
459
460 if (e->macro.recording) {
461 macro_search_hook(&e->macro, pat, e->search.reverse, add_to_history);
462 }
463
464 // Unselect, unless selection mode is active
465 view_set_selection_type(e->view, e->view->select_mode);
466
467 e->err.command_name = NULL;
468 bool found = search_next(e->view, &e->search, e->options.case_sensitive_search);
469 cmdline_clear(c);
470 pop_input_mode(e);
471 return found;
472 }
473
474 // Note that some of the `Command::flags` entries here aren't actually
475 // used in the `cmd` handler and are only included to mirror commands
476 // of the same name in normal mode. This is done as a convenience for
477 // allowing key binding commands like e.g. `bind -cns C-M-c 'copy -bk'`
478 // to be used, instead of needing 2 different commands (with and without
479 // the `-k` flag for normal vs. command/search modes).
480
481 #define CMD(name, flags, min, max, func) \
482 {name, flags, 0, min, max, func}
483
484 static const Command common_cmds[] = {
485 CMD("bol", "st", 0, 0, cmd_bol), // Ignored flags: s, t
486 CMD("cancel", "", 0, 0, cmd_cancel),
487 CMD("clear", "Ii", 0, 0, cmd_clear), // Ignored flags: I, i
488 CMD("copy", "bikp", 0, 0, cmd_copy), // Ignored flag: k
489 CMD("delete", "", 0, 0, cmd_delete),
490 CMD("delete-eol", "n", 0, 0, cmd_delete_eol), // Ignored flag: n
491 CMD("delete-word", "s", 0, 0, cmd_delete_word), // Ignored flag: s
492 CMD("eol", "", 0, 0, cmd_eol),
493 CMD("erase", "", 0, 0, cmd_erase),
494 CMD("erase-bol", "", 0, 0, cmd_erase_bol),
495 CMD("erase-word", "s", 0, 0, cmd_erase_word), // Ignored flag: s
496 CMD("insert", "km", 1, 1, cmd_insert), // Ignored flag: k
497 CMD("left", "", 0, 0, cmd_left),
498 CMD("paste", "acmn", 0, 0, cmd_paste), // Ignored flags: a, c
499 CMD("right", "", 0, 0, cmd_right),
500 CMD("toggle", "gv", 1, -1, cmd_toggle), // Ignored flag: v
501 CMD("word-bwd", "s", 0, 0, cmd_word_bwd), // Ignored flag: s
502 CMD("word-fwd", "s", 0, 0, cmd_word_fwd), // Ignored flag: s
503 };
504
505 static const Command search_cmds[] = {
506 CMD("accept", "eH", 0, 0, cmd_search_mode_accept),
507 CMD("direction", "", 0, 0, cmd_direction),
508 CMD("history-next", "S", 0, 0, cmd_search_history_next),
509 CMD("history-prev", "S", 0, 0, cmd_search_history_prev),
510 };
511
512 static const Command command_cmds[] = {
513 CMD("accept", "H", 0, 0, cmd_command_mode_accept),
514 CMD("complete-next", "", 0, 0, cmd_complete_next),
515 CMD("complete-prev", "", 0, 0, cmd_complete_prev),
516 CMD("history-next", "S", 0, 0, cmd_command_history_next),
517 CMD("history-prev", "S", 0, 0, cmd_command_history_prev),
518 };
519
520 373 static const Command *find_cmd_mode_command(const char *name)
521 {
522 373 const Command *cmd = BSEARCH(name, common_cmds, command_cmp);
523
2/2
✓ Branch 3 → 4 taken 64 times.
✓ Branch 3 → 5 taken 309 times.
373 return cmd ? cmd : BSEARCH(name, command_cmds, command_cmp);
524 }
525
526 360 static const Command *find_search_mode_command(const char *name)
527 {
528 360 const Command *cmd = BSEARCH(name, common_cmds, command_cmp);
529
2/2
✓ Branch 3 → 4 taken 63 times.
✓ Branch 3 → 5 taken 297 times.
360 return cmd ? cmd : BSEARCH(name, search_cmds, command_cmp);
530 }
531
532 const CommandSet cmd_mode_commands = {
533 .lookup = find_cmd_mode_command
534 };
535
536 const CommandSet search_mode_commands = {
537 .lookup = find_search_mode_command
538 };
539