dte test coverage


Directory: ./
File: src/commands.c
Date: 2025-02-14 16:55:22
Exec Total Coverage
Lines: 1202 1546 77.7%
Functions: 105 113 92.9%
Branches: 492 850 57.9%

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