vis: use standard registers for macro recordings
[vis.git] / vis-cmds.c
blob38c810fe7a5b5b43f1108d873d2aacdd26666b1b
1 #include <stdbool.h>
2 #include <string.h>
3 #include <strings.h>
4 #include <stdio.h>
5 #include <limits.h>
6 #include <errno.h>
7 #include <unistd.h>
8 #include <fcntl.h>
9 #include <ctype.h>
10 #include <sys/select.h>
11 #include <sys/types.h>
12 #include <sys/wait.h>
14 #include "vis-core.h"
15 #include "text-util.h"
16 #include "text-motions.h"
17 #include "text-objects.h"
18 #include "util.h"
20 enum CmdOpt { /* option flags for command definitions */
21 CMD_OPT_NONE, /* no option (default value) */
22 CMD_OPT_FORCE, /* whether the command can be forced by appending '!' */
23 CMD_OPT_ARGS, /* whether the command line should be parsed in to space
24 * separated arguments to placed into argv, otherwise argv[1]
25 * will contain the remaining command line unmodified */
28 typedef struct { /* command definitions for the ':'-prompt */
29 const char *name[3]; /* name and optional alias for the command */
30 /* command logic called with a NULL terminated array of arguments.
31 * argv[0] will be the command name */
32 bool (*cmd)(Vis*, Filerange*, enum CmdOpt opt, const char *argv[]);
33 enum CmdOpt opt; /* command option flags */
34 } Command;
36 typedef struct { /* used to keep context when dealing with external proceses */
37 Vis *vis; /* editor instance */
38 Text *txt; /* text into which received data will be inserted */
39 size_t pos; /* position at which to insert new data */
40 Buffer err; /* used to store everything the process writes to stderr */
41 } Filter;
43 /** ':'-command implementations */
44 /* set various runtime options */
45 static bool cmd_set(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
46 /* for each argument create a new window and open the corresponding file */
47 static bool cmd_open(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
48 /* close current window (discard modifications if forced ) and open argv[1],
49 * if no argv[1] is given re-read to current file from disk */
50 static bool cmd_edit(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
51 /* close the current window, discard modifications if forced */
52 static bool cmd_quit(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
53 /* close all windows which show current file, discard modifications if forced */
54 static bool cmd_bdelete(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
55 /* close all windows, exit editor, discard modifications if forced */
56 static bool cmd_qall(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
57 /* for each argument try to insert the file content at current cursor postion */
58 static bool cmd_read(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
59 static bool cmd_substitute(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
60 /* if no argument are given, split the current window horizontally,
61 * otherwise open the file */
62 static bool cmd_split(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
63 /* if no argument are given, split the current window vertically,
64 * otherwise open the file */
65 static bool cmd_vsplit(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
66 /* create a new empty window and arrange all windows either horizontally or vertically */
67 static bool cmd_new(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
68 static bool cmd_vnew(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
69 /* save the file displayed in the current window and close it */
70 static bool cmd_wq(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
71 /* save the file displayed in the current window if it was changvis, then close the window */
72 static bool cmd_xit(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
73 /* save the file displayed in the current window to the name given.
74 * do not change internal filname association. further :w commands
75 * without arguments will still write to the old filename */
76 static bool cmd_write(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
77 /* save the file displayed in the current window to the name given,
78 * associate the new name with the buffer. further :w commands
79 * without arguments will write to the new filename */
80 static bool cmd_saveas(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
81 /* filter range through external program argv[1] */
82 static bool cmd_filter(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
83 /* write range to external program, display output in a new window */
84 static bool cmd_pipe(Vis *vis, Filerange*, enum CmdOpt, const char *argv[]);
85 /* switch to the previous/next saved state of the text, chronologically */
86 static bool cmd_earlier_later(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
87 /* dump current key bindings */
88 static bool cmd_help(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
89 /* change runtime key bindings */
90 static bool cmd_map(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
91 static bool cmd_unmap(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
92 /* set language specific key bindings */
93 static bool cmd_langmap(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
95 /* command recognized at the ':'-prompt. commands are found using a unique
96 * prefix match. that is if a command should be available under an abbreviation
97 * which is a prefix for another command it has to be added as an alias. the
98 * long human readable name should always come first */
99 static const Command cmds[] = {
100 /* command name / optional alias, function, options */
101 { { "bdelete" }, cmd_bdelete, CMD_OPT_FORCE },
102 { { "edit", "e" }, cmd_edit, CMD_OPT_FORCE },
103 { { "help" }, cmd_help, CMD_OPT_NONE },
104 { { "map", }, cmd_map, CMD_OPT_FORCE|CMD_OPT_ARGS },
105 { { "map-window", }, cmd_map, CMD_OPT_FORCE|CMD_OPT_ARGS },
106 { { "unmap", }, cmd_unmap, CMD_OPT_ARGS },
107 { { "unmap-window", }, cmd_unmap, CMD_OPT_ARGS },
108 { { "langmap", }, cmd_langmap, CMD_OPT_FORCE|CMD_OPT_ARGS },
109 { { "new" }, cmd_new, CMD_OPT_NONE },
110 { { "open" }, cmd_open, CMD_OPT_NONE },
111 { { "qall" }, cmd_qall, CMD_OPT_FORCE },
112 { { "quit", "q" }, cmd_quit, CMD_OPT_FORCE },
113 { { "read", }, cmd_read, CMD_OPT_FORCE },
114 { { "saveas" }, cmd_saveas, CMD_OPT_FORCE },
115 { { "set", }, cmd_set, CMD_OPT_ARGS },
116 { { "split" }, cmd_split, CMD_OPT_NONE },
117 { { "substitute", "s" }, cmd_substitute, CMD_OPT_NONE },
118 { { "vnew" }, cmd_vnew, CMD_OPT_NONE },
119 { { "vsplit", }, cmd_vsplit, CMD_OPT_NONE },
120 { { "wq", }, cmd_wq, CMD_OPT_FORCE },
121 { { "write", "w" }, cmd_write, CMD_OPT_FORCE },
122 { { "xit", }, cmd_xit, CMD_OPT_FORCE },
123 { { "earlier" }, cmd_earlier_later, CMD_OPT_NONE },
124 { { "later" }, cmd_earlier_later, CMD_OPT_NONE },
125 { { "!", }, cmd_filter, CMD_OPT_NONE },
126 { { "|", }, cmd_pipe, CMD_OPT_NONE },
127 { { NULL, }, NULL, CMD_OPT_NONE },
130 static void windows_arrange(Vis *vis, enum UiLayout layout) {
131 vis->ui->arrange(vis->ui, layout);
134 static void tabwidth_set(Vis *vis, int tabwidth) {
135 if (tabwidth < 1 || tabwidth > 8)
136 return;
137 for (Win *win = vis->windows; win; win = win->next)
138 view_tabwidth_set(win->view, tabwidth);
139 vis->tabwidth = tabwidth;
142 /* parse human-readable boolean value in s. If successful, store the result in
143 * outval and return true. Else return false and leave outval alone. */
144 static bool parse_bool(const char *s, bool *outval) {
145 for (const char **t = (const char*[]){"1", "true", "yes", "on", NULL}; *t; t++) {
146 if (!strcasecmp(s, *t)) {
147 *outval = true;
148 return true;
151 for (const char **f = (const char*[]){"0", "false", "no", "off", NULL}; *f; f++) {
152 if (!strcasecmp(s, *f)) {
153 *outval = false;
154 return true;
157 return false;
160 static bool cmd_set(Vis *vis, Filerange *range, enum CmdOpt cmdopt, const char *argv[]) {
162 typedef struct {
163 const char *names[3];
164 enum {
165 OPTION_TYPE_STRING,
166 OPTION_TYPE_BOOL,
167 OPTION_TYPE_NUMBER,
168 } type;
169 bool optional;
170 int index;
171 } OptionDef;
173 enum {
174 OPTION_AUTOINDENT,
175 OPTION_EXPANDTAB,
176 OPTION_TABWIDTH,
177 OPTION_SYNTAX,
178 OPTION_SHOW,
179 OPTION_NUMBER,
180 OPTION_NUMBER_RELATIVE,
181 OPTION_CURSOR_LINE,
182 OPTION_THEME,
183 OPTION_COLOR_COLUMN,
186 /* definitions have to be in the same order as the enum above */
187 static OptionDef options[] = {
188 [OPTION_AUTOINDENT] = { { "autoindent", "ai" }, OPTION_TYPE_BOOL },
189 [OPTION_EXPANDTAB] = { { "expandtab", "et" }, OPTION_TYPE_BOOL },
190 [OPTION_TABWIDTH] = { { "tabwidth", "tw" }, OPTION_TYPE_NUMBER },
191 [OPTION_SYNTAX] = { { "syntax" }, OPTION_TYPE_STRING, true },
192 [OPTION_SHOW] = { { "show" }, OPTION_TYPE_STRING },
193 [OPTION_NUMBER] = { { "numbers", "nu" }, OPTION_TYPE_BOOL },
194 [OPTION_NUMBER_RELATIVE] = { { "relativenumbers", "rnu" }, OPTION_TYPE_BOOL },
195 [OPTION_CURSOR_LINE] = { { "cursorline", "cul" }, OPTION_TYPE_BOOL },
196 [OPTION_THEME] = { { "theme" }, OPTION_TYPE_STRING },
197 [OPTION_COLOR_COLUMN] = { { "colorcolumn", "cc" }, OPTION_TYPE_NUMBER },
200 if (!vis->options) {
201 if (!(vis->options = map_new()))
202 return false;
203 for (int i = 0; i < LENGTH(options); i++) {
204 options[i].index = i;
205 for (const char **name = options[i].names; *name; name++) {
206 if (!map_put(vis->options, *name, &options[i]))
207 return false;
212 if (!argv[1]) {
213 vis_info_show(vis, "Expecting: set option [value]");
214 return false;
217 Arg arg;
218 bool invert = false;
219 OptionDef *opt = NULL;
221 if (!strncasecmp(argv[1], "no", 2)) {
222 opt = map_closest(vis->options, argv[1]+2);
223 if (opt && opt->type == OPTION_TYPE_BOOL)
224 invert = true;
225 else
226 opt = NULL;
229 if (!opt)
230 opt = map_closest(vis->options, argv[1]);
231 if (!opt) {
232 vis_info_show(vis, "Unknown option: `%s'", argv[1]);
233 return false;
236 switch (opt->type) {
237 case OPTION_TYPE_STRING:
238 if (!opt->optional && !argv[2]) {
239 vis_info_show(vis, "Expecting string option value");
240 return false;
242 arg.s = argv[2];
243 break;
244 case OPTION_TYPE_BOOL:
245 if (!argv[2]) {
246 arg.b = true;
247 } else if (!parse_bool(argv[2], &arg.b)) {
248 vis_info_show(vis, "Expecting boolean option value not: `%s'", argv[2]);
249 return false;
251 if (invert)
252 arg.b = !arg.b;
253 break;
254 case OPTION_TYPE_NUMBER:
255 if (!argv[2]) {
256 vis_info_show(vis, "Expecting number");
257 return false;
259 /* TODO: error checking? long type */
260 arg.i = strtoul(argv[2], NULL, 10);
261 break;
264 switch (opt->index) {
265 case OPTION_EXPANDTAB:
266 vis->expandtab = arg.b;
267 break;
268 case OPTION_AUTOINDENT:
269 vis->autoindent = arg.b;
270 break;
271 case OPTION_TABWIDTH:
272 tabwidth_set(vis, arg.i);
273 break;
274 case OPTION_SYNTAX:
275 if (!argv[2]) {
276 const char *syntax = view_syntax_get(vis->win->view);
277 if (syntax)
278 vis_info_show(vis, "Syntax definition in use: `%s'", syntax);
279 else
280 vis_info_show(vis, "No syntax definition in use");
281 return true;
284 if (parse_bool(argv[2], &arg.b) && !arg.b)
285 return view_syntax_set(vis->win->view, NULL);
286 if (!view_syntax_set(vis->win->view, argv[2])) {
287 vis_info_show(vis, "Unknown syntax definition: `%s'", argv[2]);
288 return false;
290 break;
291 case OPTION_SHOW:
292 if (!argv[2]) {
293 vis_info_show(vis, "Expecting: spaces, tabs, newlines");
294 return false;
296 const char *keys[] = { "spaces", "tabs", "newlines" };
297 const int values[] = {
298 UI_OPTION_SYMBOL_SPACE,
299 UI_OPTION_SYMBOL_TAB|UI_OPTION_SYMBOL_TAB_FILL,
300 UI_OPTION_SYMBOL_EOL,
302 int flags = view_options_get(vis->win->view);
303 for (const char **args = &argv[2]; *args; args++) {
304 for (int i = 0; i < LENGTH(keys); i++) {
305 if (strcmp(*args, keys[i]) == 0) {
306 flags |= values[i];
307 } else if (strstr(*args, keys[i]) == *args) {
308 bool show;
309 const char *v = *args + strlen(keys[i]);
310 if (*v == '=' && parse_bool(v+1, &show)) {
311 if (show)
312 flags |= values[i];
313 else
314 flags &= ~values[i];
319 view_options_set(vis->win->view, flags);
320 break;
321 case OPTION_NUMBER: {
322 enum UiOption opt = view_options_get(vis->win->view);
323 if (arg.b) {
324 opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE;
325 opt |= UI_OPTION_LINE_NUMBERS_ABSOLUTE;
326 } else {
327 opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE;
329 view_options_set(vis->win->view, opt);
330 break;
332 case OPTION_NUMBER_RELATIVE: {
333 enum UiOption opt = view_options_get(vis->win->view);
334 if (arg.b) {
335 opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE;
336 opt |= UI_OPTION_LINE_NUMBERS_RELATIVE;
337 } else {
338 opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE;
340 view_options_set(vis->win->view, opt);
341 break;
343 case OPTION_CURSOR_LINE: {
344 enum UiOption opt = view_options_get(vis->win->view);
345 if (arg.b)
346 opt |= UI_OPTION_CURSOR_LINE;
347 else
348 opt &= ~UI_OPTION_CURSOR_LINE;
349 view_options_set(vis->win->view, opt);
350 break;
352 case OPTION_THEME:
353 if (!vis_theme_load(vis, arg.s)) {
354 vis_info_show(vis, "Failed to load theme: `%s'", arg.s);
355 return false;
357 break;
358 case OPTION_COLOR_COLUMN:
359 view_colorcolumn_set(vis->win->view, arg.i);
360 break;
363 return true;
366 static bool is_file_pattern(const char *pattern) {
367 if (!pattern)
368 return false;
369 struct stat meta;
370 if (stat(pattern, &meta) == 0 && S_ISDIR(meta.st_mode))
371 return true;
372 return strchr(pattern, '*') || strchr(pattern, '[') || strchr(pattern, '{');
375 static const char *file_open_dialog(Vis *vis, const char *pattern) {
376 if (!is_file_pattern(pattern))
377 return pattern;
378 /* this is a bit of a hack, we temporarily replace the text/view of the active
379 * window such that we can use cmd_filter as is */
380 char vis_open[512];
381 static char filename[PATH_MAX];
382 Filerange range = text_range_empty();
383 Win *win = vis->win;
384 File *file = win->file;
385 Text *txt_orig = file->text;
386 View *view_orig = win->view;
387 Text *txt = text_load(NULL);
388 View *view = view_new(txt, NULL);
389 filename[0] = '\0';
390 snprintf(vis_open, sizeof(vis_open)-1, "vis-open %s", pattern ? pattern : "");
392 if (!txt || !view)
393 goto out;
394 win->view = view;
395 file->text = txt;
397 if (cmd_filter(vis, &range, CMD_OPT_NONE, (const char *[]){ "open", vis_open, NULL })) {
398 size_t len = text_size(txt);
399 if (len >= sizeof(filename))
400 len = 0;
401 if (len > 0)
402 text_bytes_get(txt, 0, --len, filename);
403 filename[len] = '\0';
406 out:
407 view_free(view);
408 text_free(txt);
409 win->view = view_orig;
410 file->text = txt_orig;
411 return filename[0] ? filename : NULL;
414 static bool openfiles(Vis *vis, const char **files) {
415 for (; *files; files++) {
416 const char *file = file_open_dialog(vis, *files);
417 if (!file)
418 return false;
419 errno = 0;
420 if (!vis_window_new(vis, file)) {
421 vis_info_show(vis, "Could not open `%s' %s", file,
422 errno ? strerror(errno) : "");
423 return false;
426 return true;
429 static bool cmd_open(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
430 if (!argv[1])
431 return vis_window_new(vis, NULL);
432 return openfiles(vis, &argv[1]);
435 static void info_unsaved_changes(Vis *vis) {
436 vis_info_show(vis, "No write since last change (add ! to override)");
439 static bool cmd_edit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
440 Win *oldwin = vis->win;
441 if (!(opt & CMD_OPT_FORCE) && !vis_window_closable(oldwin)) {
442 info_unsaved_changes(vis);
443 return false;
445 if (!argv[1])
446 return vis_window_reload(oldwin);
447 if (!openfiles(vis, &argv[1]))
448 return false;
449 if (vis->win != oldwin)
450 vis_window_close(oldwin);
451 return vis->win != oldwin;
454 static bool has_windows(Vis *vis) {
455 for (Win *win = vis->windows; win; win = win->next) {
456 if (!win->file->internal)
457 return true;
459 return false;
462 static bool cmd_quit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
463 if (!(opt & CMD_OPT_FORCE) && !vis_window_closable(vis->win)) {
464 info_unsaved_changes(vis);
465 return false;
467 vis_window_close(vis->win);
468 if (!has_windows(vis))
469 vis_exit(vis, EXIT_SUCCESS);
470 return true;
473 static bool cmd_xit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
474 if (text_modified(vis->win->file->text) && !cmd_write(vis, range, opt, argv)) {
475 if (!(opt & CMD_OPT_FORCE))
476 return false;
478 return cmd_quit(vis, range, opt, argv);
481 static bool cmd_bdelete(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
482 Text *txt = vis->win->file->text;
483 if (text_modified(txt) && !(opt & CMD_OPT_FORCE)) {
484 info_unsaved_changes(vis);
485 return false;
487 for (Win *next, *win = vis->windows; win; win = next) {
488 next = win->next;
489 if (win->file->text == txt)
490 vis_window_close(win);
492 if (!has_windows(vis))
493 vis_exit(vis, EXIT_SUCCESS);
494 return true;
497 static bool cmd_qall(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
498 for (Win *next, *win = vis->windows; win; win = next) {
499 next = win->next;
500 if (!win->file->internal && (!text_modified(win->file->text) || (opt & CMD_OPT_FORCE)))
501 vis_window_close(win);
503 if (!has_windows(vis)) {
504 vis_exit(vis, EXIT_SUCCESS);
505 return true;
506 } else {
507 info_unsaved_changes(vis);
508 return false;
512 static bool cmd_read(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
513 char cmd[255];
515 if (!argv[1]) {
516 vis_info_show(vis, "Filename or command expected");
517 return false;
520 bool iscmd = (opt & CMD_OPT_FORCE) || argv[1][0] == '!';
521 const char *arg = argv[1]+(argv[1][0] == '!');
522 snprintf(cmd, sizeof cmd, "%s%s", iscmd ? "" : "cat ", arg);
524 size_t pos = view_cursor_get(vis->win->view);
525 if (!text_range_valid(range))
526 *range = (Filerange){ .start = pos, .end = pos };
527 Filerange delete = *range;
528 range->start = range->end;
530 bool ret = cmd_filter(vis, range, opt, (const char*[]){ argv[0], "sh", "-c", cmd, NULL});
531 if (ret)
532 text_delete_range(vis->win->file->text, &delete);
533 return ret;
536 static bool cmd_substitute(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
537 char pattern[255];
538 if (!text_range_valid(range))
539 *range = text_object_line(vis->win->file->text, view_cursor_get(vis->win->view));
540 snprintf(pattern, sizeof pattern, "s%s", argv[1]);
541 return cmd_filter(vis, range, opt, (const char*[]){ argv[0], "sed", pattern, NULL});
544 static bool cmd_split(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
545 enum UiOption options = view_options_get(vis->win->view);
546 windows_arrange(vis, UI_LAYOUT_HORIZONTAL);
547 if (!argv[1])
548 return vis_window_split(vis->win);
549 bool ret = openfiles(vis, &argv[1]);
550 view_options_set(vis->win->view, options);
551 return ret;
554 static bool cmd_vsplit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
555 enum UiOption options = view_options_get(vis->win->view);
556 windows_arrange(vis, UI_LAYOUT_VERTICAL);
557 if (!argv[1])
558 return vis_window_split(vis->win);
559 bool ret = openfiles(vis, &argv[1]);
560 view_options_set(vis->win->view, options);
561 return ret;
564 static bool cmd_new(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
565 windows_arrange(vis, UI_LAYOUT_HORIZONTAL);
566 return vis_window_new(vis, NULL);
569 static bool cmd_vnew(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
570 windows_arrange(vis, UI_LAYOUT_VERTICAL);
571 return vis_window_new(vis, NULL);
574 static bool cmd_wq(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
575 if (cmd_write(vis, range, opt, argv))
576 return cmd_quit(vis, range, opt, argv);
577 return false;
580 static bool cmd_write(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
581 File *file = vis->win->file;
582 Text *text = file->text;
583 if (!text_range_valid(range))
584 *range = (Filerange){ .start = 0, .end = text_size(text) };
585 if (!argv[1])
586 argv[1] = file->name;
587 if (!argv[1]) {
588 if (file->is_stdin) {
589 if (strchr(argv[0], 'q')) {
590 ssize_t written = text_write_range(text, range, STDOUT_FILENO);
591 if (written == -1 || (size_t)written != text_range_size(range)) {
592 vis_info_show(vis, "Can not write to stdout");
593 return false;
595 /* make sure the file is marked as saved i.e. not modified */
596 text_save_range(text, range, NULL);
597 return true;
599 vis_info_show(vis, "No filename given, use 'wq' to write to stdout");
600 return false;
602 vis_info_show(vis, "Filename expected");
603 return false;
606 if (argv[1][0] == '!') {
607 argv[1]++;
608 return cmd_pipe(vis, range, opt, argv);
611 for (const char **name = &argv[1]; *name; name++) {
612 struct stat meta;
613 if (!(opt & CMD_OPT_FORCE) && file->stat.st_mtime && stat(*name, &meta) == 0 &&
614 file->stat.st_mtime < meta.st_mtime) {
615 vis_info_show(vis, "WARNING: file has been changed since reading it");
616 return false;
618 if (!text_save_range(text, range, *name)) {
619 vis_info_show(vis, "Can't write `%s'", *name);
620 return false;
622 if (!file->name) {
623 vis_window_name(vis->win, *name);
624 file->name = vis->win->file->name;
626 if (strcmp(file->name, *name) == 0)
627 file->stat = text_stat(text);
628 if (vis->event && vis->event->file_save)
629 vis->event->file_save(vis, file);
631 return true;
634 static bool cmd_saveas(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
635 if (cmd_write(vis, range, opt, argv)) {
636 vis_window_name(vis->win, argv[1]);
637 vis->win->file->stat = text_stat(vis->win->file->text);
638 return true;
640 return false;
643 int vis_pipe(Vis *vis, void *context, Filerange *range, const char *argv[],
644 ssize_t (*read_stdout)(void *context, char *data, size_t len),
645 ssize_t (*read_stderr)(void *context, char *data, size_t len)) {
647 /* if an invalid range was given, stdin (i.e. key board input) is passed
648 * through the external command. */
649 Text *text = vis->win->file->text;
650 View *view = vis->win->view;
651 int pin[2], pout[2], perr[2], status = -1;
652 bool interactive = !text_range_valid(range);
653 size_t pos = view_cursor_get(view);
654 Filerange rout = *range;
655 if (interactive)
656 rout = (Filerange){ .start = pos, .end = pos };
658 if (pipe(pin) == -1)
659 return -1;
660 if (pipe(pout) == -1) {
661 close(pin[0]);
662 close(pin[1]);
663 return -1;
666 if (pipe(perr) == -1) {
667 close(pin[0]);
668 close(pin[1]);
669 close(pout[0]);
670 close(pout[1]);
671 return -1;
674 vis->ui->terminal_save(vis->ui);
675 pid_t pid = fork();
677 if (pid == -1) {
678 close(pin[0]);
679 close(pin[1]);
680 close(pout[0]);
681 close(pout[1]);
682 close(perr[0]);
683 close(perr[1]);
684 vis_info_show(vis, "fork failure: %s", strerror(errno));
685 return -1;
686 } else if (pid == 0) { /* child i.e filter */
687 if (!interactive)
688 dup2(pin[0], STDIN_FILENO);
689 close(pin[0]);
690 close(pin[1]);
691 dup2(pout[1], STDOUT_FILENO);
692 close(pout[1]);
693 close(pout[0]);
694 if (!interactive)
695 dup2(perr[1], STDERR_FILENO);
696 close(perr[0]);
697 close(perr[1]);
698 if (!argv[2])
699 execl("/bin/sh", "sh", "-c", argv[1], NULL);
700 else
701 execvp(argv[1], (char**)argv+1);
702 vis_info_show(vis, "exec failure: %s", strerror(errno));
703 exit(EXIT_FAILURE);
706 vis->cancel_filter = false;
708 close(pin[0]);
709 close(pout[1]);
710 close(perr[1]);
712 fcntl(pout[0], F_SETFL, O_NONBLOCK);
713 fcntl(perr[0], F_SETFL, O_NONBLOCK);
716 fd_set rfds, wfds;
718 do {
719 if (vis->cancel_filter) {
720 kill(-pid, SIGTERM);
721 break;
724 FD_ZERO(&rfds);
725 FD_ZERO(&wfds);
726 if (pin[1] != -1)
727 FD_SET(pin[1], &wfds);
728 if (pout[0] != -1)
729 FD_SET(pout[0], &rfds);
730 if (perr[0] != -1)
731 FD_SET(perr[0], &rfds);
733 if (select(FD_SETSIZE, &rfds, &wfds, NULL, NULL) == -1) {
734 if (errno == EINTR)
735 continue;
736 vis_info_show(vis, "Select failure");
737 break;
740 if (pin[1] != -1 && FD_ISSET(pin[1], &wfds)) {
741 Filerange junk = rout;
742 if (junk.end > junk.start + PIPE_BUF)
743 junk.end = junk.start + PIPE_BUF;
744 ssize_t len = text_write_range(text, &junk, pin[1]);
745 if (len > 0) {
746 rout.start += len;
747 if (text_range_size(&rout) == 0) {
748 close(pout[1]);
749 pout[1] = -1;
751 } else {
752 close(pin[1]);
753 pin[1] = -1;
754 if (len == -1)
755 vis_info_show(vis, "Error writing to external command");
759 if (pout[0] != -1 && FD_ISSET(pout[0], &rfds)) {
760 char buf[BUFSIZ];
761 ssize_t len = read(pout[0], buf, sizeof buf);
762 if (len > 0) {
763 if (read_stdout)
764 (*read_stdout)(context, buf, len);
765 } else if (len == 0) {
766 close(pout[0]);
767 pout[0] = -1;
768 } else if (errno != EINTR && errno != EWOULDBLOCK) {
769 vis_info_show(vis, "Error reading from filter stdout");
770 close(pout[0]);
771 pout[0] = -1;
775 if (perr[0] != -1 && FD_ISSET(perr[0], &rfds)) {
776 char buf[BUFSIZ];
777 ssize_t len = read(perr[0], buf, sizeof buf);
778 if (len > 0) {
779 if (read_stderr)
780 (*read_stderr)(context, buf, len);
781 } else if (len == 0) {
782 close(perr[0]);
783 perr[0] = -1;
784 } else if (errno != EINTR && errno != EWOULDBLOCK) {
785 vis_info_show(vis, "Error reading from filter stderr");
786 close(perr[0]);
787 perr[0] = -1;
791 } while (pin[1] != -1 || pout[0] != -1 || perr[0] != -1);
793 if (pin[1] != -1)
794 close(pin[1]);
795 if (pout[0] != -1)
796 close(pout[0]);
797 if (perr[0] != -1)
798 close(perr[0]);
800 for (pid_t died; (died = waitpid(pid, &status, 0)) != -1 && pid != died;);
802 vis->ui->terminal_restore(vis->ui);
804 return status;
807 static ssize_t read_stdout(void *context, char *data, size_t len) {
808 Filter *filter = context;
809 text_insert(filter->txt, filter->pos, data, len);
810 filter->pos += len;
811 return len;
814 static ssize_t read_stderr(void *context, char *data, size_t len) {
815 Filter *filter = context;
816 buffer_append(&filter->err, data, len);
817 return len;
820 static bool cmd_filter(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
821 Text *txt = vis->win->file->text;
822 View *view = vis->win->view;
824 Filter filter = {
825 .vis = vis,
826 .txt = vis->win->file->text,
827 .pos = range->end != EPOS ? range->end : view_cursor_get(view),
830 buffer_init(&filter.err);
832 /* The general idea is the following:
834 * 1) take a snapshot
835 * 2) write [range.start, range.end] to exteneral command
836 * 3) read the output of the external command and insert it after the range
837 * 4) depending on the exit status of the external command
838 * - on success: delete original range
839 * - on failure: revert to previous snapshot
841 * 2) and 3) happend in small junks
844 text_snapshot(txt);
846 int status = vis_pipe(vis, &filter, range, argv, read_stdout, read_stderr);
848 if (status == 0) {
849 if (text_range_valid(range)) {
850 text_delete_range(txt, range);
851 view_cursor_to(view, range->start);
853 text_snapshot(txt);
854 } else {
855 /* make sure we have somehting to undo */
856 text_insert(txt, filter.pos, " ", 1);
857 text_undo(txt);
860 if (vis->cancel_filter)
861 vis_info_show(vis, "Command cancelled");
862 else if (status == 0)
863 vis_info_show(vis, "Command succeded");
864 else if (filter.err.len > 0)
865 vis_info_show(vis, "Command failed: %s", filter.err.data);
866 else
867 vis_info_show(vis, "Command failed");
869 buffer_release(&filter.err);
871 return !vis->cancel_filter && status == 0;
874 static ssize_t read_stdout_new(void *context, char *data, size_t len) {
875 Filter *filter = context;
877 if (!filter->txt && vis_window_new(filter->vis, NULL))
878 filter->txt = filter->vis->win->file->text;
880 if (filter->txt) {
881 text_insert(filter->txt, filter->pos, data, len);
882 filter->pos += len;
884 return len;
887 static bool cmd_pipe(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
888 Text *txt = vis->win->file->text;
889 if (!text_range_valid(range))
890 *range = (Filerange){ .start = 0, .end = text_size(txt) };
892 Filter filter = {
893 .vis = vis,
894 .txt = NULL,
895 .pos = 0,
898 buffer_init(&filter.err);
900 int status = vis_pipe(vis, &filter, range, argv, read_stdout_new, read_stderr);
902 if (vis->cancel_filter)
903 vis_info_show(vis, "Command cancelled");
904 else if (status == 0)
905 vis_info_show(vis, "Command succeded");
906 else if (filter.err.len > 0)
907 vis_info_show(vis, "Command failed: %s", filter.err.data);
908 else
909 vis_info_show(vis, "Command failed");
911 buffer_release(&filter.err);
913 if (filter.txt)
914 text_save(filter.txt, NULL);
916 return !vis->cancel_filter && status == 0;
919 static bool cmd_earlier_later(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
920 Text *txt = vis->win->file->text;
921 char *unit = "";
922 long count = 1;
923 size_t pos = EPOS;
924 if (argv[1]) {
925 errno = 0;
926 count = strtol(argv[1], &unit, 10);
927 if (errno || unit == argv[1] || count < 0) {
928 vis_info_show(vis, "Invalid number");
929 return false;
932 if (*unit) {
933 while (*unit && isspace((unsigned char)*unit))
934 unit++;
935 switch (*unit) {
936 case 'd': count *= 24; /* fall through */
937 case 'h': count *= 60; /* fall through */
938 case 'm': count *= 60; /* fall through */
939 case 's': break;
940 default:
941 vis_info_show(vis, "Unknown time specifier (use: s,m,h or d)");
942 return false;
945 if (argv[0][0] == 'e')
946 count = -count; /* earlier, move back in time */
948 pos = text_restore(txt, text_state(txt) + count);
952 if (!*unit) {
953 if (argv[0][0] == 'e')
954 pos = text_earlier(txt, count);
955 else
956 pos = text_later(txt, count);
959 time_t state = text_state(txt);
960 char buf[32];
961 strftime(buf, sizeof buf, "State from %H:%M", localtime(&state));
962 vis_info_show(vis, "%s", buf);
964 return pos != EPOS;
967 static bool print_keylayout(const char *key, void *value, void *data) {
968 return text_appendf(data, " %-15s\t%s\n", key, (char*)value);
971 static bool print_keybinding(const char *key, void *value, void *data) {
972 Text *txt = data;
973 KeyBinding *binding = value;
974 const char *desc = binding->alias;
975 if (!desc && binding->action)
976 desc = binding->action->help;
977 return text_appendf(txt, " %-15s\t%s\n", key, desc ? desc : "");
980 static void print_mode(Mode *mode, Text *txt) {
981 if (!map_empty(mode->bindings))
982 text_appendf(txt, "\n %s\n\n", mode->name);
983 map_iterate(mode->bindings, print_keybinding, txt);
986 static bool print_action(const char *key, void *value, void *data) {
987 Text *txt = data;
988 KeyAction *action = value;
989 return text_appendf(txt, " %-30s\t%s\n", key, action->help);
992 static bool cmd_help(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
993 if (!vis_window_new(vis, NULL))
994 return false;
996 Text *txt = vis->win->file->text;
998 text_appendf(txt, "vis %s, compiled " __DATE__ " " __TIME__ "\n\n", VERSION);
1000 text_appendf(txt, " Modes\n\n");
1001 for (int i = 0; i < LENGTH(vis_modes); i++) {
1002 Mode *mode = &vis_modes[i];
1003 if (mode->help)
1004 text_appendf(txt, " %-15s\t%s\n", mode->name, mode->help);
1008 if (!map_empty(vis->keymap)) {
1009 text_appendf(txt, "\n Layout specific mappings (affects all modes except INSERT/REPLACE)\n\n");
1010 map_iterate(vis->keymap, print_keylayout, txt);
1013 print_mode(&vis_modes[VIS_MODE_NORMAL], txt);
1014 print_mode(&vis_modes[VIS_MODE_OPERATOR_PENDING], txt);
1015 print_mode(&vis_modes[VIS_MODE_VISUAL], txt);
1016 print_mode(&vis_modes[VIS_MODE_INSERT], txt);
1018 text_appendf(txt, "\n :-Commands\n\n");
1019 for (const Command *cmd = cmds; cmd && cmd->name[0]; cmd++)
1020 text_appendf(txt, " %s\n", cmd->name[0]);
1022 text_appendf(txt, "\n Key binding actions\n\n");
1023 map_iterate(vis->actions, print_action, txt);
1025 text_save(txt, NULL);
1026 return true;
1029 static enum VisMode str2vismode(const char *mode) {
1030 const char *modes[] = {
1031 [VIS_MODE_NORMAL] = "normal",
1032 [VIS_MODE_OPERATOR_PENDING] = "operator-pending",
1033 [VIS_MODE_VISUAL] = "visual",
1034 [VIS_MODE_VISUAL_LINE] = "visual-line",
1035 [VIS_MODE_INSERT] = "insert",
1036 [VIS_MODE_REPLACE] = "replace",
1039 for (size_t i = 0; i < LENGTH(modes); i++) {
1040 if (mode && modes[i] && strcmp(mode, modes[i]) == 0)
1041 return i;
1043 return VIS_MODE_INVALID;
1046 static bool cmd_langmap(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
1047 const char *nonlatin = argv[1];
1048 const char *latin = argv[2];
1049 bool mapped = true;
1051 if (!latin || !nonlatin) {
1052 vis_info_show(vis, "usage: langmap <non-latin keys> <latin keys>");
1053 return false;
1056 while (*latin && *nonlatin) {
1057 size_t i = 0, j = 0;
1058 char latin_key[8], nonlatin_key[8];
1059 do {
1060 if (i < sizeof(latin_key)-1)
1061 latin_key[i++] = *latin;
1062 latin++;
1063 } while (!ISUTF8(*latin));
1064 do {
1065 if (j < sizeof(nonlatin_key)-1)
1066 nonlatin_key[j++] = *nonlatin;
1067 nonlatin++;
1068 } while (!ISUTF8(*nonlatin));
1069 latin_key[i] = '\0';
1070 nonlatin_key[j] = '\0';
1071 mapped &= vis_keymap_add(vis, nonlatin_key, strdup(latin_key));
1074 return mapped;
1077 static bool cmd_map(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
1078 bool local = strstr(argv[0], "-") != NULL;
1079 enum VisMode mode = str2vismode(argv[1]);
1080 const char *lhs = argv[2];
1081 const char *rhs = argv[3];
1083 if (mode == VIS_MODE_INVALID || !lhs || !rhs) {
1084 vis_info_show(vis, "usage: map mode lhs rhs\n");
1085 return false;
1088 KeyBinding *binding = calloc(1, sizeof *binding);
1089 if (!binding)
1090 return false;
1091 if (rhs[0] == '<') {
1092 const char *next = vis_keys_next(vis, rhs);
1093 if (next && next[-1] == '>') {
1094 const char *start = rhs + 1;
1095 const char *end = next - 1;
1096 char key[64];
1097 if (end > start && end - start - 1 < (ptrdiff_t)sizeof key) {
1098 memcpy(key, start, end - start);
1099 key[end - start] = '\0';
1100 binding->action = map_get(vis->actions, key);
1105 if (!binding->action) {
1106 binding->alias = strdup(rhs);
1107 if (!binding->alias) {
1108 free(binding);
1109 return false;
1113 bool mapped;
1114 if (local)
1115 mapped = vis_window_mode_map(vis->win, mode, lhs, binding);
1116 else
1117 mapped = vis_mode_map(vis, mode, lhs, binding);
1119 if (!mapped && opt & CMD_OPT_FORCE) {
1120 if (local) {
1121 mapped = vis_window_mode_unmap(vis->win, mode, lhs) &&
1122 vis_window_mode_map(vis->win, mode, lhs, binding);
1123 } else {
1124 mapped = vis_mode_unmap(vis, mode, lhs) &&
1125 vis_mode_map(vis, mode, lhs, binding);
1129 if (!mapped)
1130 free(binding);
1131 return mapped;
1134 static bool cmd_unmap(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
1135 bool local = strstr(argv[0], "-") != NULL;
1136 enum VisMode mode = str2vismode(argv[1]);
1137 const char *lhs = argv[2];
1139 if (mode == VIS_MODE_INVALID || !lhs) {
1140 vis_info_show(vis, "usage: unmap mode lhs rhs\n");
1141 return false;
1144 if (local)
1145 return vis_window_mode_unmap(vis->win, mode, lhs);
1146 else
1147 return vis_mode_unmap(vis, mode, lhs);
1150 static Filepos parse_pos(Win *win, char **cmd) {
1151 size_t pos = EPOS;
1152 View *view = win->view;
1153 Text *txt = win->file->text;
1154 Mark *marks = win->file->marks;
1155 switch (**cmd) {
1156 case '.':
1157 pos = text_line_begin(txt, view_cursor_get(view));
1158 (*cmd)++;
1159 break;
1160 case '$':
1161 pos = text_size(txt);
1162 (*cmd)++;
1163 break;
1164 case '\'':
1165 (*cmd)++;
1166 if ('a' <= **cmd && **cmd <= 'z')
1167 pos = text_mark_get(txt, marks[**cmd - 'a']);
1168 else if (**cmd == '<')
1169 pos = text_mark_get(txt, marks[VIS_MARK_SELECTION_START]);
1170 else if (**cmd == '>')
1171 pos = text_mark_get(txt, marks[VIS_MARK_SELECTION_END]);
1172 (*cmd)++;
1173 break;
1174 case '/':
1175 (*cmd)++;
1176 char *pattern_end = strchr(*cmd, '/');
1177 if (!pattern_end)
1178 return EPOS;
1179 *pattern_end++ = '\0';
1180 Regex *regex = text_regex_new();
1181 if (!regex)
1182 return EPOS;
1183 if (!text_regex_compile(regex, *cmd, 0)) {
1184 *cmd = pattern_end;
1185 pos = text_search_forward(txt, view_cursor_get(view), regex);
1187 text_regex_free(regex);
1188 break;
1189 case '+':
1190 case '-':
1192 CursorPos curspos = view_cursor_getpos(view);
1193 long long line = curspos.line + strtoll(*cmd, cmd, 10);
1194 if (line < 0)
1195 line = 0;
1196 pos = text_pos_by_lineno(txt, line);
1197 break;
1199 default:
1200 if ('0' <= **cmd && **cmd <= '9')
1201 pos = text_pos_by_lineno(txt, strtoul(*cmd, cmd, 10));
1202 break;
1205 return pos;
1208 static Filerange parse_range(Win *win, char **cmd) {
1209 Text *txt = win->file->text;
1210 Filerange r = text_range_empty();
1211 Mark *marks = win->file->marks;
1212 char start = **cmd;
1213 switch (**cmd) {
1214 case '%':
1215 r.start = 0;
1216 r.end = text_size(txt);
1217 (*cmd)++;
1218 break;
1219 case '*':
1220 r.start = text_mark_get(txt, marks[VIS_MARK_SELECTION_START]);
1221 r.end = text_mark_get(txt, marks[VIS_MARK_SELECTION_END]);
1222 (*cmd)++;
1223 break;
1224 default:
1225 r.start = parse_pos(win, cmd);
1226 if (**cmd != ',') {
1227 if (start == '.')
1228 r.end = text_line_next(txt, r.start);
1229 return r;
1231 (*cmd)++;
1232 r.end = parse_pos(win, cmd);
1233 break;
1235 return r;
1238 static const Command *lookup_cmd(Vis *vis, const char *name) {
1239 if (!vis->cmds) {
1240 if (!(vis->cmds = map_new()))
1241 return NULL;
1243 for (const Command *cmd = cmds; cmd && cmd->name[0]; cmd++) {
1244 for (const char *const *name = cmd->name; *name; name++)
1245 map_put(vis->cmds, *name, cmd);
1248 return map_closest(vis->cmds, name);
1251 bool vis_cmd(Vis *vis, const char *cmdline) {
1252 enum CmdOpt opt = CMD_OPT_NONE;
1253 while (*cmdline == ':')
1254 cmdline++;
1255 size_t len = strlen(cmdline);
1256 char *line = malloc(len+2);
1257 if (!line)
1258 return false;
1259 strncpy(line, cmdline, len+1);
1261 for (char *end = line + len - 1; end >= line && isspace((unsigned char)*end); end--)
1262 *end = '\0';
1264 char *name = line;
1266 Filerange range = parse_range(vis->win, &name);
1267 if (!text_range_valid(&range)) {
1268 /* if only one position was given, jump to it */
1269 if (range.start != EPOS && !*name) {
1270 view_cursor_to(vis->win->view, range.start);
1271 free(line);
1272 return true;
1275 if (name != line) {
1276 vis_info_show(vis, "Invalid range\n");
1277 free(line);
1278 return false;
1281 /* skip leading white space */
1282 while (*name == ' ')
1283 name++;
1284 char *param = name;
1285 while (*param && (isalpha((unsigned char)*param) || *param == '-' || *param == '|'))
1286 param++;
1288 if (*param == '!') {
1289 if (param != name) {
1290 opt |= CMD_OPT_FORCE;
1291 *param = ' ';
1292 } else {
1293 param++;
1297 memmove(param+1, param, strlen(param)+1);
1298 *param++ = '\0'; /* separate command name from parameters */
1300 const Command *cmd = lookup_cmd(vis, name);
1301 if (!cmd) {
1302 vis_info_show(vis, "Not an editor command");
1303 free(line);
1304 return false;
1307 char *s = param;
1308 const char *argv[32] = { name };
1309 for (int i = 1; i < LENGTH(argv); i++) {
1310 while (s && isspace((unsigned char)*s))
1311 s++;
1312 if (s && !*s)
1313 s = NULL;
1314 argv[i] = s;
1315 if (!(cmd->opt & CMD_OPT_ARGS)) {
1316 /* remove trailing spaces */
1317 if (s) {
1318 while (*s) s++;
1319 while (*(--s) == ' ') *s = '\0';
1321 s = NULL;
1323 if (s) {
1324 while (*s && !isspace((unsigned char)*s))
1325 s++;
1326 if (*s)
1327 *s++ = '\0';
1329 /* strip out a single '!' argument to make ":q !" work */
1330 if (argv[i] && !strcmp(argv[i], "!")) {
1331 opt |= CMD_OPT_FORCE;
1332 i--;
1336 cmd->cmd(vis, &range, opt, argv);
1337 free(line);
1338 return true;