dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 75.9% 1195 / 4 / 1579
Functions: 90.9% 100 / 0 / 110
Branches: 54.3% 513 / 128 / 1073

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