dte test coverage


Directory: ./
File: src/cmdline.c
Date: 2025-07-03 15:44:24
Exec Total Coverage
Lines: 140 263 53.2%
Functions: 21 36 58.3%
Branches: 41 100 41.0%

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