dte test coverage


Directory: ./
File: src/commands.c
Date: 2025-07-19 20:13:10
Exec Total Coverage
Lines: 1176 1525 77.1%
Functions: 100 108 92.6%
Branches: 494 871 56.7%

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