dte test coverage


Directory: ./
File: src/cmdline.c
Date: 2025-09-07 23:01:39
Exec Total Coverage
Lines: 140 263 53.2%
Functions: 21 36 58.3%
Branches: 41 102 40.2%

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 0 (2→3) taken 1 times.
✓ Branch 1 (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 0 (4→6) taken 1 times.
✗ Branch 1 (4→14) not taken.
1 if (i == len) {
148 return true;
149 }
150
151
3/4
✓ Branch 0 (6→7) taken 6 times.
✗ Branch 1 (6→8) not taken.
✓ Branch 2 (7→5) taken 5 times.
✓ Branch 3 (7→8) taken 1 times.
6 while (i < len && is_word_byte(buf[i])) {
152 5 i++;
153 }
154
155
3/4
✓ Branch 0 (10→11) taken 2 times.
✗ Branch 1 (10→12) not taken.
✓ Branch 2 (11→9) taken 1 times.
✓ Branch 3 (11→12) taken 1 times.
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 0 (4→5) taken 2 times.
✗ Branch 1 (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 0 (4→5) taken 2 times.
✗ Branch 1 (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 0 (5→6) taken 3 times.
✗ Branch 1 (5→7) not taken.
✓ Branch 2 (6→5) taken 1 times.
✓ Branch 3 (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 0 (8→9) taken 2 times.
✗ Branch 1 (8→10) not taken.
✗ Branch 2 (9→8) not taken.
✓ Branch 3 (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 0 (11→12) taken 12 times.
✗ Branch 1 (11→13) not taken.
✓ Branch 2 (12→11) taken 10 times.
✓ Branch 3 (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 do_history_prev(const History *hist, CommandLine *c, bool prefix_search)
223 {
224 if (!c->search_pos) {
225 free(c->search_text);
226 c->search_text = string_clone_cstring(&c->buf);
227 }
228
229 const char *search_text = prefix_search ? c->search_text : "";
230 if (history_search_forward(hist, &c->search_pos, search_text)) {
231 BUG_ON(!c->search_pos);
232 set_text(c, c->search_pos->text);
233 }
234
235 maybe_reset_completion(c);
236 return true;
237 }
238
239 static bool do_history_next(const History *hist, CommandLine *c, bool prefix_search)
240 {
241 if (!c->search_pos) {
242 goto out;
243 }
244
245 const char *search_text = prefix_search ? c->search_text : "";
246 if (history_search_backward(hist, &c->search_pos, search_text)) {
247 BUG_ON(!c->search_pos);
248 set_text(c, c->search_pos->text);
249 } else {
250 set_text(c, c->search_text);
251 c->search_pos = NULL;
252 }
253
254 out:
255 maybe_reset_completion(c);
256 return true;
257 }
258
259 static bool cmd_search_history_next(EditorState *e, const CommandArgs *a)
260 {
261 BUG_ON(a->nr_args);
262 bool prefix_search = !cmdargs_has_flag(a, 'S');
263 return do_history_next(&e->search_history, &e->cmdline, prefix_search);
264 }
265
266 static bool cmd_search_history_prev(EditorState *e, const CommandArgs *a)
267 {
268 BUG_ON(a->nr_args);
269 bool prefix_search = !cmdargs_has_flag(a, 'S');
270 return do_history_prev(&e->search_history, &e->cmdline, prefix_search);
271 }
272
273 static bool cmd_command_history_next(EditorState *e, const CommandArgs *a)
274 {
275 BUG_ON(a->nr_args);
276 bool prefix_search = !cmdargs_has_flag(a, 'S');
277 return do_history_next(&e->command_history, &e->cmdline, prefix_search);
278 }
279
280 static bool cmd_command_history_prev(EditorState *e, const CommandArgs *a)
281 {
282 BUG_ON(a->nr_args);
283 bool prefix_search = !cmdargs_has_flag(a, 'S');
284 return do_history_prev(&e->command_history, &e->cmdline, prefix_search);
285 }
286
287 2 static bool cmd_left(EditorState *e, const CommandArgs *a)
288 {
289 2 BUG_ON(a->nr_args);
290 2 CommandLine *c = &e->cmdline;
291
2/2
✓ Branch 0 (4→5) taken 1 times.
✓ Branch 1 (4→6) taken 1 times.
2 if (c->pos) {
292 1 u_prev_char(c->buf.buffer, &c->pos);
293 }
294 2 return cmdline_soft_reset(c);
295 }
296
297 static bool cmd_paste(EditorState *e, const CommandArgs *a)
298 {
299 const Clipboard *clip = &e->clipboard;
300 const size_t len = clip->len;
301 if (len == 0) {
302 return true;
303 }
304
305 CommandLine *c = &e->cmdline;
306 char newline_replacement = cmdargs_has_flag(a, 'n') ? ';' : ' ';
307 string_insert_buf(&c->buf, c->pos, clip->buf, len);
308 strn_replace_byte(c->buf.buffer + c->pos, len, '\n', newline_replacement);
309 c->pos += cmdargs_has_flag(a, 'm') ? len : 0;
310 return cmdline_soft_reset(c);
311 }
312
313 2 static bool cmd_right(EditorState *e, const CommandArgs *a)
314 {
315 2 BUG_ON(a->nr_args);
316 2 CommandLine *c = &e->cmdline;
317
1/2
✓ Branch 0 (4→5) taken 2 times.
✗ Branch 1 (4→6) not taken.
2 if (c->pos < c->buf.len) {
318 2 u_get_char(c->buf.buffer, c->buf.len, &c->pos);
319 }
320 2 return cmdline_soft_reset(c);
321 }
322
323 static bool cmd_toggle(EditorState *e, const CommandArgs *a)
324 {
325 const char *option_name = a->args[0];
326 bool global = cmdargs_has_flag(a, 'g');
327 size_t nr_values = a->nr_args - 1;
328 if (nr_values == 0) {
329 return toggle_option(e, option_name, global, false);
330 }
331
332 char **values = a->args + 1;
333 return toggle_option_values(e, option_name, global, false, values, nr_values);
334 }
335
336 2 static bool cmd_word_bwd(EditorState *e, const CommandArgs *a)
337 {
338 2 BUG_ON(a->nr_args);
339 2 CommandLine *c = &e->cmdline;
340
2/2
✓ Branch 0 (4→5) taken 1 times.
✓ Branch 1 (4→6) taken 1 times.
2 if (c->pos <= 1) {
341 1 c->pos = 0;
342 1 return cmdline_soft_reset(c);
343 }
344
345 1 const char *const buf = c->buf.buffer;
346 1 size_t i = c->pos - 1;
347
348
3/4
✓ Branch 0 (8→9) taken 2 times.
✗ Branch 1 (8→10) not taken.
✓ Branch 2 (9→7) taken 1 times.
✓ Branch 3 (9→10) taken 1 times.
2 while (i > 0 && !is_word_byte(buf[i])) {
349 1 i--;
350 }
351
352
3/4
✓ Branch 0 (12→13) taken 6 times.
✗ Branch 1 (12→14) not taken.
✓ Branch 2 (13→11) taken 5 times.
✓ Branch 3 (13→14) taken 1 times.
6 while (i > 0 && is_word_byte(buf[i])) {
353 5 i--;
354 }
355
356
1/2
✓ Branch 0 (14→15) taken 1 times.
✗ Branch 1 (14→16) not taken.
1 if (i > 0) {
357 1 i++;
358 }
359
360 1 c->pos = i;
361 1 return cmdline_soft_reset(c);
362 }
363
364 1 static bool cmd_word_fwd(EditorState *e, const CommandArgs *a)
365 {
366 1 BUG_ON(a->nr_args);
367 1 CommandLine *c = &e->cmdline;
368 1 const char *buf = c->buf.buffer;
369 1 const size_t len = c->buf.len;
370 1 size_t i = c->pos;
371
372
3/4
✓ Branch 0 (6→7) taken 6 times.
✗ Branch 1 (6→8) not taken.
✓ Branch 2 (7→5) taken 5 times.
✓ Branch 3 (7→8) taken 1 times.
6 while (i < len && is_word_byte(buf[i])) {
373 5 i++;
374 }
375
376
3/4
✓ Branch 0 (10→11) taken 1 times.
✓ Branch 1 (10→12) taken 1 times.
✓ Branch 2 (11→9) taken 1 times.
✗ Branch 3 (11→12) not taken.
2 while (i < len && !is_word_byte(buf[i])) {
377 1 i++;
378 }
379
380 1 c->pos = i;
381 1 return cmdline_soft_reset(c);
382 }
383
384 static bool cmd_complete_next(EditorState *e, const CommandArgs *a)
385 {
386 BUG_ON(a->nr_args);
387 complete_command_next(e);
388 return true;
389 }
390
391 static bool cmd_complete_prev(EditorState *e, const CommandArgs *a)
392 {
393 BUG_ON(a->nr_args);
394 complete_command_prev(e);
395 return true;
396 }
397
398 static bool cmd_direction(EditorState *e, const CommandArgs *a)
399 {
400 BUG_ON(a->nr_args);
401 toggle_search_direction(&e->search);
402 return true;
403 }
404
405 static bool cmd_command_mode_accept(EditorState *e, const CommandArgs *a)
406 {
407 CommandLine *c = &e->cmdline;
408 reset_completion(c);
409 pop_input_mode(e);
410
411 const char *str = string_borrow_cstring(&c->buf);
412 cmdline_clear(c);
413 if (!cmdargs_has_flag(a, 'H') && str[0] != ' ') {
414 // This is done before handle_command() because "command [text]"
415 // can modify the contents of the command-line
416 history_append(&e->command_history, str);
417 }
418
419 e->err.command_name = NULL;
420 return handle_normal_command(e, str, true);
421 }
422
423 static bool cmd_search_mode_accept(EditorState *e, const CommandArgs *a)
424 {
425 CommandLine *c = &e->cmdline;
426 bool add_to_history = !cmdargs_has_flag(a, 'H');
427 const char *pat = NULL;
428
429 if (c->buf.len > 0) {
430 String *s = &c->buf;
431 if (cmdargs_has_flag(a, 'e')) {
432 // Escape the regex; to match as plain text
433 char *original = string_clone_cstring(s);
434 size_t origlen = string_clear(s);
435 size_t bufsize = xmul(2, origlen) + 1;
436 char *buf = string_reserve_space(s, bufsize);
437 s->len = regexp_escapeb(buf, bufsize, original, origlen);
438 BUG_ON(s->len < origlen);
439 free(original);
440 }
441
442 pat = string_borrow_cstring(s);
443 search_set_regexp(&e->search, pat);
444 if (add_to_history) {
445 history_append(&e->search_history, pat);
446 }
447 }
448
449 if (e->macro.recording) {
450 macro_search_hook(&e->macro, pat, e->search.reverse, add_to_history);
451 }
452
453 // Unselect, unless selection mode is active
454 view_set_selection_type(e->view, e->view->select_mode);
455
456 e->err.command_name = NULL;
457 bool found = search_next(e->view, &e->search, e->options.case_sensitive_search);
458 cmdline_clear(c);
459 pop_input_mode(e);
460 return found;
461 }
462
463 // Note that some of the `Command::flags` entries here aren't actually
464 // used in the `cmd` handler and are only included to mirror commands
465 // of the same name in normal mode. This is done as a convenience for
466 // allowing key binding commands like e.g. `bind -cns C-M-c 'copy -bk'`
467 // to be used, instead of needing 2 different commands (with and without
468 // the `-k` flag for normal vs. command/search modes).
469
470 #define CMD(name, flags, min, max, func) \
471 {name, flags, 0, min, max, func}
472
473 static const Command common_cmds[] = {
474 CMD("bol", "st", 0, 0, cmd_bol), // Ignored flags: s, t
475 CMD("cancel", "", 0, 0, cmd_cancel),
476 CMD("clear", "Ii", 0, 0, cmd_clear), // Ignored flags: I, i
477 CMD("copy", "bikp", 0, 0, cmd_copy), // Ignored flag: k
478 CMD("delete", "", 0, 0, cmd_delete),
479 CMD("delete-eol", "n", 0, 0, cmd_delete_eol), // Ignored flag: n
480 CMD("delete-word", "s", 0, 0, cmd_delete_word), // Ignored flag: s
481 CMD("eol", "", 0, 0, cmd_eol),
482 CMD("erase", "", 0, 0, cmd_erase),
483 CMD("erase-bol", "", 0, 0, cmd_erase_bol),
484 CMD("erase-word", "s", 0, 0, cmd_erase_word), // Ignored flag: s
485 CMD("left", "", 0, 0, cmd_left),
486 CMD("paste", "acmn", 0, 0, cmd_paste), // Ignored flags: a, c
487 CMD("right", "", 0, 0, cmd_right),
488 CMD("toggle", "gv", 1, -1, cmd_toggle), // Ignored flag: v
489 CMD("word-bwd", "s", 0, 0, cmd_word_bwd), // Ignored flag: s
490 CMD("word-fwd", "s", 0, 0, cmd_word_fwd), // Ignored flag: s
491 };
492
493 static const Command search_cmds[] = {
494 CMD("accept", "eH", 0, 0, cmd_search_mode_accept),
495 CMD("direction", "", 0, 0, cmd_direction),
496 CMD("history-next", "S", 0, 0, cmd_search_history_next),
497 CMD("history-prev", "S", 0, 0, cmd_search_history_prev),
498 };
499
500 static const Command command_cmds[] = {
501 CMD("accept", "H", 0, 0, cmd_command_mode_accept),
502 CMD("complete-next", "", 0, 0, cmd_complete_next),
503 CMD("complete-prev", "", 0, 0, cmd_complete_prev),
504 CMD("history-next", "S", 0, 0, cmd_command_history_next),
505 CMD("history-prev", "S", 0, 0, cmd_command_history_prev),
506 };
507
508 373 static const Command *find_cmd_mode_command(const char *name)
509 {
510 373 const Command *cmd = BSEARCH(name, common_cmds, command_cmp);
511
2/2
✓ Branch 0 (3→4) taken 64 times.
✓ Branch 1 (3→5) taken 309 times.
373 return cmd ? cmd : BSEARCH(name, command_cmds, command_cmp);
512 }
513
514 360 static const Command *find_search_mode_command(const char *name)
515 {
516 360 const Command *cmd = BSEARCH(name, common_cmds, command_cmp);
517
2/2
✓ Branch 0 (3→4) taken 63 times.
✓ Branch 1 (3→5) taken 297 times.
360 return cmd ? cmd : BSEARCH(name, search_cmds, command_cmp);
518 }
519
520 const CommandSet cmd_mode_commands = {
521 .lookup = find_cmd_mode_command
522 };
523
524 const CommandSet search_mode_commands = {
525 .lookup = find_search_mode_command
526 };
527