dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 75.5% 1173 / 1 / 1554
Functions: 90.7% 98 / 0 / 108
Branches: 53.9% 503 / 120 / 1053

src/commands.c
Line Branch Exec Source
1 #include <errno.h>
2 #include <fcntl.h>
3 #include <glob.h>
4 #include <signal.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <sys/stat.h>
8 #include <unistd.h>
9 #include "commands.h"
10 #include "bind.h"
11 #include "bookmark.h"
12 #include "buffer.h"
13 #include "case.h"
14 #include "change.h"
15 #include "cmdline.h"
16 #include "command/alias.h"
17 #include "command/args.h"
18 #include "command/error.h"
19 #include "command/macro.h"
20 #include "compiler.h"
21 #include "config.h"
22 #include "convert.h"
23 #include "copy.h"
24 #include "delete.h"
25 #include "editor.h"
26 #include "encoding.h"
27 #include "exec.h"
28 #include "file-option.h"
29 #include "filetype.h"
30 #include "frame.h"
31 #include "history.h"
32 #include "indent.h"
33 #include "insert.h"
34 #include "join.h"
35 #include "load-save.h"
36 #include "lock.h"
37 #include "mode.h"
38 #include "move.h"
39 #include "msg.h"
40 #include "regexp.h"
41 #include "replace.h"
42 #include "search.h"
43 #include "selection.h"
44 #include "show.h"
45 #include "spawn.h"
46 #include "syntax/color.h"
47 #include "tag.h"
48 #include "terminal/cursor.h"
49 #include "terminal/mode.h"
50 #include "terminal/osc52.h"
51 #include "terminal/style.h"
52 #include "terminal/terminal.h"
53 #include "ui.h"
54 #include "util/arith.h"
55 #include "util/array.h"
56 #include "util/ascii.h"
57 #include "util/bsearch.h"
58 #include "util/debug.h"
59 #include "util/errorcode.h"
60 #include "util/intern.h"
61 #include "util/log.h"
62 #include "util/numtostr.h"
63 #include "util/path.h"
64 #include "util/str-array.h"
65 #include "util/str-util.h"
66 #include "util/strtonum.h"
67 #include "util/time-util.h"
68 #include "util/xmalloc.h"
69 #include "util/xsnprintf.h"
70 #include "util/xstring.h"
71 #include "view.h"
72 #include "window.h"
73 #include "wrap.h"
74
75 8297 static bool has_flag(const CommandArgs *a, unsigned char flag)
76 {
77 8297 return cmdargs_has_flag(a, flag);
78 }
79
80 226 static bool cmd_alias(EditorState *e, const CommandArgs *a)
81 {
82 226 const char *const name = a->args[0];
83 226 const char *const cmd = a->args[1];
84 226 ErrorBuffer *ebuf = &e->err;
85
86
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 225 times.
226 if (unlikely(name[0] == '\0')) {
87 1 return error_msg(ebuf, "Empty alias name not allowed");
88 }
89
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 11 taken 224 times.
225 if (unlikely(name[0] == '-')) {
90 // Disallowing this simplifies auto-completion for "alias "
91 1 return error_msg(ebuf, "Alias name cannot begin with '-'");
92 }
93
94
2/2
✓ Branch 11 → 6 taken 1692 times.
✓ Branch 11 → 12 taken 223 times.
1915 for (size_t i = 0; name[i]; i++) {
95 1692 unsigned char c = name[i];
96
5/6
✓ Branch 6 → 7 taken 127 times.
✓ Branch 6 → 10 taken 1565 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 10 taken 126 times.
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
1692 if (unlikely(!(is_word_byte(c) || c == '-' || c == '?' || c == '!'))) {
97 1 return error_msg(ebuf, "Invalid byte in alias name: %c (0x%02hhX)", c, c);
98 }
99 }
100
101
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 222 times.
223 if (unlikely(find_normal_command(name))) {
102 1 return error_msg(ebuf, "Can't replace existing command %s with an alias", name);
103 }
104
105
2/2
✓ Branch 15 → 16 taken 220 times.
✓ Branch 15 → 17 taken 2 times.
222 if (likely(cmd)) {
106 220 add_alias(&e->aliases, name, cmd);
107 } else {
108 2 remove_alias(&e->aliases, name);
109 }
110
111 return true;
112 }
113
114 1641 static bool cmd_bind(EditorState *e, const CommandArgs *a)
115 {
116 1641 const char *keystr = a->args[a->nr_flag_args];
117 1641 const char *cmd = a->args[a->nr_flag_args + 1];
118 1641 KeyCode key = keycode_from_str(keystr);
119
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 9 taken 1640 times.
1641 if (unlikely(key == KEY_NONE)) {
120
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 8 taken 1 time.
1 if (has_flag(a, 'q')) {
121 ✗ LOG_INFO("bind -q: dropped invalid key string: %s", keystr);
122 ✗ return false;
123 }
124 1 return error_msg(&e->err, "invalid key string: %s", keystr);
125 }
126
127 1640 ModeHandler *modes[10];
128 1640 size_t nmodes = 0;
129 1640 static_assert(ARRAYLEN(modes) <= ARRAYLEN(a->flags));
130
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1640 times.
1640 if (has_flag(a, 'n')) {
131 ✗ modes[nmodes++] = e->normal_mode;
132 }
133
2/2
✓ Branch 13 → 14 taken 360 times.
✓ Branch 13 → 15 taken 1280 times.
1640 if (has_flag(a, 'c')) {
134 360 modes[nmodes++] = e->command_mode;
135 }
136
2/2
✓ Branch 16 → 17 taken 369 times.
✓ Branch 16 → 18 taken 1271 times.
1640 if (has_flag(a, 's')) {
137 369 modes[nmodes++] = e->search_mode;
138 }
139
140
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 24 taken 1640 times.
1640 if (unlikely(nmodes + a->nr_flag_args > ARRAYLEN(modes))) {
141 // This is already prevented by the ARGERR_TOO_MANY_OPTIONS check
142 // in do_parse_args(), but since that's only incidental, it's still
143 // checked here
144 ✗ return error_msg(&e->err, "too many modes specified");
145 }
146
147 // Gather pointers to modes specified via `-T modename`. This is done
148 // separately from adding/removing bindings, partly so that either all
149 // modes are processed or none are (if the error below is triggered).
150
2/2
✓ Branch 24 → 20 taken 1 time.
✓ Branch 24 → 25 taken 1639 times.
1640 for (size_t i = 0, n = a->nr_flag_args; i < n; i++) {
151 1 const char *name = a->args[i];
152 1 ModeHandler *mode = get_mode_handler(&e->modes, name);
153
1/2
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 23 not taken.
1 if (unlikely(!mode)) {
154 1 return error_msg(&e->err, "can't bind key in unknown mode '%s'", name);
155 }
156 ✗ modes[nmodes++] = mode;
157 }
158
159
2/2
✓ Branch 25 → 26 taken 1252 times.
✓ Branch 25 → 27 taken 387 times.
1639 if (nmodes == 0) {
160 // No [-cnsT] flags used; default to normal mode
161 1252 modes[nmodes++] = e->normal_mode;
162 }
163
164
1/2
✗ Branch 27 → 30 not taken.
✓ Branch 27 → 31 taken 1639 times.
1639 if (!cmd) {
165 ✗ for (size_t i = 0; i < nmodes; i++) {
166 ✗ remove_binding(&modes[i]->key_bindings, key);
167 }
168 return true;
169 }
170
171 1639 CommandRunner runner = cmdrunner(e, NULL);
172
2/2
✓ Branch 35 → 32 taken 1981 times.
✓ Branch 35 → 36 taken 1639 times.
3620 for (size_t i = 0; i < nmodes; i++) {
173 1981 runner.cmds = modes[i]->cmds;
174 1981 CachedCommand *cc = cached_command_new(&runner, cmd);
175 1981 add_binding(&modes[i]->key_bindings, key, cc);
176 }
177
178 return true;
179 }
180
181 4 static bool cmd_bof(EditorState *e, const CommandArgs *a)
182 {
183 4 handle_selection_flags(e->view, a);
184 4 move_bof(e->view);
185 4 return true;
186 }
187
188 7 static bool cmd_bol(EditorState *e, const CommandArgs *a)
189 {
190 7 SmartBolType type = BOL_SIMPLE;
191
1/4
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 5 not taken.
✗ Branch 3 → 6 not taken.
✓ Branch 3 → 7 taken 7 times.
7 switch (cmdargs_pick_winning_flag(a, "rst")) {
192 ✗ case 'r': type = BOL_TOGGLE_LR; break;
193 ✗ case 's': type = BOL_INDENT; break;
194 ✗ case 't': type = BOL_TOGGLE_RL; break;
195 }
196
197 7 handle_selection_flags(e->view, a);
198 7 move_bol(e->view, type);
199 7 return true;
200 }
201
202 1 static bool cmd_bolsf(EditorState *e, const CommandArgs *a)
203 {
204 1 BUG_ON(a->nr_args);
205 1 View *view = e->view;
206 1 handle_selection_flags(view, a);
207
208
1/2
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
1 if (!block_iter_bol(&view->cursor)) {
209 1 long margin = window_get_scroll_margin(e->window);
210 1 long top = view->vy + margin;
211
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
1 if (view->cy > top) {
212 1 move_up(view, view->cy - top);
213 } else {
214 ✗ block_iter_bof(&view->cursor);
215 }
216 }
217
218 1 view_reset_preferred_x(view);
219 1 return true;
220 }
221
222 1 static bool cmd_bookmark(EditorState *e, const CommandArgs *a)
223 {
224 1 PointerArray *bookmarks = &e->bookmarks;
225 1 ErrorBuffer *ebuf = &e->err;
226 1 bool verbose = has_flag(a, 'v');
227
228
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 14 taken 1 time.
1 if (has_flag(a, 'r')) {
229 ✗ size_t popped = bookmark_pop(bookmarks, e->window, &e->buffers);
230 ✗ if (!verbose || popped == 0) {
231 ✗ return verbose ? info_msg(ebuf, "no bookmarks to pop") : true;
232 }
233 ✗ return info_msg (
234 ebuf, "popped %zu bookmark%s (%zu remaining)",
235 popped, (popped == 1) ? "" : "s", bookmarks->count
236 );
237 }
238
239 1 FileLocation *loc = get_current_file_location(e->view);
240 1 size_t idx = bookmark_push(bookmarks, loc);
241
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 22 taken 1 time.
1 if (!verbose) {
242 return true;
243 }
244
245 ✗ return info_msg (
246 ebuf, "pushed bookmark #%zu at position %lu,%lu (%s%s)",
247 idx, loc->line, loc->column,
248 ✗ loc->filename ? "" : "unsaved buffer #",
249 ✗ loc->filename ? loc->filename : ulong_to_str(loc->buffer_id)
250 );
251 }
252
253 2 static bool cmd_case(EditorState *e, const CommandArgs *a)
254 {
255 2 char mode = cmdargs_pick_winning_flag(a, "lu");
256
1/2
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 5 not taken.
4 change_case(e->view, mode ? mode : 't');
257 2 return true;
258 }
259
260 4 static void mark_tabbar_changed(Window *window, void* UNUSED_ARG(data))
261 {
262 4 window->update_tabbar = true;
263 4 }
264
265 6 static bool cmd_cd(EditorState *e, const CommandArgs *a)
266 {
267 6 const char *dir = a->args[0];
268 6 ErrorBuffer *ebuf = &e->err;
269
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 5 times.
6 if (unlikely(dir[0] == '\0')) {
270 1 return error_msg(ebuf, "directory argument cannot be empty");
271 }
272
273
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 8 taken 3 times.
5 if (streq(dir, "-")) {
274 2 dir = xgetenv("OLDPWD");
275
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 2 times.
2 if (!dir) {
276 ✗ return error_msg(ebuf, "OLDPWD not set");
277 }
278 }
279
280 5 char buf[8192];
281 5 const char *cwd = getcwd(buf, sizeof(buf));
282
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 4 times.
5 if (chdir(dir) != 0) {
283 1 return error_msg_errno(ebuf, "changing directory failed");
284 }
285
286
1/2
✓ Branch 12 → 13 taken 4 times.
✗ Branch 12 → 17 not taken.
4 if (likely(cwd)) {
287 4 int r = setenv("OLDPWD", cwd, 1);
288
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 4 times.
4 if (unlikely(r != 0)) {
289 ✗ LOG_WARNING("failed to set OLDPWD: %s", strerror(errno));
290 }
291 }
292
293 4 cwd = getcwd(buf, sizeof(buf));
294
1/2
✓ Branch 18 → 19 taken 4 times.
✗ Branch 18 → 23 not taken.
4 if (likely(cwd)) {
295 4 int r = setenv("PWD", cwd, 1);
296
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 23 taken 4 times.
4 if (unlikely(r != 0)) {
297 ✗ LOG_WARNING("failed to set PWD: %s", strerror(errno));
298 }
299 }
300
301
2/2
✓ Branch 26 → 24 taken 136 times.
✓ Branch 26 → 27 taken 4 times.
140 for (size_t i = 0, n = e->buffers.count; i < n; i++) {
302 136 Buffer *buffer = e->buffers.ptrs[i];
303 136 buffer_update_short_filename_cwd(buffer, e->home_dir, cwd);
304 }
305
306 4 frame_for_each_window(e->root_frame, mark_tabbar_changed, NULL);
307 4 e->screen_update |= UPDATE_TERM_TITLE;
308
309 4 bool verbose = has_flag(a, 'v');
310
1/6
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 35 taken 4 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
4 return !verbose || info_msg(ebuf, "changed directory to: %s", cwd ? cwd : dir);
311 }
312
313 ✗ static bool cmd_center_view(EditorState *e, const CommandArgs *a)
314 {
315 ✗ BUG_ON(a->nr_args);
316 ✗ e->view->force_center = true;
317 ✗ return true;
318 }
319
320 4 static bool cmd_clear(EditorState *e, const CommandArgs *a)
321 {
322 4 char iflag = cmdargs_pick_winning_flag(a, "iI");
323
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 4 times.
4 bool auto_indent = iflag ? (iflag == 'i') : e->buffer->options.auto_indent;
324 4 clear_lines(e->view, auto_indent);
325 4 return true;
326 }
327
328 26 static bool cmd_close(EditorState *e, const CommandArgs *a)
329 {
330 26 bool force = has_flag(a, 'f');
331
4/4
✓ Branch 3 → 4 taken 19 times.
✓ Branch 3 → 13 taken 7 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 13 taken 17 times.
26 if (!force && !view_can_close(e->view)) {
332 2 bool prompt = has_flag(a, 'p');
333
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 1 time.
2 if (!prompt) {
334 1 return error_msg (
335 &e->err,
336 "The buffer is modified; "
337 "save or run 'close -f' to close without saving"
338 );
339 }
340
341
1/2
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 11 not taken.
1 if (unlikely(e->flags & EFLAG_HEADLESS)) {
342 1 return error_msg(&e->err, "-p flag unavailable in headless mode");
343 }
344
345 ✗ static const char str[] = "Close without saving changes? [y/N]";
346 ✗ if (dialog_prompt(e, str, "ny") != 'y') {
347 return false;
348 }
349 }
350
351 24 bool allow_quit = has_flag(a, 'q');
352
1/6
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 18 taken 24 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 18 not taken.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
24 if (allow_quit && e->buffers.count == 1 && e->root_frame->frames.count <= 1) {
353 ✗ e->status = EDITOR_EXIT_OK;
354 ✗ return true;
355 }
356
357 24 bool allow_wclose = has_flag(a, 'w');
358
1/4
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 23 taken 24 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 23 not taken.
24 if (allow_wclose && e->window->views.count <= 1) {
359 ✗ window_close(e->window);
360 ✗ return true;
361 }
362
363 24 window_close_current_view(e->window);
364 24 set_view(e->window->view);
365 24 return true;
366 }
367
368 ✗ static bool cmd_command(EditorState *e, const CommandArgs *a)
369 {
370 ✗ const char *text = a->args[0];
371 ✗ push_input_mode(e, e->command_mode);
372 ✗ if (text) {
373 ✗ cmdline_set_text(&e->cmdline, text);
374 }
375 ✗ return true;
376 }
377
378 1 static bool cmd_compile(EditorState *e, const CommandArgs *a)
379 {
380 1 Compiler *compiler = find_compiler(&e->compilers, a->args[0]);
381
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
1 if (unlikely(!compiler)) {
382 1 return error_msg(&e->err, "No such error parser %s", a->args[0]);
383 }
384
385 ✗ bool quiet = has_flag(a, 's');
386 ✗ if ((e->flags & EFLAG_HEADLESS) && !quiet) {
387 ✗ LOG_INFO("automatically added -s flag to compile command (headless mode)");
388 ✗ quiet = true;
389 }
390
391 ✗ SpawnContext ctx = {
392 ✗ .argv = (const char **)a->args + 1,
393 ✗ .ebuf = &e->err,
394 .quiet = quiet,
395 };
396
397 ✗ char abc = cmdargs_pick_winning_flag(a, "ABC");
398 ✗ size_t idx = abc ? abc - 'A' : e->options.msg_compile;
399 ✗ MessageList *messages = &e->messages[idx];
400 ✗ clear_messages(messages);
401
402 ✗ yield_terminal(e, quiet);
403 ✗ bool prompt = has_flag(a, 'p');
404 ✗ bool read_stdout = has_flag(a, '1');
405 ✗ bool spawned = spawn_compiler(&ctx, compiler, messages, read_stdout);
406 ✗ resume_terminal(e, quiet, spawned && prompt);
407
408 ✗ activate_current_message_save(messages, &e->bookmarks, e->view, &e->err);
409 ✗ return spawned;
410 }
411
412 4 static bool cmd_copy(EditorState *e, const CommandArgs *a)
413 {
414 4 static const FlagMapping map[] = {
415 {'b', TCOPY_CLIPBOARD},
416 {'p', TCOPY_PRIMARY},
417 };
418
419 4 Terminal *term = &e->terminal;
420 4 TermCopyFlags flags = cmdargs_convert_flags(a, map, ARRAYLEN(map));
421
2/4
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 7 not taken.
✓ Branch 5 → 9 taken 4 times.
4 bool internal = has_flag(a, 'i') || flags == 0;
422 ✗ bool osc52 = flags && (term->features & TFLAG_OSC52_COPY);
423
424
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 18 taken 4 times.
4 if (a->nr_args) {
425 ✗ StringView text = strview(a->args[0]);
426 ✗ if (internal) {
427 ✗ char *copy = strview_clone_cstring(text);
428 ✗ record_copy(&e->clipboard, copy, text.length, false);
429 }
430 ✗ if (osc52) {
431 ✗ if (!term_osc52_copy(&term->obuf, text, flags)) {
432 ✗ error_msg_errno(&e->err, "OSC 52 copy failed");
433 }
434 }
435 ✗ return true;
436 }
437
438 4 const View *view = e->view;
439 4 BlockIter bi;
440 4 size_t size;
441 4 bool line_copy;
442
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 4 times.
4 if (view->selection) {
443 ✗ SelectionInfo info = init_selection(view);
444 ✗ size = info.eo - info.so;
445 ✗ bi = info.si;
446 ✗ line_copy = (view->selection == SELECT_LINES);
447 } else {
448 4 bi = view->cursor;
449 4 block_iter_bol(&bi);
450 4 BlockIter tmp = bi;
451 4 size = block_iter_eat_line(&tmp);
452 4 line_copy = true;
453 }
454
455
1/2
✓ Branch 24 → 25 taken 4 times.
✗ Branch 24 → 40 not taken.
4 if (unlikely(size == 0)) {
456 return true;
457 }
458
459 4 String text = block_iter_get_bytes(bi, size);
460
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 30 taken 4 times.
4 if (osc52) {
461 ✗ if (!term_osc52_copy(&term->obuf, strview_from_string(&text), flags)) {
462 ✗ error_msg_errno(&e->err, "OSC 52 copy failed");
463 }
464 }
465
466
1/2
✓ Branch 30 → 31 taken 4 times.
✗ Branch 30 → 33 not taken.
4 if (internal) {
467 4 char *cstr = string_steal_cstring(&text);
468 4 record_copy(&e->clipboard, cstr, size, line_copy); // Takes ownership
469 } else {
470 ✗ string_free(&text);
471 }
472
473
2/4
✓ Branch 35 → 36 taken 4 times.
✗ Branch 35 → 39 not taken.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 4 times.
4 return has_flag(a, 'k') || unselect(e->view);
474 }
475
476 3 static bool cmd_cursor(EditorState *e, const CommandArgs *a)
477 {
478
1/2
✗ Branch 2 → 4 not taken.
✓ Branch 2 → 6 taken 3 times.
3 if (unlikely(a->nr_args == 0)) {
479 // Reset all cursor styles
480 ✗ for (CursorInputMode m = 0; m < ARRAYLEN(e->cursor_styles); m++) {
481 ✗ e->cursor_styles[m] = get_default_cursor_style(m);
482 }
483 ✗ e->screen_update |= UPDATE_CURSOR_STYLE;
484 ✗ return true;
485 }
486
487 3 CursorInputMode mode = cursor_mode_from_str(a->args[0]);
488
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 2 times.
3 if (unlikely(mode >= NR_CURSOR_MODES)) {
489 1 return error_msg(&e->err, "invalid mode argument: %s", a->args[0]);
490 }
491
492 2 TermCursorStyle style = get_default_cursor_style(mode);
493
1/2
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 13 not taken.
2 if (a->nr_args >= 2) {
494 2 style.type = cursor_type_from_str(a->args[1]);
495
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 1 time.
2 if (unlikely(style.type == CURSOR_INVALID)) {
496 1 return error_msg(&e->err, "invalid cursor type: %s", a->args[1]);
497 }
498 }
499
500
1/2
✓ Branch 13 → 14 taken 1 time.
✗ Branch 13 → 17 not taken.
1 if (a->nr_args >= 3) {
501 1 style.color = cursor_color_from_str(a->args[2]);
502
1/2
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 17 not taken.
1 if (unlikely(style.color == COLOR_INVALID)) {
503 1 return error_msg(&e->err, "invalid cursor color: %s", a->args[2]);
504 }
505 }
506
507 ✗ e->cursor_styles[mode] = style;
508 ✗ e->screen_update |= UPDATE_CURSOR_STYLE;
509 ✗ return true;
510 }
511
512 2 static bool cmd_cut(EditorState *e, const CommandArgs *a)
513 {
514 2 BUG_ON(a->nr_args);
515 2 View *view = e->view;
516 2 long preferred_x = view_get_preferred_x(view);
517 2 size_t size;
518 2 bool line_copy;
519
520
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 1 time.
2 if (view->selection) {
521 1 line_copy = (view->selection == SELECT_LINES);
522 1 size = prepare_selection(view);
523 1 unselect(view);
524 } else {
525 1 line_copy = true;
526 1 block_iter_bol(&view->cursor);
527 1 BlockIter tmp = view->cursor;
528 1 size = block_iter_eat_line(&tmp);
529 }
530
531
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 18 not taken.
2 if (size == 0) {
532 return true;
533 }
534
535 2 String buf = block_iter_get_bytes(view->cursor, size);
536 2 char *cstr = string_steal_cstring(&buf);
537 2 record_copy(&e->clipboard, cstr, size, line_copy); // Takes ownership
538 2 buffer_delete_bytes(view, size);
539
540
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 1 time.
2 if (line_copy) {
541 1 move_to_preferred_x(view, preferred_x);
542 }
543
544 return true;
545 }
546
547 9 static bool cmd_def_mode(EditorState *e, const CommandArgs *a)
548 {
549 9 ErrorBuffer *ebuf = &e->err;
550 9 const char *name = a->args[0];
551
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 7 times.
9 if (name[0] == '\0' || name[0] == '-') {
552 2 return error_msg(ebuf, "mode name can't be empty or start with '-'");
553 }
554
555 7 HashMap *modes = &e->modes;
556
2/2
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 3 times.
7 if (hashmap_get(modes, name)) {
557 4 return error_msg(ebuf, "mode '%s' already exists", name);
558 }
559
560 3 PointerArray ftmodes = ptr_array_new(0);
561
2/2
✓ Branch 18 → 9 taken 2 times.
✓ Branch 18 → 19 taken 1 time.
3 for (size_t i = 1, n = a->nr_args; i < n; i++) {
562 2 const char *ftname = a->args[i];
563 2 ModeHandler *mode = get_mode_handler(modes, ftname);
564
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 13 taken 1 time.
2 if (unlikely(!mode)) {
565 1 ptr_array_free_array(&ftmodes);
566 1 return error_msg(ebuf, "unknown fallthrough mode '%s'", ftname);
567 }
568
1/2
✓ Branch 13 → 14 taken 1 time.
✗ Branch 13 → 16 not taken.
1 if (unlikely(mode->cmds != &normal_commands)) {
569 // TODO: Support "command" and "search" as fallback modes?
570 // If implemented, all involved modes need to use the same
571 // `CommandSet`.
572 1 ptr_array_free_array(&ftmodes);
573 1 return error_msg(ebuf, "unable to use '%s' as fall-through mode", ftname);
574 }
575 ✗ ptr_array_append(&ftmodes, mode);
576 }
577
578 1 static const FlagMapping map[] = {
579 {'u', MHF_NO_TEXT_INSERTION},
580 {'U', MHF_NO_TEXT_INSERTION | MHF_NO_TEXT_INSERTION_RECURSIVE},
581 };
582
583 1 ModeHandler *mode = new_mode(modes, name, &normal_commands, 0);
584 1 mode->flags = cmdargs_convert_flags(a, map, ARRAYLEN(map));
585 1 mode->fallthrough_modes = ftmodes;
586 1 return true;
587 }
588
589 9 static bool cmd_delete(EditorState *e, const CommandArgs *a)
590 {
591 9 BUG_ON(a->nr_args);
592 9 delete_ch(e->view);
593 9 return true;
594 }
595
596 2 static bool cmd_delete_eol(EditorState *e, const CommandArgs *a)
597 {
598 2 View *view = e->view;
599
1/2
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 14 not taken.
2 if (view->selection) {
600 // TODO: return false?
601 return true;
602 }
603
604 2 bool delete_newline_if_at_eol = has_flag(a, 'n');
605 2 BlockIter bi = view->cursor;
606
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 11 taken 2 times.
2 if (delete_newline_if_at_eol) {
607 ✗ CodePoint ch;
608 ✗ if (block_iter_get_char(&view->cursor, &ch) == 1 && ch == '\n') {
609 ✗ delete_ch(view);
610 ✗ return true;
611 }
612 }
613
614 2 buffer_delete_bytes(view, block_iter_eol(&bi));
615 2 return true;
616 }
617
618 ✗ static bool cmd_delete_line(EditorState *e, const CommandArgs *a)
619 {
620 ✗ BUG_ON(a->nr_args);
621 ✗ View *view = e->view;
622 ✗ long x = view_get_preferred_x(view);
623 ✗ bool whole_lines = true;
624 ✗ size_t del_count;
625
626 ✗ if (view->selection) {
627 ✗ whole_lines = !has_flag(a, 'S');
628 ✗ view->selection = whole_lines ? SELECT_LINES : view->selection;
629 ✗ del_count = prepare_selection(view);
630 ✗ unselect(view);
631 } else {
632 ✗ block_iter_bol(&view->cursor);
633 ✗ BlockIter tmp = view->cursor;
634 ✗ del_count = block_iter_eat_line(&tmp);
635 }
636
637 ✗ buffer_delete_bytes(view, del_count);
638 ✗ if (whole_lines) {
639 ✗ move_to_preferred_x(view, x);
640 }
641
642 ✗ return true;
643 }
644
645 1 static bool cmd_delete_word(EditorState *e, const CommandArgs *a)
646 {
647 1 bool skip_non_word = has_flag(a, 's');
648 1 BlockIter bi = e->view->cursor;
649 1 buffer_delete_bytes(e->view, word_fwd(&bi, skip_non_word));
650 1 return true;
651 }
652
653 14 static bool cmd_down(EditorState *e, const CommandArgs *a)
654 {
655 14 handle_selection_flags(e->view, a);
656 14 move_down(e->view, 1);
657 14 return true;
658 }
659
660 6 static bool cmd_eof(EditorState *e, const CommandArgs *a)
661 {
662 6 handle_selection_flags(e->view, a);
663 6 move_eof(e->view);
664 6 return true;
665 }
666
667 6 static bool cmd_eol(EditorState *e, const CommandArgs *a)
668 {
669 6 handle_selection_flags(e->view, a);
670 6 move_eol(e->view);
671 6 return true;
672 }
673
674 1 static bool cmd_eolsf(EditorState *e, const CommandArgs *a)
675 {
676 1 BUG_ON(a->nr_args);
677 1 View *view = e->view;
678 1 handle_selection_flags(view, a);
679
680
1/2
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
1 if (!block_iter_eol(&view->cursor)) {
681 1 Window *window = e->window;
682 1 long margin = window_get_scroll_margin(window);
683 1 long bottom = view->vy + window->edit_h - 1 - margin;
684
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
1 if (view->cy < bottom) {
685 ✗ move_down(view, bottom - view->cy);
686 } else {
687 1 block_iter_eof(&view->cursor);
688 }
689 }
690
691 1 view_reset_preferred_x(view);
692 1 return true;
693 }
694
695 6 static bool cmd_erase(EditorState *e, const CommandArgs *a)
696 {
697 6 BUG_ON(a->nr_args);
698 6 erase(e->view);
699 6 return true;
700 }
701
702 1 static bool cmd_erase_bol(EditorState *e, const CommandArgs *a)
703 {
704 1 BUG_ON(a->nr_args);
705 1 buffer_erase_bytes(e->view, block_iter_bol(&e->view->cursor));
706 1 return true;
707 }
708
709 2 static bool cmd_erase_word(EditorState *e, const CommandArgs *a)
710 {
711 2 View *view = e->view;
712 2 bool skip_non_word = has_flag(a, 's');
713 2 buffer_erase_bytes(view, word_bwd(&view->cursor, skip_non_word));
714 2 return true;
715 }
716
717 263 static bool cmd_errorfmt(EditorState *e, const CommandArgs *a)
718 {
719 263 const size_t nr_args = a->nr_args;
720 263 BUG_ON(nr_args == 0 || nr_args > 2 + ERRORFMT_CAPTURE_MAX);
721 263 const char *name = a->args[0];
722
723
2/2
✓ Branch 4 → 5 taken 18 times.
✓ Branch 4 → 7 taken 245 times.
263 if (nr_args == 1) {
724 18 remove_compiler(&e->compilers, name);
725 18 return true;
726 }
727
728
2/2
✓ Branch 8 → 9 taken 9 times.
✓ Branch 8 → 12 taken 236 times.
245 if (has_flag(a, 'c')) {
729 9 Compiler *c = find_compiler(&e->compilers, name);
730
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 9 times.
9 if (c) {
731 ✗ ptr_array_clear(&c->error_formats, FREE_FUNC(free_error_format));
732 }
733 }
734
735 245 static_assert(NR_ERRFMT_INDICES == 4);
736 245 size_t max_idx = 0;
737 245 int8_t indices[NR_ERRFMT_INDICES] = {
738 [ERRFMT_FILE] = -1,
739 [ERRFMT_LINE] = -1,
740 [ERRFMT_COLUMN] = -1,
741 [ERRFMT_MESSAGE] = 0,
742 };
743
744
2/2
✓ Branch 20 → 13 taken 291 times.
✓ Branch 20 → 21 taken 244 times.
535 for (size_t i = 0, n = nr_args - 2; i < n; i++) {
745 291 char *cap_name = a->args[i + 2];
746
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 291 times.
291 if (streq(cap_name, "_")) {
747 ✗ continue;
748 }
749 291 ssize_t cap_idx = errorfmt_capture_name_to_index(cap_name);
750
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 290 times.
291 if (unlikely(cap_idx < 0)) {
751 1 return error_msg(&e->err, "unknown substring name %s", cap_name);
752 }
753 290 max_idx = i + 1;
754 290 indices[cap_idx] = max_idx;
755 }
756
757 244 const char *pattern = a->args[1];
758 244 regex_t re;
759
1/2
✓ Branch 22 → 23 taken 244 times.
✗ Branch 22 → 29 not taken.
244 if (unlikely(!regexp_compile(&e->err, &re, pattern, 0))) {
760 return false;
761 }
762
763
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 26 taken 243 times.
244 if (unlikely(max_idx > re.re_nsub)) {
764 1 regfree(&re);
765 1 return error_msg (
766 &e->err, "expected %zu subexpressions in regex, but found %zu: '%s'",
767 max_idx, re.re_nsub, pattern
768 );
769 }
770
771 243 bool ignore = has_flag(a, 'i');
772 243 add_error_fmt(&e->compilers, name, pattern, &re, indices, ignore);
773 243 return true;
774 }
775
776 18 static bool cmd_exec(EditorState *e, const CommandArgs *a)
777 {
778 18 ExecAction actions[3] = {EXEC_TTY, EXEC_TTY, EXEC_TTY};
779 18 ExecFlags exec_flags = 0;
780 18 bool lflag = false;
781 18 bool move_after_insert = false;
782
783
2/2
✓ Branch 18 → 3 taken 36 times.
✓ Branch 18 → 19 taken 15 times.
51 for (size_t i = 0, n = a->nr_flags, argidx = 0, fd; i < n; i++) {
784
4/10
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 6 times.
✗ Branch 3 → 6 not taken.
✓ Branch 3 → 7 taken 18 times.
✗ Branch 3 → 8 not taken.
✗ Branch 3 → 9 not taken.
✗ Branch 3 → 10 not taken.
✗ Branch 3 → 11 not taken.
✗ Branch 3 → 12 not taken.
✓ Branch 3 → 13 taken 8 times.
36 switch (a->flags[i]) {
785 case 'e': fd = STDERR_FILENO; break;
786 4 case 'i': fd = STDIN_FILENO; break;
787 6 case 'o': fd = STDOUT_FILENO; break;
788 ✗ case 'p': exec_flags |= EXECFLAG_PROMPT; continue;
789 18 case 's': exec_flags |= EXECFLAG_QUIET; continue;
790 ✗ case 't': exec_flags &= ~EXECFLAG_QUIET; continue;
791 ✗ case 'l': lflag = true; continue;
792 ✗ case 'm': move_after_insert = true; continue;
793 ✗ case 'n': exec_flags |= EXECFLAG_STRIP_NL; continue;
794 ✗ default: BUG("unexpected flag"); return false;
795 }
796 18 const char *action_name = a->args[argidx++];
797 18 ExecAction action = lookup_exec_action(action_name, fd);
798
2/2
✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 16 taken 15 times.
18 if (unlikely(action == EXEC_INVALID)) {
799 3 return error_msg(&e->err, "invalid action for -%c: '%s'", a->flags[i], action_name);
800 }
801 15 actions[fd] = action;
802 }
803
804
1/4
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 22 taken 15 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
15 if (lflag && actions[STDIN_FILENO] == EXEC_BUFFER) {
805 // For compat. with old "filter" and "pipe-to" commands
806 ✗ actions[STDIN_FILENO] = EXEC_LINE;
807 }
808
809
2/4
✓ Branch 22 → 23 taken 15 times.
✗ Branch 22 → 26 not taken.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 26 taken 15 times.
15 if ((e->flags & EFLAG_HEADLESS) && !(exec_flags & EXECFLAG_QUIET)) {
810 ✗ LOG_INFO("automatically added -s flag to exec command (headless mode)");
811 ✗ exec_flags |= EXECFLAG_QUIET;
812 }
813
814 15 const char **argv = (const char **)a->args + a->nr_flag_args;
815 15 ssize_t outlen = handle_exec(e, argv, actions, exec_flags);
816
2/2
✓ Branch 27 → 28 taken 10 times.
✓ Branch 27 → 29 taken 5 times.
15 if (outlen <= 0) {
817 10 return outlen == 0;
818 }
819
820
1/4
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 32 taken 5 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
5 if (move_after_insert && actions[STDOUT_FILENO] == EXEC_BUFFER) {
821 ✗ block_iter_skip_bytes(&e->view->cursor, outlen);
822 }
823 return true;
824 }
825
826 20 static bool cmd_ft(EditorState *e, const CommandArgs *a)
827 {
828 20 ErrorBuffer *ebuf = &e->err;
829 20 char **args = a->args;
830 20 const char *filetype = args[0];
831
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 19 times.
20 if (unlikely(!is_valid_filetype_name(filetype))) {
832 1 return error_msg(ebuf, "Invalid filetype name: '%s'", filetype);
833 }
834
835 19 FileDetectionType dt = FT_EXTENSION;
836
2/5
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 7 not taken.
✓ Branch 5 → 8 taken 18 times.
✗ Branch 5 → 9 not taken.
✓ Branch 5 → 10 taken 1 time.
19 switch (cmdargs_pick_winning_flag(a, "bcfi")) {
837 ✗ case 'b': dt = FT_BASENAME; break;
838 ✗ case 'c': dt = FT_CONTENT; break;
839 18 case 'f': dt = FT_FILENAME; break;
840 ✗ case 'i': dt = FT_INTERPRETER; break;
841 }
842
843 19 PointerArray *filetypes = &e->filetypes;
844 19 size_t nfailed = 0;
845
2/2
✓ Branch 15 → 11 taken 19 times.
✓ Branch 15 → 16 taken 19 times.
38 for (size_t i = 1, n = a->nr_args; i < n; i++) {
846
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 19 times.
19 if (!add_filetype(filetypes, filetype, args[i], dt, ebuf)) {
847 ✗ nfailed++;
848 }
849 }
850
851 19 return nfailed == 0;
852 }
853
854 850 static bool cmd_hi(EditorState *e, const CommandArgs *a)
855 {
856
2/2
✓ Branch 2 → 3 taken 9 times.
✓ Branch 2 → 5 taken 841 times.
850 if (unlikely(a->nr_args == 0)) {
857 9 exec_builtin_color_reset(e);
858 9 return true;
859 }
860
861 841 char **strs = a->args + 1;
862 841 size_t strs_len = a->nr_args - 1;
863 841 TermStyle style;
864 841 ssize_t n = parse_term_style(&style, strs, strs_len);
865
866
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 21 taken 839 times.
841 if (unlikely(n != strs_len)) {
867 2 bool quiet = has_flag(a, 'q');
868 2 ErrorBuffer *ebuf = &e->err;
869
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 14 taken 1 time.
2 if (n < 0) {
870
2/4
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 13 not taken.
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 13 not taken.
2 return quiet || error_msg(ebuf, "too many colors");
871 }
872 1 BUG_ON(n > strs_len);
873
2/4
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
2 return quiet || error_msg(ebuf, "invalid color or attribute: '%s'", strs[n]);
874 }
875
876 839 TermFeatureFlags features = e->terminal.features;
877 839 bool true_color = !!(features & TFLAG_TRUE_COLOR);
878
1/4
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 839 times.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
839 bool optimize = (true_color && e->options.optimize_true_color);
879 839 int32_t fg = color_to_nearest(style.fg, features, optimize);
880 839 int32_t bg = color_to_nearest(style.bg, features, optimize);
881
7/8
✓ Branch 26 → 27 taken 839 times.
✗ Branch 26 → 31 not taken.
✓ Branch 28 → 29 taken 225 times.
✓ Branch 28 → 31 taken 614 times.
✓ Branch 29 → 30 taken 52 times.
✓ Branch 29 → 35 taken 173 times.
✓ Branch 30 → 31 taken 20 times.
✓ Branch 30 → 35 taken 32 times.
839 if (!true_color && has_flag(a, 'c') && (fg != style.fg || bg != style.bg)) {
882 return true;
883 }
884
885 634 style.fg = fg;
886 634 style.bg = bg;
887 634 bool changed = set_highlight_style(&e->styles, a->args[0], &style);
888
2/2
✓ Branch 32 → 33 taken 125 times.
✓ Branch 32 → 34 taken 509 times.
634 e->screen_update |= changed ? (UPDATE_SYNTAX_STYLES | UPDATE_ALL_WINDOWS) : 0;
889 634 return true;
890 }
891
892 104 static bool cmd_include(EditorState *e, const CommandArgs *a)
893 {
894 104 ConfigFlags flags = has_flag(a, 'q') ? CFG_NOFLAGS : CFG_MUST_EXIST;
895
2/2
✓ Branch 4 → 5 taken 37 times.
✓ Branch 4 → 6 taken 67 times.
104 if (has_flag(a, 'b')) {
896 37 flags |= CFG_BUILTIN;
897 }
898
899 104 SystemErrno err = read_normal_config(e, a->args[0], flags);
900 // TODO: Clean up read_normal_config() so this can be simplified to `err == 0`
901
4/6
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 11 taken 102 times.
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 10 not taken.
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 11 not taken.
104 return err == 0 || (err == ENOENT && !(flags & CFG_MUST_EXIST));
902 }
903
904 90 static bool cmd_insert(EditorState *e, const CommandArgs *a)
905 {
906 90 StringView text = strview(a->args[0]);
907
2/2
✓ Branch 3 → 6 taken 20 times.
✓ Branch 3 → 7 taken 70 times.
90 if (has_flag(a, 'k')) {
908
2/2
✓ Branch 6 → 4 taken 142 times.
✓ Branch 6 → 10 taken 20 times.
162 for (size_t i = 0; i < text.length; i++) {
909 142 insert_ch(e->view, text.data[i]);
910 }
911 return true;
912 }
913
914 70 bool move_after = has_flag(a, 'm');
915 70 insert_text(e->view, text, move_after);
916 70 return true;
917 }
918
919 16 static bool cmd_join(EditorState *e, const CommandArgs *a)
920 {
921
2/2
✓ Branch 2 → 3 taken 14 times.
✓ Branch 2 → 4 taken 2 times.
16 StringView delim = strview(a->args[0] ? a->args[0] : " ");
922 16 join_lines(e->view, delim);
923 16 return true;
924 }
925
926 16 static bool cmd_left(EditorState *e, const CommandArgs *a)
927 {
928 16 handle_selection_flags(e->view, a);
929 16 move_cursor_left(e->view);
930 16 return true;
931 }
932
933 4 static bool cmd_line(EditorState *e, const CommandArgs *a)
934 {
935 4 const char *str = a->args[0];
936 4 size_t line, column;
937
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 1 time.
4 if (unlikely(!str_to_xfilepos(strview(str), &line, &column))) {
938 3 return error_msg(&e->err, "Invalid line number: %s", str);
939 }
940
941 1 View *view = e->view;
942 1 long x = view_get_preferred_x(view);
943 1 handle_selection_flags(view, a);
944
945
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
1 if (column >= 1) {
946 // Column was specified; move to exact position
947 ✗ move_to_filepos(view, line, column);
948 } else {
949 // Column was omitted; move to line while preserving current column
950 1 move_to_line(view, line);
951 1 move_to_preferred_x(view, x);
952 }
953
954 return true;
955 }
956
957 2 static bool cmd_macro(EditorState *e, const CommandArgs *a)
958 {
959 2 MacroRecorder *m = &e->macro;
960 2 const char *action = a->args[0];
961
962
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 9 taken 1 time.
2 if (streq(action, "play") || streq(action, "run")) {
963
2/2
✓ Branch 8 → 5 taken 10 times.
✓ Branch 8 → 30 taken 1 time.
11 for (size_t i = 0, n = m->macro.count; i < n; i++) {
964 10 const char *cmd_str = m->macro.ptrs[i];
965
1/2
✓ Branch 6 → 7 taken 10 times.
✗ Branch 6 → 30 not taken.
10 if (!handle_normal_command(e, cmd_str, false)) {
966 return false;
967 }
968 }
969 return true;
970 }
971
972
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 12 taken 1 time.
1 if (streq(action, "toggle")) {
973 ✗ action = m->recording ? "stop" : "record";
974 }
975
976
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 17 taken 1 time.
1 if (streq(action, "record")) {
977 ✗ bool r = macro_record(m);
978 ✗ return info_msg(&e->err, "%s", r ? "Recording macro" : "Already recording");
979 }
980
981
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 24 taken 1 time.
1 if (streq(action, "stop")) {
982 ✗ if (!macro_stop(m)) {
983 ✗ return info_msg(&e->err, "Not recording");
984 }
985 ✗ size_t count = m->macro.count;
986 ✗ const char *plural = (count != 1) ? "s" : "";
987 ✗ return info_msg(&e->err, "Macro recording stopped; %zu command%s saved", count, plural);
988 }
989
990
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 29 taken 1 time.
1 if (streq(action, "cancel")) {
991 ✗ bool r = macro_cancel(m);
992 ✗ return info_msg(&e->err, "%s", r ? "Macro recording cancelled" : "Not recording");
993 }
994
995 1 return error_msg(&e->err, "Unknown action '%s'", action);
996 }
997
998 5 static bool cmd_match_bracket(EditorState *e, const CommandArgs *a)
999 {
1000 5 BUG_ON(a->nr_args);
1001 5 View *view = e->view;
1002 5 CodePoint cursor_char;
1003
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 4 times.
5 if (!block_iter_get_char(&view->cursor, &cursor_char)) {
1004 1 return error_msg(&e->err, "No character under cursor");
1005 }
1006
1007 4 CodePoint target = cursor_char;
1008 4 BlockIter bi = view->cursor;
1009 4 size_t level = 0;
1010 4 CodePoint u = 0;
1011
1012
4/5
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 7 → 11 taken 1 time.
✗ Branch 7 → 12 not taken.
✓ Branch 7 → 13 taken 1 time.
4 switch (cursor_char) {
1013 1 case '<':
1014 case '[':
1015 case '{':
1016 1 target++;
1017 // Fallthrough
1018 2 case '(':
1019 2 target++;
1020 2 goto search_fwd;
1021 1 case '>':
1022 case ']':
1023 case '}':
1024 1 target--;
1025 // Fallthrough
1026 1 case ')':
1027 1 target--;
1028 1 goto search_bwd;
1029 1 default:
1030 1 return error_msg(&e->err, "Character under cursor not matchable");
1031 }
1032
1033 2 search_fwd:
1034 2 block_iter_next_char(&bi, &u);
1035 2 BUG_ON(u != cursor_char);
1036
2/2
✓ Branch 24 → 15 taken 46 times.
✓ Branch 24 → 25 taken 1 time.
47 while (block_iter_next_char(&bi, &u)) {
1037
2/2
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 20 taken 44 times.
46 if (u == target) {
1038
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 19 taken 1 time.
2 if (level == 0) {
1039 1 block_iter_prev_char(&bi, &u);
1040 1 goto found;
1041 }
1042 1 level--;
1043
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 43 times.
44 } else if (u == cursor_char) {
1044 1 level++;
1045 }
1046 }
1047 1 goto not_found;
1048
1049 1 search_bwd:
1050
1/2
✓ Branch 34 → 26 taken 38 times.
✗ Branch 34 → 35 not taken.
38 while (block_iter_prev_char(&bi, &u)) {
1051
2/2
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 30 taken 37 times.
38 if (u == target) {
1052
1/2
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 29 not taken.
1 if (level == 0) {
1053 1 goto found;
1054 }
1055 ✗ level--;
1056
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 37 times.
37 } else if (u == cursor_char) {
1057 ✗ level++;
1058 }
1059 }
1060
1061 ✗ not_found:
1062 1 return error_msg(&e->err, "No matching bracket found");
1063
1064 2 found:
1065 2 handle_selection_flags(view, a);
1066 2 view->cursor = bi;
1067 2 return true;
1068 }
1069
1070 1 static bool cmd_mode(EditorState *e, const CommandArgs *a)
1071 {
1072 1 const char *name = a->args[0];
1073 1 ModeHandler *handler = get_mode_handler(&e->modes, name);
1074
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
1 if (!handler) {
1075 1 return error_msg(&e->err, "unknown mode '%s'", name);
1076 }
1077
1078 ✗ push_input_mode(e, handler);
1079 ✗ return true;
1080 }
1081
1082 11 static bool cmd_move_tab(EditorState *e, const CommandArgs *a)
1083 {
1084 11 Window *window = e->window;
1085 11 const size_t ntabs = window->views.count;
1086 11 const char *str = a->args[0];
1087 11 size_t to, from = ptr_array_xindex(&window->views, e->view);
1088
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 6 taken 5 times.
11 if (streq(str, "left")) {
1089 6 to = wrapping_decrement(from, ntabs);
1090
2/2
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 9 taken 2 times.
5 } else if (streq(str, "right")) {
1091 3 to = wrapping_increment(from, ntabs);
1092 } else {
1093
3/4
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 12 not taken.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 1 time.
2 if (!str_to_size(str, &to) || to == 0) {
1094 1 return error_msg(&e->err, "Invalid tab position %s", str);
1095 }
1096 1 to = MIN(to, ntabs) - 1;
1097 }
1098 10 ptr_array_move(&window->views, from, to);
1099 10 window->update_tabbar = true;
1100 10 return true;
1101 }
1102
1103 2 static bool cmd_msg(EditorState *e, const CommandArgs *a)
1104 {
1105 2 const char *str = a->args[0];
1106
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 6 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
2 if (str && cmdargs_has_any_flag(a, "np")) {
1107 1 return error_msg (
1108 &e->err,
1109 "flags [-n|-p] can't be used with [number] argument"
1110 );
1111 }
1112
1113 1 char abc = cmdargs_pick_winning_flag(a, "ABC");
1114
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
1 MessageList *msgs = &e->messages[abc ? abc - 'A' : 0];
1115 1 size_t n = msgs->array.count;
1116
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 28 taken 1 time.
1 if (n == 0) {
1117 return true;
1118 }
1119
1120 ✗ bool wrap = has_flag(a, 'w');
1121 ✗ size_t p = msgs->pos;
1122 ✗ switch (cmdargs_pick_winning_flag(a, "np")) {
1123 ✗ case 'n':
1124 ✗ p = wrap ? wrapping_increment(p, n) : saturating_increment(p, n - 1);
1125 ✗ break;
1126 ✗ case 'p':
1127 ✗ p = wrap ? wrapping_decrement(p, n) : saturating_decrement(p);
1128 ✗ break;
1129 ✗ case 0:
1130 ✗ if (str) {
1131 ✗ if (!str_to_size(str, &p) || p == 0) {
1132 ✗ return error_msg(&e->err, "invalid message index: %s", str);
1133 }
1134 ✗ p = MIN(p - 1, n - 1);
1135 }
1136 }
1137
1138 ✗ msgs->pos = p;
1139 ✗ return activate_current_message(msgs, e->window, &e->err);
1140 }
1141
1142 12 static bool cmd_new_line(EditorState *e, const CommandArgs *a)
1143 {
1144 12 bool autoindent = e->buffer->options.auto_indent;
1145 12 NewlineIndentType type = autoindent ? NLI_AUTO_INDENT : NLI_NO_INDENT;
1146
2/4
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 5 not taken.
✓ Branch 3 → 6 taken 4 times.
✓ Branch 3 → 7 taken 8 times.
12 switch (cmdargs_pick_winning_flag(a, "iIm")) {
1147 ✗ case 'I': type = NLI_NO_INDENT; break;
1148 ✗ case 'i': type = NLI_AUTO_INDENT; break;
1149 4 case 'm': type = NLI_COPY_INDENT; break;
1150 }
1151
1152 12 bool above_cursor = has_flag(a, 'a');
1153 12 new_line(e->view, above_cursor, type);
1154 12 return true;
1155 }
1156
1157 1 static bool cmd_next(EditorState *e, const CommandArgs *a)
1158 {
1159 1 BUG_ON(a->nr_args);
1160 1 const PointerArray *views = &e->window->views;
1161 1 size_t current = ptr_array_xindex(views, e->view);
1162 1 size_t next = wrapping_increment(current, views->count);
1163 1 set_view(views->ptrs[next]);
1164 1 return true;
1165 }
1166
1167 1 static bool xglob(ErrorBuffer *ebuf, char **args, glob_t *globbuf)
1168 {
1169 1 BUG_ON(!args);
1170 1 BUG_ON(!args[0]);
1171 1 int err = glob(*args, GLOB_NOCHECK, NULL, globbuf);
1172
2/4
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 11 not taken.
✗ Branch 10 → 8 not taken.
✓ Branch 10 → 11 taken 1 time.
1 while (err == 0 && *++args) {
1173 ✗ err = glob(*args, GLOB_NOCHECK | GLOB_APPEND, NULL, globbuf);
1174 }
1175
1176
1/2
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 18 not taken.
1 if (likely(err == 0)) {
1177 1 BUG_ON(globbuf->gl_pathc == 0);
1178 1 BUG_ON(!globbuf->gl_pathv);
1179 1 BUG_ON(!globbuf->gl_pathv[0]);
1180 return true;
1181 }
1182
1183 ✗ BUG_ON(err == GLOB_NOMATCH);
1184 ✗ globfree(globbuf);
1185 ✗ return error_msg(ebuf, "glob: %s", (err == GLOB_NOSPACE) ? strerror(ENOMEM) : "failed");
1186 }
1187
1188 48 static bool cmd_open(EditorState *e, const CommandArgs *a)
1189 {
1190 48 ErrorBuffer *ebuf = &e->err;
1191 48 bool temporary = has_flag(a, 't');
1192
3/4
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 47 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
48 if (unlikely(temporary && a->nr_args > 0)) {
1193 1 return error_msg(ebuf, "'open -t' can't be used with filename arguments");
1194 }
1195
1196 47 const char *requested_encoding = NULL;
1197 47 char **args = a->args;
1198
2/2
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 21 taken 44 times.
47 if (unlikely(a->nr_flag_args > 0)) {
1199 // The "-e" flag is the only one that takes an argument, so the
1200 // above condition implies it was used
1201 3 BUG_ON(!has_flag(a, 'e'));
1202 3 requested_encoding = args[a->nr_flag_args - 1];
1203 3 args += a->nr_flag_args;
1204 }
1205
1206 3 const char *encoding = NULL; // (Auto-detect)
1207
1/2
✓ Branch 10 → 11 taken 3 times.
✗ Branch 10 → 21 not taken.
3 if (requested_encoding) {
1208 3 EncodingType enctype = lookup_encoding(requested_encoding);
1209
1/2
✓ Branch 12 → 13 taken 3 times.
✗ Branch 12 → 14 not taken.
3 if (enctype == UTF8) {
1210 3 encoding = encoding_from_type(UTF8);
1211 ✗ } else if (conversion_supported_by_iconv(requested_encoding, "UTF-8")) {
1212 ✗ encoding = encoding_normalize(requested_encoding);
1213 } else {
1214 ✗ if (errno == EINVAL) {
1215 ✗ return error_msg(ebuf, "Unsupported encoding '%s'", requested_encoding);
1216 }
1217 ✗ return error_msg (
1218 ebuf,
1219 "iconv conversion from '%s' failed: %s",
1220 requested_encoding,
1221 strerror(errno)
1222 );
1223 }
1224 }
1225
1226 47 Window *window = e->window;
1227
2/2
✓ Branch 21 → 22 taken 42 times.
✓ Branch 21 → 25 taken 5 times.
47 if (a->nr_args == 0) {
1228 42 View *view = window_open_new_file(window);
1229 42 view->buffer->temporary = temporary;
1230
2/2
✓ Branch 23 → 24 taken 3 times.
✓ Branch 23 → 34 taken 39 times.
42 if (requested_encoding) {
1231 3 buffer_set_encoding(view->buffer, encoding, e->options.utf8_bom);
1232 }
1233 return true;
1234 }
1235
1236 5 char **paths = args;
1237 5 glob_t globbuf;
1238 5 bool use_glob = has_flag(a, 'g');
1239
2/2
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 30 taken 4 times.
5 if (use_glob) {
1240
1/2
✓ Branch 28 → 29 taken 1 time.
✗ Branch 28 → 34 not taken.
1 if (!xglob(ebuf, args, &globbuf)) {
1241 return false;
1242 }
1243 1 paths = globbuf.gl_pathv;
1244 }
1245
1246 5 View *first_opened = window_open_files(window, paths, encoding);
1247
1248
2/2
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 33 taken 4 times.
5 if (use_glob) {
1249 1 globfree(&globbuf);
1250 }
1251
1252 5 return !!first_opened;
1253 }
1254
1255 147 static bool cmd_option(EditorState *e, const CommandArgs *a)
1256 {
1257 147 BUG_ON(a->nr_args < 3);
1258 147 const char *arg0 = a->args[0];
1259 147 char **strs = a->args + 1;
1260 147 size_t nstrs = a->nr_args - 1;
1261 147 ErrorBuffer *ebuf = &e->err;
1262
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 144 times.
147 if (unlikely(arg0[0] == '\0')) {
1263 3 return error_msg(ebuf, "first argument cannot be empty");
1264 }
1265
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 143 times.
144 if (unlikely(nstrs & 1)) {
1266 1 return error_msg(ebuf, "missing option value");
1267 }
1268
2/2
✓ Branch 9 → 10 taken 135 times.
✓ Branch 9 → 28 taken 8 times.
143 if (unlikely(!validate_local_options(ebuf, strs))) {
1269 return false;
1270 }
1271
1272 135 PointerArray *opts = &e->file_options;
1273
2/2
✓ Branch 11 → 12 taken 10 times.
✓ Branch 11 → 17 taken 125 times.
135 if (has_flag(a, 'r')) {
1274 10 FileTypeOrFileName u = {.filename = regexp_intern(ebuf, arg0)};
1275
1/2
✓ Branch 13 → 14 taken 10 times.
✗ Branch 13 → 16 not taken.
10 if (unlikely(!u.filename)) {
1276 return false;
1277 }
1278 10 add_file_options(opts, FOPTS_FILENAME, u, strs, nstrs);
1279 10 return true;
1280 }
1281
1282 125 StringView filetype_names = strview(arg0);
1283 125 size_t errors = 0;
1284
1285
2/2
✓ Branch 26 → 18 taken 295 times.
✓ Branch 26 → 27 taken 125 times.
420 for (size_t pos = 0; pos < filetype_names.length; ) {
1286 295 const StringView ft = get_delim(filetype_names, &pos, ',');
1287
2/2
✓ Branch 19 → 20 taken 8 times.
✓ Branch 19 → 22 taken 287 times.
295 if (unlikely(!is_valid_filetype_name_sv(ft))) {
1288 8 error_msg(ebuf, "invalid filetype name: '%.*s'", SV_FMT(ft));
1289 8 errors++;
1290 8 continue;
1291 }
1292 287 FileTypeOrFileName u = {.filetype = mem_intern(ft.data, ft.length)};
1293 287 add_file_options(opts, FOPTS_FILETYPE, u, strs, nstrs);
1294 }
1295
1296 125 return !errors;
1297 }
1298
1299 9 static bool cmd_blkdown(EditorState *e, const CommandArgs *a)
1300 {
1301 9 View *view = e->view;
1302 9 handle_selection_flags(view, a);
1303
1304 // If current line is blank, skip past consecutive blank lines
1305 9 StringView line = get_current_line(view->cursor);
1306
2/2
✓ Branch 5 → 9 taken 2 times.
✓ Branch 5 → 14 taken 7 times.
9 if (strview_isblank(line)) {
1307
1/2
✓ Branch 10 → 6 taken 3 times.
✗ Branch 10 → 14 not taken.
3 while (block_iter_next_line(&view->cursor)) {
1308 3 line = block_iter_get_line(&view->cursor);
1309
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 14 taken 2 times.
3 if (!strview_isblank(line)) {
1310 break;
1311 }
1312 }
1313 }
1314
1315 // Skip past non-blank lines
1316
2/2
✓ Branch 16 → 11 taken 21 times.
✓ Branch 16 → 17 taken 4 times.
25 while (block_iter_next_line(&view->cursor)) {
1317 21 line = block_iter_get_line(&view->cursor);
1318
2/2
✓ Branch 13 → 15 taken 16 times.
✓ Branch 13 → 17 taken 5 times.
21 if (strview_isblank(line)) {
1319 break;
1320 }
1321 }
1322
1323 // If we've reached the last populated line in the buffer, move
1324 // down one line (to EOF)
1325 9 BlockIter tmp = view->cursor;
1326
3/4
✓ Branch 18 → 19 taken 9 times.
✗ Branch 18 → 21 not taken.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 19 → 21 taken 5 times.
9 if (block_iter_eat_line(&tmp) && block_iter_is_eof(&tmp)) {
1327 4 view->cursor = tmp;
1328 }
1329
1330 9 return true;
1331 }
1332
1333 4 static bool cmd_blkup(EditorState *e, const CommandArgs *a)
1334 {
1335 4 View *view = e->view;
1336 4 handle_selection_flags(view, a);
1337
1338 // If cursor is on the first line, just move to bol
1339
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 3 times.
4 if (view->cy == 0) {
1340 1 block_iter_bol(&view->cursor);
1341 1 return true;
1342 }
1343
1344 // If current line is blank, skip past consecutive blank lines
1345 3 StringView line = get_current_line(view->cursor);
1346
2/2
✓ Branch 8 → 12 taken 1 time.
✓ Branch 8 → 17 taken 2 times.
3 if (strview_isblank(line)) {
1347
1/2
✓ Branch 13 → 9 taken 3 times.
✗ Branch 13 → 17 not taken.
3 while (block_iter_prev_line(&view->cursor)) {
1348 3 line = block_iter_get_line(&view->cursor);
1349
2/2
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 17 taken 1 time.
3 if (!strview_isblank(line)) {
1350 break;
1351 }
1352 }
1353 }
1354
1355 // Skip past non-blank lines
1356
2/2
✓ Branch 19 → 14 taken 14 times.
✓ Branch 19 → 20 taken 2 times.
16 while (block_iter_prev_line(&view->cursor)) {
1357 14 line = block_iter_get_line(&view->cursor);
1358
2/2
✓ Branch 16 → 18 taken 13 times.
✓ Branch 16 → 20 taken 1 time.
14 if (strview_isblank(line)) {
1359 break;
1360 }
1361 }
1362
1363 return true;
1364 }
1365
1366 6 static bool cmd_paste(EditorState *e, const CommandArgs *a)
1367 {
1368 6 PasteLinesType type = PASTE_LINES_BELOW_CURSOR;
1369
2/3
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
✓ Branch 3 → 6 taken 4 times.
6 switch (cmdargs_pick_winning_flag(a, "ac")) {
1370 ✗ case 'a': type = PASTE_LINES_ABOVE_CURSOR; break;
1371 2 case 'c': type = PASTE_LINES_INLINE; break;
1372 }
1373
1374 6 bool move_after = has_flag(a, 'm');
1375 6 paste(&e->clipboard, e->view, type, move_after);
1376 6 return true;
1377 }
1378
1379 2 static bool cmd_pgdown(EditorState *e, const CommandArgs *a)
1380 {
1381 2 View *view = e->view;
1382 2 handle_selection_flags(view, a);
1383
1384 2 Window *window = e->window;
1385 2 long margin = window_get_scroll_margin(window);
1386 2 long bottom = view->vy + window->edit_h - 1 - margin;
1387 2 long count;
1388
1389
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
2 if (view->cy < bottom) {
1390 ✗ count = bottom - view->cy;
1391 } else {
1392 2 count = window->edit_h - 1 - (margin * 2);
1393 }
1394
1395 2 move_down(view, count);
1396 2 return true;
1397 }
1398
1399 1 static bool cmd_pgup(EditorState *e, const CommandArgs *a)
1400 {
1401 1 View *view = e->view;
1402 1 handle_selection_flags(view, a);
1403
1404 1 Window *window = e->window;
1405 1 long margin = window_get_scroll_margin(window);
1406 1 long top = view->vy + margin;
1407 1 long count;
1408
1409
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
1 if (view->cy > top) {
1410 1 count = view->cy - top;
1411 } else {
1412 ✗ count = window->edit_h - 1 - (margin * 2);
1413 }
1414
1415 1 move_up(view, count);
1416 1 return true;
1417 }
1418
1419 ✗ static bool cmd_play(EditorState *e, const CommandArgs *a)
1420 {
1421 ✗ const MacroRecorder *m = &e->macro;
1422 ✗ bool keep_going = has_flag(a, 'k');
1423
1424 ✗ for (size_t i = 0, n = m->macro.count; i < n; i++) {
1425 ✗ const char *cmd_str = m->macro.ptrs[i];
1426 ✗ if (!handle_normal_command(e, cmd_str, false) && !keep_going) {
1427 return false;
1428 }
1429 }
1430
1431 return true;
1432 }
1433
1434 1 static bool cmd_prev(EditorState *e, const CommandArgs *a)
1435 {
1436 1 BUG_ON(a->nr_args);
1437 1 const PointerArray *views = &e->window->views;
1438 1 size_t current = ptr_array_xindex(views, e->view);
1439 1 size_t prev = wrapping_decrement(current, views->count);
1440 1 set_view(views->ptrs[prev]);
1441 1 return true;
1442 }
1443
1444 3 static View *window_find_modified_view(Window *window)
1445 {
1446
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 7 taken 3 times.
3 if (buffer_modified(window->view->buffer)) {
1447 return window->view;
1448 }
1449 ✗ for (size_t i = 0, n = window->views.count; i < n; i++) {
1450 ✗ View *view = window->views.ptrs[i];
1451 ✗ if (buffer_modified(view->buffer)) {
1452 return view;
1453 }
1454 }
1455 return NULL;
1456 }
1457
1458 5 static size_t count_modified_buffers(const PointerArray *buffers, View **first)
1459 {
1460 5 View *modified = NULL;
1461 5 size_t nr_modified = 0;
1462
2/2
✓ Branch 8 → 3 taken 8 times.
✓ Branch 8 → 9 taken 5 times.
13 for (size_t i = 0, n = buffers->count; i < n; i++) {
1463 8 Buffer *buffer = buffers->ptrs[i];
1464
2/2
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 5 taken 3 times.
8 if (!buffer_modified(buffer)) {
1465 5 continue;
1466 }
1467 3 nr_modified++;
1468
1/2
✓ Branch 5 → 6 taken 3 times.
✗ Branch 5 → 7 not taken.
3 if (!modified) {
1469 3 modified = buffer_get_first_view(buffer);
1470 }
1471 }
1472
1473 5 BUG_ON(nr_modified > 0 && !modified);
1474 5 *first = modified;
1475 5 return nr_modified;
1476 }
1477
1478 7 static bool cmd_quit(EditorState *e, const CommandArgs *a)
1479 {
1480 7 static const FlagMapping fmap[] = {
1481 {'C', EFLAG_CMD_HIST},
1482 {'S', EFLAG_SEARCH_HIST},
1483 {'F', EFLAG_FILE_HIST},
1484 {'H', EFLAG_ALL_HIST},
1485 };
1486
1487 7 ErrorBuffer *ebuf = &e->err;
1488 7 unsigned int exit_code = EDITOR_EXIT_OK;
1489
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 7 taken 2 times.
7 if (a->nr_args) {
1490 5 const char *arg = a->args[0];
1491 5 const unsigned int max = EDITOR_EXIT_MAX;
1492
4/4
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 6 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 3 times.
5 if (!str_to_uint(arg, &exit_code) || exit_code > EDITOR_EXIT_MAX) {
1493 2 return error_msg (
1494 ebuf,
1495 "exit code should be an integer between 0 and %u; got '%s'",
1496 max, arg
1497 );
1498 }
1499 }
1500
1501 5 View *first_modified = NULL;
1502 5 size_t n = count_modified_buffers(&e->buffers, &first_modified);
1503
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 3 times.
5 if (n == 0) {
1504 2 goto exit;
1505 }
1506
1507 3 BUG_ON(!first_modified);
1508
1/2
✓ Branch 12 → 13 taken 3 times.
✗ Branch 12 → 14 not taken.
3 const char *plural = (n != 1) ? "s" : "";
1509
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 3 times.
3 if (has_flag(a, 'f')) {
1510 ✗ LOG_INFO("force quitting with %zu modified buffer%s", n, plural);
1511 ✗ goto exit;
1512 }
1513
1514 // Activate a modified view (giving preference to the current view or
1515 // a view in the current window)
1516 3 View *view = window_find_modified_view(e->window);
1517
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 3 times.
3 set_view(view ? view : first_modified);
1518
1519
2/2
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 2 times.
3 if (!has_flag(a, 'p')) {
1520 1 return error_msg(ebuf, "Save modified files or run 'quit -f' to quit without saving");
1521 }
1522
1523
1/2
✓ Branch 24 → 25 taken 2 times.
✗ Branch 24 → 26 not taken.
2 if (unlikely(e->flags & EFLAG_HEADLESS)) {
1524 2 return error_msg(ebuf, "-p flag unavailable in headless mode");
1525 }
1526
1527 ✗ char question[128];
1528 ✗ xsnprintf (
1529 question, sizeof question,
1530 "Quit without saving %zu modified buffer%s? [y/N]",
1531 n, plural
1532 );
1533
1534 ✗ if (dialog_prompt(e, question, "ny") != 'y') {
1535 return false;
1536 }
1537 ✗ LOG_INFO("quit prompt accepted with %zu modified buffer%s", n, plural);
1538
1539 2 exit:;
1540 2 EditorFlags histflags = cmdargs_convert_flags(a, fmap, ARRAYLEN(fmap));
1541 2 e->flags &= ~histflags;
1542 2 e->status = exit_code;
1543 2 return true;
1544 }
1545
1546 ✗ static bool cmd_record(EditorState *e, const CommandArgs *a)
1547 {
1548 ✗ MacroRecorder *m = &e->macro;
1549 ✗ char action = cmdargs_pick_winning_flag(a, "crs");
1550 ✗ const char *msg;
1551
1552 ✗ switch (action ? action : (m->recording ? 's' : 'r')) {
1553 ✗ case 'c':
1554 ✗ msg = macro_cancel(m) ? "Macro recording cancelled" : "Not recording";
1555 break;
1556 ✗ case 'r':
1557 ✗ msg = macro_record(m) ? "Recording macro" : "Already recording";
1558 break;
1559 ✗ case 's':
1560 ✗ if (macro_stop(m)) {
1561 ✗ size_t count = m->macro.count;
1562 ✗ return info_msg (
1563 &e->err, "Macro recording stopped; %zu command%s saved",
1564 count, (count != 1) ? "s" : ""
1565 );
1566 }
1567 msg = "Not recording";
1568 break;
1569 ✗ default:
1570 − BUG("unexpected flag: %c", action);
1571 return false;
1572 }
1573
1574 ✗ return info_msg(&e->err, "%s", msg);
1575 }
1576
1577 7 static bool cmd_redo(EditorState *e, const CommandArgs *a)
1578 {
1579 7 char *arg = a->args[0];
1580 7 unsigned long change_id = 0;
1581
5/6
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 7 taken 1 time.
✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 1 time.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 5 times.
7 if (arg && (!str_to_ulong(arg, &change_id) || change_id == 0)) {
1582 1 return error_msg(&e->err, "Invalid change id: %s", arg);
1583 }
1584
1585
3/4
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 12 taken 3 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 3 times.
6 return redo(e->view, &e->err, change_id) && unselect(e->view);
1586 }
1587
1588 1 static bool cmd_refresh(EditorState *e, const CommandArgs *a)
1589 {
1590 1 BUG_ON(a->nr_args);
1591 1 e->screen_update |= UPDATE_ALL;
1592 1 return true;
1593 }
1594
1595 8 static bool repeat_insert (
1596 View *view,
1597 ErrorBuffer *ebuf,
1598 const char *str,
1599 unsigned int count,
1600 bool move_after
1601 ) {
1602 8 BUG_ON(count < 2);
1603 8 size_t str_len = strlen(str);
1604 8 size_t bufsize;
1605
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 8 times.
8 if (unlikely(size_multiply_overflows(count, str_len, &bufsize))) {
1606 ✗ return error_msg(ebuf, "Repeated insert would overflow");
1607 }
1608
1/2
✓ Branch 7 → 8 taken 8 times.
✗ Branch 7 → 29 not taken.
8 if (unlikely(bufsize == 0)) {
1609 return true;
1610 }
1611
1612 8 char *buf = malloc(bufsize);
1613
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 8 times.
8 if (unlikely(!buf)) {
1614 ✗ return error_msg_errno(ebuf, "malloc");
1615 }
1616
1617 8 char tmp[4096];
1618
2/2
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 13 taken 5 times.
8 if (str_len == 1) {
1619 3 memset(buf, str[0], bufsize);
1620 3 LOG_DEBUG("Optimized %u inserts of 1 byte into 1 memset()", count);
1621 3 goto insert;
1622
3/4
✓ Branch 13 → 14 taken 4 times.
✓ Branch 13 → 15 taken 1 time.
✗ Branch 15 → 14 not taken.
✓ Branch 15 → 19 taken 1 time.
5 } else if (bufsize < 2 * sizeof(tmp) || str_len > sizeof(tmp) / 8) {
1623
2/2
✓ Branch 17 → 16 taken 65 times.
✓ Branch 17 → 18 taken 4 times.
69 for (size_t i = 0; i < count; i++) {
1624 65 memcpy(buf + (i * str_len), str, str_len);
1625 }
1626 4 goto insert;
1627 }
1628
1629 1 size_t strs_per_tmp = sizeof(tmp) / str_len;
1630 1 size_t tmp_len = strs_per_tmp * str_len;
1631 1 size_t tmps_per_buf = bufsize / tmp_len;
1632 1 size_t remainder = bufsize % tmp_len;
1633
1634 // Create a block of text containing `strs_per_tmp` concatenated strs
1635
2/2
✓ Branch 21 → 20 taken 1024 times.
✓ Branch 21 → 23 taken 1 time.
1025 for (size_t i = 0; i < strs_per_tmp; i++) {
1636 1024 memcpy(tmp + (i * str_len), str, str_len);
1637 }
1638
1639 // Copy `tmps_per_buf` copies of `tmp` into `buf`
1640
2/2
✓ Branch 23 → 22 taken 2 times.
✓ Branch 23 → 24 taken 1 time.
3 for (size_t i = 0; i < tmps_per_buf; i++) {
1641 2 memcpy(buf + (i * tmp_len), tmp, tmp_len);
1642 }
1643
1644 // Copy the remainder into `buf` (if any)
1645
1/2
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 26 not taken.
1 if (remainder) {
1646 1 memcpy(buf + (tmps_per_buf * tmp_len), tmp, remainder);
1647 }
1648
1649 1 LOG_DEBUG (
1650 "Optimized %u inserts of %zu bytes into %zu inserts of %zu bytes",
1651 count, str_len,
1652 tmps_per_buf, tmp_len
1653 );
1654
1655 8 insert:
1656 8 insert_text(view, string_view(buf, bufsize), move_after);
1657 8 free(buf);
1658 8 return true;
1659 }
1660
1661 ✗ static bool cmd_reopen(EditorState *e, const CommandArgs* UNUSED_ARG(a))
1662 {
1663 ✗ for (const FileHistoryEntry *h = e->file_history.last; h; h = h->prev) {
1664 ✗ const char *path = h->filename;
1665 // The combination of walking the history and doing a linear search
1666 // (with find_buffer()) here makes this O(m*n) in the worst case,
1667 // although n (e->buffers.count) will typically be small and m
1668 // (e->file_history.entries.count) is bounded by FILEHIST_MAX_ENTRIES
1669 ✗ if (
1670 ✗ !find_buffer(&e->buffers, path) // Not already open in the editor
1671 ✗ && access(path, R_OK) == 0 // File exists and is readable
1672 ✗ && window_open_file(e->window, path, NULL) // Reopened successfully
1673 ) {
1674 return true;
1675 }
1676 }
1677
1678 ✗ return error_msg(&e->err, "no reopenable files in history");
1679 }
1680
1681 39 static bool cmd_repeat(EditorState *e, const CommandArgs *a)
1682 {
1683 39 ErrorBuffer *ebuf = &e->err;
1684 39 unsigned int count;
1685
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 38 times.
39 if (unlikely(!str_to_uint(a->args[0], &count))) {
1686 1 return error_msg(ebuf, "Not a valid repeat count: %s", a->args[0]);
1687 }
1688
2/2
✓ Branch 5 → 6 taken 36 times.
✓ Branch 5 → 20 taken 2 times.
38 if (unlikely(count == 0)) {
1689 return true;
1690 }
1691
1692 36 const Command *cmd = find_normal_command(a->args[1]);
1693
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 35 times.
36 if (unlikely(!cmd)) {
1694 1 return error_msg(ebuf, "No such command: %s", a->args[1]);
1695 }
1696
1697 35 CommandArgs a2 = cmdargs_new(a->args + 2);
1698
1/2
✓ Branch 10 → 11 taken 35 times.
✗ Branch 10 → 20 not taken.
35 if (unlikely(!parse_args(cmd, &a2, ebuf))) {
1699 return false;
1700 }
1701
1702
6/6
✓ Branch 11 → 12 taken 32 times.
✓ Branch 11 → 15 taken 3 times.
✓ Branch 12 → 13 taken 10 times.
✓ Branch 12 → 15 taken 22 times.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 16 taken 8 times.
35 if (count > 1 && cmd->cmd == cmd_insert && !has_flag(&a2, 'k')) {
1703 // Use optimized implementation for repeated "insert"
1704 8 bool move_after = has_flag(&a2, 'm');
1705 8 return repeat_insert(e->view, ebuf, a2.args[0], count, move_after);
1706 }
1707
1708
2/2
✓ Branch 19 → 18 taken 1096 times.
✓ Branch 19 → 20 taken 27 times.
1123 while (count--) {
1709 1096 command_func_call(e, ebuf, cmd, &a2);
1710 }
1711
1712 // TODO: return false if fn() fails?
1713 return true;
1714 }
1715
1716 14 static bool cmd_replace(EditorState *e, const CommandArgs *a)
1717 {
1718 14 static const FlagMapping map[] = {
1719 {'b', REPLACE_BASIC},
1720 {'c', REPLACE_CONFIRM},
1721 {'g', REPLACE_GLOBAL},
1722 {'i', REPLACE_IGNORE_CASE},
1723 };
1724
1725 14 const char *pattern = a->args[0];
1726 14 ReplaceFlags flags = cmdargs_convert_flags(a, map, ARRAYLEN(map));
1727
1728
3/4
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 6 taken 12 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 6 not taken.
14 if (unlikely((flags & REPLACE_CONFIRM) && (e->flags & EFLAG_HEADLESS))) {
1729 2 return error_msg(&e->err, "-c flag unavailable in headless mode");
1730 }
1731
1732 12 String escaped = string_new(0);
1733
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 13 taken 12 times.
12 if (has_flag(a, 'e')) {
1734 ✗ StringView pat = strview(pattern);
1735 ✗ if (strview_contains_char_type(pat, ASCII_REGEX)) {
1736 ✗ escaped = regexp_escape(pat);
1737 ✗ pattern = string_borrow_cstring(&escaped);
1738 }
1739 ✗ flags &= ~REPLACE_BASIC;
1740 }
1741
1742
2/2
✓ Branch 13 → 14 taken 10 times.
✓ Branch 13 → 15 taken 2 times.
12 const char *replacement = a->args[1] ? a->args[1] : "";
1743 12 bool r = reg_replace(e, pattern, replacement, flags);
1744 12 string_free(&escaped);
1745 12 return r;
1746 }
1747
1748 31 static bool cmd_right(EditorState *e, const CommandArgs *a)
1749 {
1750 31 handle_selection_flags(e->view, a);
1751 31 move_cursor_right(e->view);
1752 31 return true;
1753 }
1754
1755 ✗ static bool stat_changed(const FileInfo *file, const struct stat *st)
1756 {
1757 // Don't compare st_mode because we allow chmod 755 etc.
1758 ✗ return !timespecs_equal(get_stat_mtime(st), &file->mtime)
1759 ✗ || st->st_dev != file->dev
1760 ✗ || st->st_ino != file->ino
1761 ✗ || st->st_size != file->size;
1762 }
1763
1764 ✗ static SystemErrno save_unmodified_buffer(Buffer *buffer, const char *filename)
1765 {
1766 ✗ SaveUnmodifiedType type = buffer->options.save_unmodified;
1767 ✗ if (type == SAVE_NONE) {
1768 ✗ LOG_INFO("buffer unchanged; leaving file untouched");
1769 ✗ return 0;
1770 }
1771
1772 ✗ BUG_ON(type != SAVE_TOUCH);
1773 ✗ struct timespec times[2];
1774 ✗ if (unlikely(clock_gettime(CLOCK_REALTIME, &times[0]) != 0)) {
1775 ✗ return LOG_ERRNO("aborting partial save; clock_gettime() failed");
1776 }
1777
1778 ✗ times[1] = times[0];
1779 ✗ if (unlikely(utimensat(AT_FDCWD, filename, times, 0) != 0)) {
1780 ✗ return LOG_ERRNO("aborting partial save; utimensat() failed");
1781 }
1782
1783 ✗ buffer->file.mtime = times[0];
1784 ✗ LOG_INFO("buffer unchanged; mtime/atime updated");
1785 ✗ return 0;
1786 }
1787
1788 25 static bool cmd_save(EditorState *e, const CommandArgs *a)
1789 {
1790 25 Buffer *buffer = e->buffer;
1791 25 ErrorBuffer *ebuf = &e->err;
1792
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 25 times.
25 if (unlikely(buffer->stdout_buffer)) {
1793 ✗ const char *f = buffer_filename(buffer);
1794 ✗ return info_msg(ebuf, "%s can't be saved; it will be piped to stdout on exit", f);
1795 }
1796
1797 25 char du_flag = cmdargs_pick_winning_flag(a, "du");
1798
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 24 times.
25 bool crlf = du_flag ? (du_flag == 'd') : buffer->crlf_newlines;
1799
1800 25 const char *requested_encoding = NULL;
1801 25 char **args = a->args;
1802
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 14 taken 23 times.
25 if (unlikely(a->nr_flag_args > 0)) {
1803 2 BUG_ON(!has_flag(a, 'e'));
1804 2 requested_encoding = args[a->nr_flag_args - 1];
1805 2 args += a->nr_flag_args;
1806 }
1807
1808 25 const char *encoding = buffer->encoding;
1809 25 bool bom = buffer->bom;
1810
2/2
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 32 taken 23 times.
25 if (requested_encoding) {
1811 2 EncodingType et = lookup_encoding(requested_encoding);
1812
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 21 taken 1 time.
2 if (et == UTF8) {
1813
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 32 taken 1 time.
1 if (!encoding_is_utf8(encoding)) {
1814 // Encoding changed
1815 ✗ encoding = encoding_from_type(UTF8);
1816 ✗ bom = e->options.utf8_bom;
1817 }
1818
1/2
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 28 not taken.
1 } else if (conversion_supported_by_iconv("UTF-8", requested_encoding)) {
1819 1 encoding = encoding_normalize(requested_encoding);
1820
1/2
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 32 not taken.
1 if (encoding != buffer->encoding) {
1821 // Encoding changed
1822 1 bom = !!get_bom_for_encoding(lookup_encoding(encoding));
1823 }
1824 } else {
1825 ✗ if (errno == EINVAL) {
1826 ✗ return error_msg(ebuf, "Unsupported encoding '%s'", requested_encoding);
1827 }
1828 ✗ return error_msg (
1829 ebuf,
1830 "iconv conversion to '%s' failed: %s",
1831 requested_encoding,
1832 strerror(errno)
1833 );
1834 }
1835 }
1836
1837 25 char b_flag = cmdargs_pick_winning_flag(a, "bB");
1838
1/2
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 25 times.
25 bom = b_flag ? (b_flag == 'b') : bom;
1839
1840 25 char *absolute = buffer->abs_filename;
1841 25 bool force = has_flag(a, 'f');
1842 25 bool new_locked = false;
1843
2/2
✓ Branch 36 → 37 taken 24 times.
✓ Branch 36 → 45 taken 1 time.
25 if (a->nr_args > 0) {
1844
2/2
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 39 taken 23 times.
24 if (args[0][0] == '\0') {
1845 1 return error_msg(ebuf, "Empty filename not allowed");
1846 }
1847 23 char *tmp = path_absolute(args[0]);
1848
1/2
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 23 times.
23 if (!tmp) {
1849 ✗ return error_msg_errno(ebuf, "Failed to make absolute path");
1850 }
1851
3/4
✓ Branch 42 → 43 taken 2 times.
✓ Branch 42 → 55 taken 21 times.
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 55 taken 2 times.
23 if (absolute && streq(tmp, absolute)) {
1852 ✗ free(tmp);
1853 } else {
1854 absolute = tmp;
1855 }
1856 } else {
1857
1/2
✓ Branch 45 → 46 taken 1 time.
✗ Branch 45 → 52 not taken.
1 if (!absolute) {
1858
1/2
✓ Branch 47 → 48 taken 1 time.
✗ Branch 47 → 49 not taken.
1 if (!has_flag(a, 'p')) {
1859 1 return error_msg(ebuf, "No filename");
1860 }
1861 ✗ push_input_mode(e, e->command_mode);
1862 ✗ cmdline_set_text(&e->cmdline, "save ");
1863 ✗ return true;
1864 }
1865 ✗ if (buffer->readonly && !force) {
1866 ✗ return error_msg(ebuf, "Use -f to force saving read-only file");
1867 }
1868 }
1869
1870 23 mode_t old_mode = buffer->file.mode;
1871 23 bool hardlinks = false;
1872 23 struct stat st;
1873 23 bool stat_ok = !stat(absolute, &st);
1874
2/2
✓ Branch 56 → 57 taken 22 times.
✓ Branch 56 → 61 taken 1 time.
23 if (!stat_ok) {
1875
1/2
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 70 taken 22 times.
22 if (errno != ENOENT) {
1876 ✗ error_msg(ebuf, "stat failed for %s: %s", absolute, strerror(errno));
1877 ✗ goto error;
1878 }
1879 } else {
1880 1 if (
1881
1/2
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 66 taken 1 time.
1 absolute == buffer->abs_filename
1882 ✗ && !force
1883 ✗ && stat_changed(&buffer->file, &st)
1884 ) {
1885 ✗ error_msg (
1886 ebuf,
1887 "File has been modified by another process; "
1888 "use 'save -f' to force overwrite"
1889 );
1890 ✗ goto error;
1891 }
1892
1/2
✓ Branch 66 → 67 taken 1 time.
✗ Branch 66 → 69 not taken.
1 if (S_ISDIR(st.st_mode)) {
1893 1 error_msg(ebuf, "Will not overwrite directory %s", absolute);
1894 1 goto error;
1895 }
1896 ✗ hardlinks = (st.st_nlink >= 2);
1897 }
1898
1899
1/2
✗ Branch 70 → 71 not taken.
✓ Branch 70 → 84 taken 22 times.
22 if (e->options.lock_files) {
1900 ✗ if (absolute == buffer->abs_filename) {
1901 ✗ if (!buffer->locked) {
1902 ✗ if (!lock_file(&e->locks_ctx, ebuf, absolute)) {
1903 ✗ if (!force) {
1904 ✗ error_msg(ebuf, "Can't lock file %s", absolute);
1905 ✗ goto error;
1906 }
1907 } else {
1908 ✗ buffer->locked = true;
1909 }
1910 }
1911 } else {
1912 ✗ if (!lock_file(&e->locks_ctx, ebuf, absolute)) {
1913 ✗ if (!force) {
1914 ✗ error_msg(ebuf, "Can't lock file %s", absolute);
1915 ✗ goto error;
1916 }
1917 } else {
1918 new_locked = true;
1919 }
1920 }
1921 }
1922
1923
1/2
✗ Branch 84 → 85 not taken.
✓ Branch 84 → 102 taken 22 times.
22 if (stat_ok) {
1924 ✗ if (absolute != buffer->abs_filename && !force) {
1925 ✗ error_msg(ebuf, "Use -f to overwrite %s", absolute);
1926 ✗ goto error;
1927 }
1928 // Allow chmod 755 etc.
1929 ✗ buffer->file.mode = st.st_mode;
1930 }
1931
1932 ✗ if (
1933 stat_ok
1934 ✗ && buffer->options.save_unmodified != SAVE_FULL
1935 ✗ && !stat_changed(&buffer->file, &st)
1936 ✗ && st.st_uid == buffer->file.uid
1937 ✗ && st.st_gid == buffer->file.gid
1938 ✗ && !buffer_modified(buffer)
1939 ✗ && absolute == buffer->abs_filename
1940 ✗ && encoding == buffer->encoding
1941 ✗ && crlf == buffer->crlf_newlines
1942 ✗ && bom == buffer->bom
1943 ✗ && save_unmodified_buffer(buffer, absolute) == 0
1944 ) {
1945 ✗ BUG_ON(new_locked);
1946 return true;
1947 }
1948
1949 22 FileSaveContext ctx = {
1950 .ebuf = ebuf,
1951 .encoding = encoding,
1952 22 .new_file_mode = e->new_file_mode,
1953 .crlf = crlf,
1954 .write_bom = bom,
1955 .hardlinks = hardlinks,
1956 };
1957
1958
1/2
✗ Branch 103 → 104 not taken.
✓ Branch 103 → 105 taken 22 times.
22 if (!save_buffer(buffer, absolute, &ctx)) {
1959 ✗ goto error;
1960 }
1961
1962 22 buffer->saved_change = buffer->cur_change;
1963 22 buffer->readonly = false;
1964 22 buffer->temporary = false;
1965 22 buffer->crlf_newlines = crlf;
1966 22 buffer->bom = bom;
1967
2/2
✓ Branch 105 → 106 taken 2 times.
✓ Branch 105 → 107 taken 20 times.
22 if (requested_encoding) {
1968 2 buffer->encoding = encoding;
1969 }
1970
1971
1/2
✓ Branch 107 → 108 taken 22 times.
✗ Branch 107 → 112 not taken.
22 if (absolute != buffer->abs_filename) {
1972
1/2
✗ Branch 108 → 109 not taken.
✓ Branch 108 → 110 taken 22 times.
22 if (buffer->locked) {
1973 // Filename changes, release old file lock
1974 ✗ unlock_file(&e->locks_ctx, ebuf, buffer->abs_filename);
1975 }
1976 22 buffer->locked = new_locked;
1977
1978 22 free(buffer->abs_filename);
1979 22 buffer->abs_filename = absolute;
1980 22 buffer_update_short_filename(buffer, e->home_dir);
1981 22 e->screen_update |= UPDATE_TERM_TITLE;
1982
1983 // Filename change is not detected (only buffer_modified() change)
1984 22 buffer_mark_tabbars_changed(buffer);
1985 }
1986
1987
3/4
✓ Branch 112 → 113 taken 20 times.
✓ Branch 112 → 124 taken 2 times.
✓ Branch 114 → 115 taken 20 times.
✗ Branch 114 → 124 not taken.
22 if (!old_mode && buffer_filetype_is_none(buffer)) {
1988 // New file and most likely user has not changed the filetype
1989
1/2
✗ Branch 116 → 117 not taken.
✓ Branch 116 → 124 taken 20 times.
20 if (buffer_detect_filetype(buffer, &e->filetypes)) {
1990 ✗ set_file_options(e, buffer);
1991 ✗ set_editorconfig_options(buffer);
1992 ✗ buffer_update_syntax(e, buffer);
1993 }
1994 }
1995
1996 return true;
1997
1998 ✗ error:
1999
0/2
✗ Branch 120 → 121 not taken.
✗ Branch 120 → 122 not taken.
1 if (new_locked) {
2000 ✗ unlock_file(&e->locks_ctx, ebuf, absolute);
2001 }
2002
1/2
✓ Branch 122 → 123 taken 1 time.
✗ Branch 122 → 124 not taken.
1 if (absolute != buffer->abs_filename) {
2003 1 free(absolute);
2004 }
2005 return false;
2006 }
2007
2008 1 static bool cmd_scroll_down(EditorState *e, const CommandArgs *a)
2009 {
2010 1 BUG_ON(a->nr_args);
2011 1 View *view = e->view;
2012 1 view->vy++;
2013
2014
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 9 not taken.
1 if (has_flag(a, 'M')) {
2015 return true; // Don't move cursor, even at scroll margin
2016 }
2017
2018 1 long margin = window_get_scroll_margin(e->window);
2019
1/2
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 9 not taken.
1 if (view->cy < view->vy + margin) {
2020 1 move_down(view, 1);
2021 }
2022
2023 return true;
2024 }
2025
2026 1 static bool cmd_scroll_pgdown(EditorState *e, const CommandArgs *a)
2027 {
2028 1 BUG_ON(a->nr_args);
2029 1 View *view = e->view;
2030 1 handle_selection_flags(view, a);
2031
2032 1 const long nlines = view->buffer->nl;
2033 1 const long edit_h = e->window->edit_h;
2034 1 const long max = (nlines - edit_h) + 1;
2035 1 long count;
2036
2037
2/4
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 11 not taken.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
1 if (view->vy < max && max > 0) {
2038 1 bool half = has_flag(a, 'h');
2039 1 unsigned int shift = half & 1;
2040 1 count = (edit_h - 1) >> shift;
2041
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
1 if (view->vy + count > max) {
2042 ✗ count = max - view->vy;
2043 }
2044 1 view->vy += count;
2045 ✗ } else if (view->cy < nlines && !has_flag(a, 'M')) {
2046 ✗ count = nlines - view->cy;
2047 } else {
2048 return true;
2049 }
2050
2051 1 move_down(view, count);
2052 1 return true;
2053 }
2054
2055 1 static bool cmd_scroll_pgup(EditorState *e, const CommandArgs *a)
2056 {
2057 1 BUG_ON(a->nr_args);
2058 1 View *view = e->view;
2059 1 handle_selection_flags(view, a);
2060
2061 1 long count;
2062
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 8 taken 1 time.
1 if (view->vy > 0) {
2063 ✗ bool half = has_flag(a, 'h');
2064 ✗ unsigned int shift = half & 1;
2065 ✗ count = MIN((e->window->edit_h - 1) >> shift, view->vy);
2066 ✗ view->vy -= count;
2067
1/4
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 13 taken 1 time.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 13 not taken.
1 } else if (view->cy > 0 && !has_flag(a, 'M')) {
2068 count = view->cy;
2069 } else {
2070 return true;
2071 }
2072
2073 ✗ move_up(view, count);
2074 ✗ return true;
2075 }
2076
2077 1 static bool cmd_scroll_up(EditorState *e, const CommandArgs *a)
2078 {
2079 1 BUG_ON(a->nr_args);
2080 1 View *view = e->view;
2081 1 view->vy -= (view->vy > 0);
2082
2083
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 9 not taken.
1 if (has_flag(a, 'M')) {
2084 return true; // Don't move cursor, even at scroll margin
2085 }
2086
2087 1 const Window *window = e->window;
2088 1 long margin = window_get_scroll_margin(window);
2089
1/2
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 9 not taken.
1 if (view->vy + (window->edit_h - margin) <= view->cy) {
2090 1 move_up(view, 1);
2091 }
2092
2093 return true;
2094 }
2095
2096 8 static bool cmd_search(EditorState *e, const CommandArgs *a)
2097 {
2098 8 const char *pattern = a->args[0];
2099 8 char npflag = cmdargs_pick_winning_flag(a, "np");
2100 8 bool use_word_under_cursor = has_flag(a, 'w');
2101 8 ErrorBuffer *ebuf = &e->err;
2102
2103
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 6 times.
8 if (!!npflag + use_word_under_cursor + !!pattern > 1) {
2104 2 return error_msg (
2105 ebuf,
2106 "flags [-n|-p|-w] and [pattern] argument are mutually exclusive"
2107 );
2108 }
2109
2110 6 View *view = e->view;
2111 6 char pattbuf[4096];
2112
2113
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 14 taken 5 times.
6 if (use_word_under_cursor) {
2114 1 StringView word = view_get_word_under_cursor(view);
2115
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 13 not taken.
1 if (word.length == 0) {
2116 1 return false; // Error message wouldn't be very useful here
2117 }
2118 1 const RegexpWordBoundaryTokens *rwbt = &e->regexp_word_tokens;
2119 1 const size_t n = rwbt->len;
2120
1/2
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 11 not taken.
1 if (unlikely(word.length >= sizeof(pattbuf) - (n * 2))) {
2121 1 return error_msg(ebuf, "word under cursor too long");
2122 }
2123 ✗ xmempcpy3(pattbuf, rwbt->start, n, word.data, word.length, rwbt->end, n + 1);
2124 ✗ pattern = pattbuf;
2125 }
2126
2127 5 SearchState *search = &e->search;
2128 5 search->reverse = has_flag(a, 'r');
2129
2130
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 5 times.
5 if (!npflag && !pattern) {
2131 ✗ push_input_mode(e, e->search_mode);
2132 ✗ return true;
2133 }
2134
2135 5 handle_selection_flags(view, a);
2136
2137 5 SearchCaseSensitivity cs = e->options.case_sensitive_search;
2138
1/4
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
✗ Branch 20 → 23 not taken.
✓ Branch 20 → 24 taken 5 times.
5 switch (cmdargs_pick_winning_flag(a, "ais")) {
2139 ✗ case 'a': cs = CSS_AUTO; break;
2140 ✗ case 'i': cs = CSS_FALSE; break;
2141 ✗ case 's': cs = CSS_TRUE; break;
2142 }
2143
2144
3/3
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 1 time.
✓ Branch 24 → 27 taken 3 times.
5 switch (npflag) {
2145 1 case 'n': return search_next(view, search, ebuf, cs);
2146 1 case 'p': return search_prev(view, search, ebuf, cs);
2147 }
2148
2149 3 BUG_ON(!pattern);
2150 3 String escaped = string_new(0);
2151
2152
2/4
✓ Branch 30 → 31 taken 3 times.
✗ Branch 30 → 37 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 37 taken 3 times.
3 if (!use_word_under_cursor && has_flag(a, 'e')) {
2153 ✗ StringView pat = strview(pattern);
2154 ✗ if (strview_contains_char_type(pat, ASCII_REGEX)) {
2155 ✗ escaped = regexp_escape(pat);
2156 ✗ pattern = string_borrow_cstring(&escaped);
2157 }
2158 }
2159
2160
2/2
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 40 taken 2 times.
3 if (!has_flag(a, 'H')) {
2161 1 history_append(&e->search_history, pattern);
2162 }
2163
2164 3 search_set_regexp(search, pattern);
2165 3 string_free(&escaped);
2166
3/4
✓ Branch 43 → 44 taken 3 times.
✗ Branch 43 → 47 not taken.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 45 → 47 taken 2 times.
4 return has_flag(a, 'x') || do_search_next(view, search, ebuf, cs, use_word_under_cursor);
2167 }
2168
2169 ✗ static bool cmd_select_block(EditorState *e, const CommandArgs *a)
2170 {
2171 ✗ BUG_ON(a->nr_args);
2172 ✗ select_block(e->view);
2173
2174 // TODO: return false if select_block() doesn't select anything?
2175 ✗ return true;
2176 }
2177
2178 3 static bool cmd_select(EditorState *e, const CommandArgs *a)
2179 {
2180 3 View *view = e->view;
2181
1/2
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 5 not taken.
3 SelectionType sel = has_flag(a, 'l') ? SELECT_LINES : SELECT_CHARS;
2182 3 bool keep = has_flag(a, 'k');
2183
2/6
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 10 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 3 times.
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 10 not taken.
3 if (!keep && view->selection && view->selection == sel) {
2184 ✗ sel = SELECT_NONE;
2185 }
2186
2187 3 view->select_mode = sel;
2188 3 view_set_selection_type(view, sel);
2189 3 return true;
2190 }
2191
2192 60 static bool cmd_set(EditorState *e, const CommandArgs *a)
2193 {
2194 60 bool global = has_flag(a, 'g');
2195 60 bool local = has_flag(a, 'l');
2196 60 bool quiet = has_flag(a, 'q');
2197
2198
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 12 taken 60 times.
60 if (!e->buffer) {
2199 ✗ if (unlikely(local)) {
2200 ✗ return quiet || error_msg(&e->err, "Flag -l makes no sense in config files");
2201 }
2202 global = true;
2203 }
2204
2205 60 char **args = a->args;
2206 60 size_t count = a->nr_args;
2207
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 16 taken 58 times.
60 if (count == 1) {
2208 2 bool ok = set_bool_option(e, args[0], local, global, quiet);
2209
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 30 taken 2 times.
2 WARN_ON(quiet && !ok);
2210 return ok;
2211 }
2212
2/2
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 26 taken 56 times.
58 if (count & 1) {
2213
3/4
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 21 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
3 return quiet || error_msg(&e->err, "One or even number of arguments expected");
2214 }
2215
2216 size_t errors = 0;
2217
2/2
✓ Branch 26 → 22 taken 56 times.
✓ Branch 26 → 27 taken 56 times.
112 for (size_t i = 0; i < count; i += 2) {
2218
2/2
✓ Branch 23 → 24 taken 22 times.
✓ Branch 23 → 25 taken 34 times.
56 if (!set_option(e, args[i], args[i + 1], local, global, quiet)) {
2219 22 errors++;
2220 }
2221 }
2222
2223
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 56 times.
56 WARN_ON(quiet && errors);
2224 56 return !errors;
2225 }
2226
2227 18 static bool cmd_setenv(EditorState *e, const CommandArgs *a)
2228 {
2229 18 const char *name = a->args[0];
2230
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 17 times.
18 if (unlikely(streq(name, "DTE_VERSION"))) {
2231 1 return error_msg(&e->err, "$DTE_VERSION cannot be changed");
2232 }
2233
2234 17 const size_t nr_args = a->nr_args;
2235 17 int res;
2236
2/2
✓ Branch 4 → 5 taken 13 times.
✓ Branch 4 → 6 taken 4 times.
17 if (nr_args == 2) {
2237 13 res = setenv(name, a->args[1], 1);
2238 } else {
2239 4 BUG_ON(nr_args != 1);
2240 4 res = unsetenv(name);
2241 }
2242
2243
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 15 taken 17 times.
17 if (likely(res == 0)) {
2244 return true;
2245 }
2246
2247 ✗ if (errno == EINVAL) {
2248 ✗ return error_msg(&e->err, "Invalid environment variable name '%s'", name);
2249 }
2250
2251 ✗ return error_msg_errno(&e->err, nr_args == 2 ? "setenv" : "unsetenv");
2252 }
2253
2254 11 static bool cmd_indent(EditorState *e, const CommandArgs *a)
2255 {
2256 11 const char *arg = a->args[0];
2257 11 int levels = 1;
2258
4/4
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 6 taken 5 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 5 times.
11 if (arg && !str_to_int(arg, &levels)) {
2259 1 return error_msg(&e->err, "Invalid number: %s", arg);
2260 }
2261
2262 10 bool reduce = has_flag(a, 'r');
2263
3/4
✓ Branch 7 → 8 taken 10 times.
✗ Branch 7 → 10 not taken.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 10 taken 7 times.
10 indent_lines(e->view, (levels && reduce) ? -levels : levels);
2264 10 return true;
2265 }
2266
2267 9 static bool cmd_show(EditorState *e, const CommandArgs *a)
2268 {
2269 9 bool write_to_cmdline = has_flag(a, 'c');
2270 9 size_t nr_args = a->nr_args;
2271
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 8 times.
9 if (write_to_cmdline && nr_args < 2) {
2272 1 return error_msg(&e->err, "\"show -c\" requires 2 arguments");
2273 }
2274
2275
1/2
✓ Branch 5 → 6 taken 8 times.
✗ Branch 5 → 8 not taken.
8 const char *type = (nr_args > 0) ? a->args[0] : NULL;
2276
2/2
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 8 taken 2 times.
8 const char *key = (nr_args > 1) ? a->args[1] : NULL;
2277 8 return show(e, type, key, write_to_cmdline);
2278 }
2279
2280 2 static bool cmd_suspend(EditorState *e, const CommandArgs *a)
2281 {
2282 2 BUG_ON(a->nr_args);
2283
1/2
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 6 not taken.
2 if (e->flags & EFLAG_HEADLESS) {
2284 2 return error_msg(&e->err, "unavailable in headless mode");
2285 }
2286
2287 ✗ if (e->status == EDITOR_INITIALIZING) {
2288 ✗ LOG_WARNING("suspend request ignored");
2289 ✗ return false;
2290 }
2291
2292 ✗ if (e->session_leader) {
2293 ✗ return error_msg(&e->err, "Session leader can't suspend");
2294 }
2295
2296 ✗ ui_end(&e->terminal, false);
2297 ✗ term_cooked();
2298 ✗ need_term_reset_on_fatal_error = 0;
2299 ✗ LOG_INFO("suspending");
2300
2301 ✗ bool suspended = !kill(0, SIGSTOP);
2302 ✗ if (suspended) {
2303 ✗ LOG_INFO("resumed");
2304 } else {
2305 ✗ error_msg_errno(&e->err, "kill");
2306 }
2307
2308 ✗ need_term_reset_on_fatal_error = 1;
2309 ✗ term_raw();
2310 ✗ ui_start(e);
2311 ✗ return suspended;
2312 }
2313
2314 2 static bool cmd_tag(EditorState *e, const CommandArgs *a)
2315 {
2316
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 1 time.
2 if (has_flag(a, 'r')) {
2317 1 bookmark_pop(&e->bookmarks, e->window, &e->buffers);
2318 1 return true;
2319 }
2320
2321 1 size_t nargs = a->nr_args;
2322 1 StringView word_under_cursor;
2323
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 9 taken 1 time.
1 if (nargs == 0) {
2324 ✗ word_under_cursor = view_get_word_under_cursor(e->view);
2325 ✗ if (word_under_cursor.length == 0) {
2326 return false;
2327 }
2328 }
2329
2330 1 char abc = cmdargs_pick_winning_flag(a, "ABC");
2331
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
1 size_t idx = abc ? abc - 'A' : e->options.msg_tag;
2332 1 MessageList *msgs = &e->messages[idx];
2333 1 clear_messages(msgs);
2334
2335 1 TagFile *tagfile = &e->tagfile;
2336 1 ErrorBuffer *ebuf = &e->err;
2337
1/2
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 24 not taken.
1 if (!load_tag_file(tagfile, ebuf)) {
2338 return false;
2339 }
2340
2341 1 const char *filename = e->buffer->abs_filename;
2342
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 1 time.
1 if (nargs == 0) {
2343 ✗ tag_lookup(tagfile, msgs, ebuf, word_under_cursor, filename);
2344 }
2345
2346
2/2
✓ Branch 21 → 19 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
2 for (size_t i = 0; i < nargs; i++) {
2347 1 StringView tagname = strview(a->args[i]);
2348 1 tag_lookup(tagfile, msgs, ebuf, tagname, filename);
2349 }
2350
2351 1 activate_current_message_save(msgs, &e->bookmarks, e->view, ebuf);
2352 1 return (msgs->array.count > 0);
2353 }
2354
2355 ✗ static bool cmd_title(EditorState *e, const CommandArgs *a)
2356 {
2357 ✗ Buffer *buffer = e->buffer;
2358 ✗ if (buffer->abs_filename) {
2359 ✗ return error_msg(&e->err, "saved buffers can't be retitled");
2360 }
2361
2362 ✗ buffer_set_display_filename(buffer, xstrdup(a->args[0]));
2363 ✗ buffer_mark_tabbars_changed(buffer);
2364 ✗ e->screen_update |= UPDATE_TERM_TITLE;
2365 ✗ return true;
2366 }
2367
2368 16 static bool cmd_toggle(EditorState *e, const CommandArgs *a)
2369 {
2370 16 bool global = has_flag(a, 'g');
2371 16 bool verbose = has_flag(a, 'v');
2372 16 const char *option_name = a->args[0];
2373 16 size_t nr_values = a->nr_args - 1;
2374
1/2
✓ Branch 4 → 5 taken 16 times.
✗ Branch 4 → 6 not taken.
16 if (nr_values == 0) {
2375 16 return toggle_option(e, option_name, global, verbose);
2376 }
2377
2378 ✗ char **values = a->args + 1;
2379 ✗ return toggle_option_values(e, option_name, global, verbose, values, nr_values);
2380 }
2381
2382 1020 static bool cmd_undo(EditorState *e, const CommandArgs *a)
2383 {
2384 1020 View *view = e->view;
2385 1020 bool move_only = has_flag(a, 'm');
2386
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1020 times.
1020 if (move_only) {
2387 ✗ const Change *change = view->buffer->cur_change;
2388 ✗ if (!change->next) {
2389 // If there's only 1 change, there's nothing meaningful to move to
2390 return false;
2391 }
2392 ✗ block_iter_goto_offset(&view->cursor, change->offset);
2393 ✗ view_reset_preferred_x(view);
2394 ✗ return true;
2395 }
2396
2397
3/4
✓ Branch 9 → 10 taken 44 times.
✓ Branch 9 → 13 taken 976 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 44 times.
1020 return undo(view, &e->err) && unselect(view);
2398 }
2399
2400 1 static bool cmd_unselect(EditorState *e, const CommandArgs *a)
2401 {
2402 1 BUG_ON(a->nr_args);
2403 1 return unselect(e->view);
2404 }
2405
2406 16 static bool cmd_up(EditorState *e, const CommandArgs *a)
2407 {
2408 16 handle_selection_flags(e->view, a);
2409 16 move_up(e->view, 1);
2410 16 return true;
2411 }
2412
2413 3 static bool cmd_view(EditorState *e, const CommandArgs *a)
2414 {
2415 3 Window *window = e->window;
2416 3 BUG_ON(window->views.count == 0);
2417 3 const char *arg = a->args[0];
2418 3 size_t idx;
2419
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 3 times.
3 if (streq(arg, "last")) {
2420 ✗ idx = window->views.count - 1;
2421 } else {
2422
4/4
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 1 time.
3 if (!str_to_size(arg, &idx) || idx == 0) {
2423 2 return error_msg(&e->err, "Invalid view index: %s", arg);
2424 }
2425 1 idx = MIN(idx, window->views.count) - 1;
2426 }
2427 1 set_view(window->views.ptrs[idx]);
2428 1 return true;
2429 }
2430
2431 4 static bool cmd_wclose(EditorState *e, const CommandArgs *a)
2432 {
2433 4 View *first_uncloseable;
2434 4 size_t n = window_count_uncloseable_views(e->window, &first_uncloseable);
2435 4 bool force = has_flag(a, 'f');
2436
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 2 times.
4 if (n == 0 || force) {
2437 2 goto close;
2438 }
2439
2440 2 bool prompt = has_flag(a, 'p');
2441 2 set_view(first_uncloseable);
2442
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 1 time.
2 if (!prompt) {
2443 1 return error_msg (
2444 &e->err,
2445 "Save modified files or run 'wclose -f' to close "
2446 "window without saving"
2447 );
2448 }
2449
2450
1/2
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 12 not taken.
1 if (unlikely(e->flags & EFLAG_HEADLESS)) {
2451 1 return error_msg(&e->err, "-p flag unavailable in headless mode");
2452 }
2453
2454 ✗ const char *plural = (n != 1) ? "s" : "";
2455 ✗ char question[128];
2456
2457 ✗ xsnprintf (
2458 question, sizeof question,
2459 "Close window without saving %zu modified buffer%s? [y/N]",
2460 n, plural
2461 );
2462
2463 ✗ if (dialog_prompt(e, question, "ny") != 'y') {
2464 return false;
2465 }
2466
2467 ✗ close:
2468 2 window_close(e->window);
2469 2 return true;
2470 }
2471
2472 1 static bool cmd_wflip(EditorState *e, const CommandArgs *a)
2473 {
2474 1 BUG_ON(a->nr_args);
2475 1 Frame *frame = e->window->frame;
2476
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
1 if (!frame->parent) {
2477 return false;
2478 }
2479 ✗ frame->parent->vertical ^= 1;
2480 ✗ e->screen_update |= UPDATE_ALL_WINDOWS;
2481 ✗ return true;
2482 }
2483
2484 1 static bool cmd_wnext(EditorState *e, const CommandArgs *a)
2485 {
2486 1 BUG_ON(a->nr_args);
2487 1 e->window = window_next(e->window);
2488 1 set_view(e->window->view);
2489 1 e->screen_update |= UPDATE_ALL;
2490 1 frame_debug(e->root_frame);
2491 1 return true;
2492 }
2493
2494 1 static bool cmd_word_bwd(EditorState *e, const CommandArgs *a)
2495 {
2496 1 handle_selection_flags(e->view, a);
2497 1 bool skip_non_word = has_flag(a, 's');
2498 1 word_bwd(&e->view->cursor, skip_non_word);
2499 1 view_reset_preferred_x(e->view);
2500 1 return true;
2501 }
2502
2503 4 static bool cmd_word_fwd(EditorState *e, const CommandArgs *a)
2504 {
2505 4 handle_selection_flags(e->view, a);
2506 4 bool skip_non_word = has_flag(a, 's');
2507 4 word_fwd(&e->view->cursor, skip_non_word);
2508 4 view_reset_preferred_x(e->view);
2509 4 return true;
2510 }
2511
2512 1 static bool cmd_wprev(EditorState *e, const CommandArgs *a)
2513 {
2514 1 BUG_ON(a->nr_args);
2515 1 e->window = window_prev(e->window);
2516 1 set_view(e->window->view);
2517 1 e->screen_update |= UPDATE_ALL;
2518 1 frame_debug(e->root_frame);
2519 1 return true;
2520 }
2521
2522 4 static bool cmd_wrap_paragraph(EditorState *e, const CommandArgs *a)
2523 {
2524 4 const char *arg = a->args[0];
2525 4 unsigned int width = e->buffer->options.text_width;
2526
2527
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 9 taken 1 time.
4 if (arg) {
2528
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 2 times.
3 if (!str_to_uint(arg, &width)) {
2529 1 return error_msg(&e->err, "invalid paragraph width: %s", arg);
2530 }
2531
2532 2 unsigned int max = TEXT_WIDTH_MAX;
2533
3/4
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 8 not taken.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 1 time.
2 if (width < 1 || width > max) {
2534 1 return error_msg(&e->err, "width must be between 1 and %u", max);
2535 }
2536 }
2537
2538 2 wrap_paragraph(e->view, width);
2539 2 return true;
2540 }
2541
2542 2 static bool cmd_wresize(EditorState *e, const CommandArgs *a)
2543 {
2544 2 Window *window = e->window;
2545
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 19 taken 1 time.
2 if (!window->frame->parent) {
2546 // Only window
2547 return false;
2548 }
2549
2550 1 ResizeDirection dir = RESIZE_DIRECTION_AUTO;
2551
1/3
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
✓ Branch 4 → 7 taken 1 time.
1 switch (cmdargs_pick_winning_flag(a, "hv")) {
2552 ✗ case 'h':
2553 ✗ dir = RESIZE_DIRECTION_HORIZONTAL;
2554 ✗ break;
2555 ✗ case 'v':
2556 ✗ dir = RESIZE_DIRECTION_VERTICAL;
2557 ✗ break;
2558 }
2559
2560 1 const char *arg = a->args[0];
2561
1/2
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 16 not taken.
1 if (arg) {
2562 1 int n;
2563
1/2
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 12 not taken.
1 if (!str_to_int(arg, &n)) {
2564 1 return error_msg(&e->err, "Invalid resize value: %s", arg);
2565 }
2566 ✗ if (arg[0] == '+' || arg[0] == '-') {
2567 ✗ frame_add_to_size(window->frame, dir, n);
2568 } else {
2569 ✗ frame_resize(window->frame, dir, n);
2570 }
2571 } else {
2572 ✗ frame_equalize_sizes(window->frame->parent);
2573 }
2574
2575 ✗ e->screen_update |= UPDATE_ALL_WINDOWS;
2576 ✗ frame_debug(e->root_frame);
2577 // TODO: return false if resize failed?
2578 ✗ return true;
2579 }
2580
2581 3 static bool cmd_wsplit(EditorState *e, const CommandArgs *a)
2582 {
2583 3 bool before = has_flag(a, 'b');
2584
1/4
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 3 times.
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 7 not taken.
3 bool use_glob = has_flag(a, 'g') && a->nr_args > 0;
2585 3 bool vertical = has_flag(a, 'h');
2586 3 bool root = has_flag(a, 'r');
2587 3 bool temporary = has_flag(a, 't');
2588
3/4
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 13 taken 2 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 15 taken 1 time.
3 bool empty = temporary || has_flag(a, 'n');
2589
2590
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 1 time.
2 if (unlikely(empty && a->nr_args > 0)) {
2591 1 return error_msg(&e->err, "flags [-n|-t] can't be used with [filename] arguments");
2592 }
2593
2594 2 char **paths = a->args;
2595 2 glob_t globbuf;
2596
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 2 times.
2 if (use_glob) {
2597 ✗ if (!xglob(&e->err, a->args, &globbuf)) {
2598 return false;
2599 }
2600 ✗ paths = globbuf.gl_pathv;
2601 }
2602
2603 2 Frame *frame;
2604
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 2 times.
2 if (root) {
2605 ✗ frame = frame_split_root(e, vertical, before);
2606 } else {
2607 2 frame = frame_split(e->window, vertical, before);
2608 }
2609
2610 2 View *save = e->view;
2611 2 e->window = frame->window;
2612 2 e->view = NULL;
2613 2 e->buffer = NULL;
2614 2 e->screen_update |= UPDATE_ALL;
2615
2616 2 View *view;
2617
2/2
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 25 taken 1 time.
2 if (empty) {
2618 1 view = window_open_new_file(e->window);
2619 1 view->buffer->temporary = temporary;
2620
1/2
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 1 time.
1 } else if (paths[0]) {
2621 ✗ view = window_open_files(e->window, paths, NULL);
2622 } else {
2623 1 view = window_add_buffer(e->window, save->buffer);
2624 1 view->cursor = save->cursor;
2625 1 set_view(view);
2626 }
2627
2628
1/2
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 2 times.
2 if (use_glob) {
2629 ✗ globfree(&globbuf);
2630 }
2631
2632
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 34 taken 2 times.
2 if (!view) {
2633 // Open failed, remove new window
2634 ✗ frame_remove(e, e->window->frame);
2635 ✗ e->view = save;
2636 ✗ e->buffer = save->buffer;
2637 ✗ e->window = save->window;
2638 }
2639
2640 2 frame_debug(e->root_frame);
2641 2 return !!view;
2642 }
2643
2644 1 static bool cmd_wswap(EditorState *e, const CommandArgs *a)
2645 {
2646 1 BUG_ON(a->nr_args);
2647 1 Frame *frame = e->window->frame;
2648 1 Frame *parent = frame->parent;
2649
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 9 not taken.
1 if (!parent) {
2650 return false;
2651 }
2652
2653 1 PointerArray *pframes = &parent->frames;
2654 1 size_t current = ptr_array_xindex(pframes, frame);
2655 1 size_t next = wrapping_increment(current, pframes->count);
2656 1 ptr_array_swap(pframes, current, next);
2657 1 e->screen_update |= UPDATE_ALL_WINDOWS;
2658 1 return true;
2659 }
2660
2661 enum {
2662 // Short aliases for CommandOptions:
2663 NA = 0,
2664 RC = CMDOPT_ALLOW_IN_RC,
2665 NFAA = CMDOPT_NO_FLAGS_AFTER_ARGS,
2666 };
2667
2668 static const Command cmds[] = {
2669 {"alias", "", RC | NFAA, 1, 2, cmd_alias},
2670 {"bind", "T=cnqs", RC | NFAA, 1, 2, cmd_bind},
2671 {"blkdown", "cl", NA, 0, 0, cmd_blkdown},
2672 {"blkup", "cl", NA, 0, 0, cmd_blkup},
2673 {"bof", "cl", NA, 0, 0, cmd_bof},
2674 {"bol", "clrst", NA, 0, 0, cmd_bol},
2675 {"bolsf", "cl", NA, 0, 0, cmd_bolsf},
2676 {"bookmark", "rv", NA, 0, 0, cmd_bookmark},
2677 {"case", "lu", NA, 0, 0, cmd_case},
2678 {"cd", "v", RC, 1, 1, cmd_cd},
2679 {"center-view", "", NA, 0, 0, cmd_center_view},
2680 {"clear", "Ii", NA, 0, 0, cmd_clear},
2681 {"close", "fpqw", NA, 0, 0, cmd_close},
2682 {"command", "", NFAA, 0, 1, cmd_command},
2683 {"compile", "1ABCps", NFAA, 2, -1, cmd_compile},
2684 {"copy", "bikp", NA, 0, 1, cmd_copy},
2685 {"cursor", "", RC, 0, 3, cmd_cursor},
2686 {"cut", "", NA, 0, 0, cmd_cut},
2687 {"def-mode", "Uu", RC, 1, 16, cmd_def_mode},
2688 {"delete", "", NA, 0, 0, cmd_delete},
2689 {"delete-eol", "n", NA, 0, 0, cmd_delete_eol},
2690 {"delete-line", "S", NA, 0, 0, cmd_delete_line},
2691 {"delete-word", "s", NA, 0, 0, cmd_delete_word},
2692 {"down", "cl", NA, 0, 0, cmd_down},
2693 {"eof", "cl", NA, 0, 0, cmd_eof},
2694 {"eol", "cl", NA, 0, 0, cmd_eol},
2695 {"eolsf", "cl", NA, 0, 0, cmd_eolsf},
2696 {"erase", "", NA, 0, 0, cmd_erase},
2697 {"erase-bol", "", NA, 0, 0, cmd_erase_bol},
2698 {"erase-word", "s", NA, 0, 0, cmd_erase_word},
2699 {"errorfmt", "ci", RC, 1, 2 + ERRORFMT_CAPTURE_MAX, cmd_errorfmt},
2700 {"exec", "e=i=lmno=pst", NFAA, 1, -1, cmd_exec},
2701 {"ft", "bcfi", RC | NFAA, 2, -1, cmd_ft},
2702 {"hi", "cq", RC | NFAA, 0, -1, cmd_hi},
2703 {"include", "bq", RC, 1, 1, cmd_include},
2704 {"indent", "r", NA, 0, 1, cmd_indent},
2705 {"insert", "km", NA, 1, 1, cmd_insert},
2706 {"join", "", NA, 0, 1, cmd_join},
2707 {"left", "cl", NA, 0, 0, cmd_left},
2708 {"line", "cl", NA, 1, 1, cmd_line},
2709 {"macro", "", NA, 1, 1, cmd_macro},
2710 {"match-bracket", "cl", NA, 0, 0, cmd_match_bracket},
2711 {"mode", "", RC, 1, 1, cmd_mode},
2712 {"move-tab", "", NA, 1, 1, cmd_move_tab},
2713 {"msg", "ABCnpw", NA, 0, 1, cmd_msg},
2714 {"new-line", "Iaim", NA, 0, 0, cmd_new_line},
2715 {"next", "", NA, 0, 0, cmd_next},
2716 {"open", "e=gt", NA, 0, -1, cmd_open},
2717 {"option", "r", RC | NFAA, 3, -1, cmd_option},
2718 {"paste", "acm", NA, 0, 0, cmd_paste},
2719 {"pgdown", "cl", NA, 0, 0, cmd_pgdown},
2720 {"pgup", "cl", NA, 0, 0, cmd_pgup},
2721 {"play", "k", NA, 0, 0, cmd_play},
2722 {"prev", "", NA, 0, 0, cmd_prev},
2723 {"quit", "CFHSfp", NA, 0, 1, cmd_quit},
2724 {"record", "crs", NA, 0, 0, cmd_record},
2725 {"redo", "", NA, 0, 1, cmd_redo},
2726 {"refresh", "", NA, 0, 0, cmd_refresh},
2727 {"reopen", "", NA, 0, 0, cmd_reopen},
2728 {"repeat", "", NFAA, 2, -1, cmd_repeat},
2729 {"replace", "bcegi", NA, 1, 2, cmd_replace},
2730 {"right", "cl", NA, 0, 0, cmd_right},
2731 {"save", "Bbde=fpu", NA, 0, 1, cmd_save},
2732 {"scroll-down", "M", NA, 0, 0, cmd_scroll_down},
2733 {"scroll-pgdown", "Mchl", NA, 0, 0, cmd_scroll_pgdown},
2734 {"scroll-pgup", "Mchl", NA, 0, 0, cmd_scroll_pgup},
2735 {"scroll-up", "M", NA, 0, 0, cmd_scroll_up},
2736 {"search", "Haceilnprswx", NA, 0, 1, cmd_search},
2737 {"select", "kl", NA, 0, 0, cmd_select},
2738 {"select-block", "", NA, 0, 0, cmd_select_block},
2739 {"set", "glq", RC, 1, -1, cmd_set},
2740 {"setenv", "", RC, 1, 2, cmd_setenv},
2741 {"show", "c", NA, 0, 2, cmd_show},
2742 {"suspend", "", NA, 0, 0, cmd_suspend},
2743 {"tag", "ABCr", NA, 0, -1, cmd_tag},
2744 {"title", "", NA, 1, 1, cmd_title},
2745 {"toggle", "gv", NA, 1, -1, cmd_toggle},
2746 {"undo", "m", NA, 0, 0, cmd_undo},
2747 {"unselect", "", NA, 0, 0, cmd_unselect},
2748 {"up", "cl", NA, 0, 0, cmd_up},
2749 {"view", "", NA, 1, 1, cmd_view},
2750 {"wclose", "fp", NA, 0, 0, cmd_wclose},
2751 {"wflip", "", NA, 0, 0, cmd_wflip},
2752 {"wnext", "", NA, 0, 0, cmd_wnext},
2753 {"word-bwd", "cls", NA, 0, 0, cmd_word_bwd},
2754 {"word-fwd", "cls", NA, 0, 0, cmd_word_fwd},
2755 {"wprev", "", NA, 0, 0, cmd_wprev},
2756 {"wrap-paragraph", "", NA, 0, 1, cmd_wrap_paragraph},
2757 {"wresize", "hv", NA, 0, 1, cmd_wresize},
2758 {"wsplit", "bghnrt", NA, 0, -1, cmd_wsplit},
2759 {"wswap", "", NA, 0, 0, cmd_wswap},
2760 };
2761
2762 269 static bool allow_macro_recording(const Command *cmd, char **args)
2763 {
2764 269 const CommandFunc fn = cmd->cmd;
2765
2/2
✓ Branch 2 → 3 taken 245 times.
✓ Branch 2 → 17 taken 24 times.
269 if (
2766
2/4
✓ Branch 3 → 4 taken 245 times.
✗ Branch 3 → 17 not taken.
✓ Branch 4 → 5 taken 245 times.
✗ Branch 4 → 17 not taken.
245 fn == cmd_macro || fn == cmd_record || fn == cmd_play
2767
3/4
✓ Branch 5 → 6 taken 221 times.
✓ Branch 5 → 17 taken 24 times.
✓ Branch 6 → 7 taken 221 times.
✗ Branch 6 → 17 not taken.
245 || fn == cmd_command || fn == cmd_mode
2768 ) {
2769 return false;
2770 }
2771
2772
2/2
✓ Branch 7 → 8 taken 168 times.
✓ Branch 7 → 17 taken 53 times.
221 if (fn == cmd_search) {
2773 168 char **args_copy = copy_string_array(args, string_array_length(args));
2774 168 CommandArgs a = cmdargs_new(args_copy);
2775 168 bool ret = true;
2776
1/2
✓ Branch 10 → 11 taken 168 times.
✗ Branch 10 → 15 not taken.
168 if (do_parse_args(cmd, &a) == ARGERR_NONE) {
2777
4/4
✓ Branch 11 → 12 taken 120 times.
✓ Branch 11 → 15 taken 48 times.
✓ Branch 13 → 14 taken 48 times.
✓ Branch 13 → 15 taken 72 times.
168 if (a.nr_args == 0 && !cmdargs_has_any_flag(&a, "npw")) {
2778 // If command is "search" with no [pattern] argument or
2779 // [-n|-p|-w] flags, the command would put the editor into
2780 // search mode, which shouldn't be recorded.
2781 48 ret = false;
2782 }
2783 }
2784 168 free_string_array(args_copy);
2785 168 return ret;
2786 }
2787
2788 if (fn == cmd_exec) {
2789 // TODO: don't record -o with open/tag/eval/msg
2790 }
2791
2792 return true;
2793 }
2794
2795 24 UNITTEST {
2796 // NOLINTBEGIN(bugprone-assert-side-effect)
2797 24 CHECK_CMDS_ARRAY(cmds);
2798
2799 24 const char *args[4] = {NULL};
2800 24 char **argp = (char**)args;
2801 24 const Command *cmd = find_normal_command("left");
2802 24 BUG_ON(!cmd);
2803 24 BUG_ON(!allow_macro_recording(cmd, argp));
2804
2805 24 cmd = find_normal_command("exec");
2806 24 BUG_ON(!cmd);
2807 24 BUG_ON(!allow_macro_recording(cmd, argp));
2808
2809 24 cmd = find_normal_command("command");
2810 24 BUG_ON(!cmd);
2811 24 BUG_ON(allow_macro_recording(cmd, argp));
2812
2813 24 cmd = find_normal_command("macro");
2814 24 BUG_ON(!cmd);
2815 24 BUG_ON(allow_macro_recording(cmd, argp));
2816
2817 24 cmd = find_normal_command("search");
2818 24 BUG_ON(!cmd);
2819 24 BUG_ON(allow_macro_recording(cmd, argp));
2820 24 args[0] = "xyz";
2821 24 BUG_ON(!allow_macro_recording(cmd, argp));
2822 24 args[0] = "-n";
2823 24 BUG_ON(!allow_macro_recording(cmd, argp));
2824 24 args[0] = "-p";
2825 24 BUG_ON(!allow_macro_recording(cmd, argp));
2826 24 args[0] = "-w";
2827 24 BUG_ON(!allow_macro_recording(cmd, argp));
2828 24 args[0] = "-Hr";
2829 24 BUG_ON(allow_macro_recording(cmd, argp));
2830 24 args[1] = "str";
2831 24 BUG_ON(!allow_macro_recording(cmd, argp));
2832 // NOLINTEND(bugprone-assert-side-effect)
2833 24 }
2834
2835 5 static void record_command(EditorState *e, const Command *cmd, char **args)
2836 {
2837
1/2
✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 5 not taken.
5 if (!allow_macro_recording(cmd, args)) {
2838 return;
2839 }
2840 5 macro_command_hook(&e->macro, cmd->name, args);
2841 }
2842
2843 6941 const Command *find_normal_command(const char *name)
2844 {
2845 6941 return BSEARCH(name, cmds, command_cmp);
2846 }
2847
2848 const CommandSet normal_commands = {
2849 .lookup = find_normal_command,
2850 .macro_record = record_command,
2851 };
2852
2853 123 const char *find_normal_alias(const EditorState *e, const char *name)
2854 {
2855 123 return find_alias(&e->aliases, name);
2856 }
2857
2858 187 bool handle_normal_command(EditorState *e, const char *cmd, bool allow_recording)
2859 {
2860 187 CommandRunner runner = normal_mode_cmdrunner(e);
2861 187 BUG_ON(runner.flags & CMDRUNNER_ALLOW_RECORDING);
2862
2/2
✓ Branch 4 → 5 taken 185 times.
✓ Branch 4 → 6 taken 2 times.
187 runner.flags |= allow_recording ? CMDRUNNER_ALLOW_RECORDING : 0;
2863 187 return handle_command(&runner, cmd);
2864 }
2865
2866 48 bool exec_normal_config(EditorState *e, StringView config)
2867 {
2868 48 CommandRunner runner = normal_mode_cmdrunner(e);
2869 48 return exec_config(&runner, config);
2870 }
2871
2872 106 SystemErrno read_normal_config(EditorState *e, const char *filename, ConfigFlags flags)
2873 {
2874 106 CommandRunner runner = normal_mode_cmdrunner(e);
2875 106 return read_config(&runner, filename, flags);
2876 }
2877
2878 5 void collect_normal_commands(PointerArray *a, StringView prefix)
2879 {
2880 5 COLLECT_STRING_FIELDS(cmds, name, a, prefix);
2881 5 }
2882