dte test coverage


Directory: ./
File: src/commands.c
Date: 2025-06-04 06:50:24
Exec Total Coverage
Lines: 1197 1547 77.4%
Functions: 104 112 92.9%
Branches: 501 875 57.3%

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