dte test coverage


Directory: ./
File: src/commands.c
Date: 2025-07-02 15:21:29
Exec Total Coverage
Lines: 1175 1529 76.8%
Functions: 100 108 92.6%
Branches: 494 874 56.5%

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