dte test coverage


Directory: ./
File: src/commands.c
Date: 2025-10-16 19:09:21
Exec Total Coverage
Lines: 1188 1540 77.1%
Functions: 100 108 92.6%
Branches: 500 893 56.0%

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