Improve large file support
[vis.git] / vis-cmds.c
blobecb13c0af9390bf2a5394bda2ba2a5628f38b616
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[]);
93 /* command recognized at the ':'-prompt. commands are found using a unique
94 * prefix match. that is if a command should be available under an abbreviation
95 * which is a prefix for another command it has to be added as an alias. the
96 * long human readable name should always come first */
97 static Command cmds[] = {
98 /* command name / optional alias, function, options */
99 { { "bdelete" }, cmd_bdelete, CMD_OPT_FORCE },
100 { { "edit", "e" }, cmd_edit, CMD_OPT_FORCE },
101 { { "help" }, cmd_help, CMD_OPT_NONE },
102 { { "map", }, cmd_map, CMD_OPT_FORCE|CMD_OPT_ARGS },
103 { { "map-window", }, cmd_map, CMD_OPT_FORCE|CMD_OPT_ARGS },
104 { { "unmap", }, cmd_unmap, CMD_OPT_ARGS },
105 { { "unmap-window", }, cmd_unmap, CMD_OPT_ARGS },
106 { { "new" }, cmd_new, CMD_OPT_NONE },
107 { { "open" }, cmd_open, CMD_OPT_NONE },
108 { { "qall" }, cmd_qall, CMD_OPT_FORCE },
109 { { "quit", "q" }, cmd_quit, CMD_OPT_FORCE },
110 { { "read", }, cmd_read, CMD_OPT_FORCE },
111 { { "saveas" }, cmd_saveas, CMD_OPT_FORCE },
112 { { "set", }, cmd_set, CMD_OPT_ARGS },
113 { { "split" }, cmd_split, CMD_OPT_NONE },
114 { { "substitute", "s" }, cmd_substitute, CMD_OPT_NONE },
115 { { "vnew" }, cmd_vnew, CMD_OPT_NONE },
116 { { "vsplit", }, cmd_vsplit, CMD_OPT_NONE },
117 { { "wq", }, cmd_wq, CMD_OPT_FORCE },
118 { { "write", "w" }, cmd_write, CMD_OPT_FORCE },
119 { { "xit", }, cmd_xit, CMD_OPT_FORCE },
120 { { "earlier" }, cmd_earlier_later, CMD_OPT_NONE },
121 { { "later" }, cmd_earlier_later, CMD_OPT_NONE },
122 { { "!", }, cmd_filter, CMD_OPT_NONE },
123 { { "|", }, cmd_pipe, CMD_OPT_NONE },
124 { { NULL, }, NULL, CMD_OPT_NONE },
127 static void windows_arrange(Vis *vis, enum UiLayout layout) {
128 vis->ui->arrange(vis->ui, layout);
131 static void tabwidth_set(Vis *vis, int tabwidth) {
132 if (tabwidth < 1 || tabwidth > 8)
133 return;
134 for (Win *win = vis->windows; win; win = win->next)
135 view_tabwidth_set(win->view, tabwidth);
136 vis->tabwidth = tabwidth;
139 /* parse human-readable boolean value in s. If successful, store the result in
140 * outval and return true. Else return false and leave outval alone. */
141 static bool parse_bool(const char *s, bool *outval) {
142 for (const char **t = (const char*[]){"1", "true", "yes", "on", NULL}; *t; t++) {
143 if (!strcasecmp(s, *t)) {
144 *outval = true;
145 return true;
148 for (const char **f = (const char*[]){"0", "false", "no", "off", NULL}; *f; f++) {
149 if (!strcasecmp(s, *f)) {
150 *outval = false;
151 return true;
154 return false;
157 static bool cmd_set(Vis *vis, Filerange *range, enum CmdOpt cmdopt, const char *argv[]) {
159 typedef struct {
160 const char *names[3];
161 enum {
162 OPTION_TYPE_STRING,
163 OPTION_TYPE_BOOL,
164 OPTION_TYPE_NUMBER,
165 } type;
166 bool optional;
167 int index;
168 } OptionDef;
170 enum {
171 OPTION_AUTOINDENT,
172 OPTION_EXPANDTAB,
173 OPTION_TABWIDTH,
174 OPTION_SYNTAX,
175 OPTION_SHOW,
176 OPTION_NUMBER,
177 OPTION_NUMBER_RELATIVE,
178 OPTION_CURSOR_LINE,
179 OPTION_THEME,
180 OPTION_COLOR_COLUMN,
183 /* definitions have to be in the same order as the enum above */
184 static OptionDef options[] = {
185 [OPTION_AUTOINDENT] = { { "autoindent", "ai" }, OPTION_TYPE_BOOL },
186 [OPTION_EXPANDTAB] = { { "expandtab", "et" }, OPTION_TYPE_BOOL },
187 [OPTION_TABWIDTH] = { { "tabwidth", "tw" }, OPTION_TYPE_NUMBER },
188 [OPTION_SYNTAX] = { { "syntax" }, OPTION_TYPE_STRING, true },
189 [OPTION_SHOW] = { { "show" }, OPTION_TYPE_STRING },
190 [OPTION_NUMBER] = { { "numbers", "nu" }, OPTION_TYPE_BOOL },
191 [OPTION_NUMBER_RELATIVE] = { { "relativenumbers", "rnu" }, OPTION_TYPE_BOOL },
192 [OPTION_CURSOR_LINE] = { { "cursorline", "cul" }, OPTION_TYPE_BOOL },
193 [OPTION_THEME] = { { "theme" }, OPTION_TYPE_STRING },
194 [OPTION_COLOR_COLUMN] = { { "colorcolumn", "cc" }, OPTION_TYPE_NUMBER },
197 if (!vis->options) {
198 if (!(vis->options = map_new()))
199 return false;
200 for (int i = 0; i < LENGTH(options); i++) {
201 options[i].index = i;
202 for (const char **name = options[i].names; *name; name++) {
203 if (!map_put(vis->options, *name, &options[i]))
204 return false;
209 if (!argv[1]) {
210 vis_info_show(vis, "Expecting: set option [value]");
211 return false;
214 Arg arg;
215 bool invert = false;
216 OptionDef *opt = NULL;
218 if (!strncasecmp(argv[1], "no", 2)) {
219 opt = map_closest(vis->options, argv[1]+2);
220 if (opt && opt->type == OPTION_TYPE_BOOL)
221 invert = true;
222 else
223 opt = NULL;
226 if (!opt)
227 opt = map_closest(vis->options, argv[1]);
228 if (!opt) {
229 vis_info_show(vis, "Unknown option: `%s'", argv[1]);
230 return false;
233 switch (opt->type) {
234 case OPTION_TYPE_STRING:
235 if (!opt->optional && !argv[2]) {
236 vis_info_show(vis, "Expecting string option value");
237 return false;
239 arg.s = argv[2];
240 break;
241 case OPTION_TYPE_BOOL:
242 if (!argv[2]) {
243 arg.b = true;
244 } else if (!parse_bool(argv[2], &arg.b)) {
245 vis_info_show(vis, "Expecting boolean option value not: `%s'", argv[2]);
246 return false;
248 if (invert)
249 arg.b = !arg.b;
250 break;
251 case OPTION_TYPE_NUMBER:
252 if (!argv[2]) {
253 vis_info_show(vis, "Expecting number");
254 return false;
256 /* TODO: error checking? long type */
257 arg.i = strtoul(argv[2], NULL, 10);
258 break;
261 switch (opt->index) {
262 case OPTION_EXPANDTAB:
263 vis->expandtab = arg.b;
264 break;
265 case OPTION_AUTOINDENT:
266 vis->autoindent = arg.b;
267 break;
268 case OPTION_TABWIDTH:
269 tabwidth_set(vis, arg.i);
270 break;
271 case OPTION_SYNTAX:
272 if (!argv[2]) {
273 const char *syntax = view_syntax_get(vis->win->view);
274 if (syntax)
275 vis_info_show(vis, "Syntax definition in use: `%s'", syntax);
276 else
277 vis_info_show(vis, "No syntax definition in use");
278 return true;
281 if (parse_bool(argv[2], &arg.b) && !arg.b)
282 return view_syntax_set(vis->win->view, NULL);
283 if (!view_syntax_set(vis->win->view, argv[2])) {
284 vis_info_show(vis, "Unknown syntax definition: `%s'", argv[2]);
285 return false;
287 break;
288 case OPTION_SHOW:
289 if (!argv[2]) {
290 vis_info_show(vis, "Expecting: spaces, tabs, newlines");
291 return false;
293 char *keys[] = { "spaces", "tabs", "newlines" };
294 int values[] = {
295 UI_OPTION_SYMBOL_SPACE,
296 UI_OPTION_SYMBOL_TAB|UI_OPTION_SYMBOL_TAB_FILL,
297 UI_OPTION_SYMBOL_EOL,
299 int flags = view_options_get(vis->win->view);
300 for (const char **args = &argv[2]; *args; args++) {
301 for (int i = 0; i < LENGTH(keys); i++) {
302 if (strcmp(*args, keys[i]) == 0) {
303 flags |= values[i];
304 } else if (strstr(*args, keys[i]) == *args) {
305 bool show;
306 const char *v = *args + strlen(keys[i]);
307 if (*v == '=' && parse_bool(v+1, &show)) {
308 if (show)
309 flags |= values[i];
310 else
311 flags &= ~values[i];
316 view_options_set(vis->win->view, flags);
317 break;
318 case OPTION_NUMBER: {
319 enum UiOption opt = view_options_get(vis->win->view);
320 if (arg.b) {
321 opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE;
322 opt |= UI_OPTION_LINE_NUMBERS_ABSOLUTE;
323 } else {
324 opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE;
326 view_options_set(vis->win->view, opt);
327 break;
329 case OPTION_NUMBER_RELATIVE: {
330 enum UiOption opt = view_options_get(vis->win->view);
331 if (arg.b) {
332 opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE;
333 opt |= UI_OPTION_LINE_NUMBERS_RELATIVE;
334 } else {
335 opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE;
337 view_options_set(vis->win->view, opt);
338 break;
340 case OPTION_CURSOR_LINE: {
341 enum UiOption opt = view_options_get(vis->win->view);
342 if (arg.b)
343 opt |= UI_OPTION_CURSOR_LINE;
344 else
345 opt &= ~UI_OPTION_CURSOR_LINE;
346 view_options_set(vis->win->view, opt);
347 break;
349 case OPTION_THEME:
350 if (!vis_theme_load(vis, arg.s)) {
351 vis_info_show(vis, "Failed to load theme: `%s'", arg.s);
352 return false;
354 break;
355 case OPTION_COLOR_COLUMN:
356 view_colorcolumn_set(vis->win->view, arg.i);
357 break;
360 return true;
363 static bool is_file_pattern(const char *pattern) {
364 if (!pattern)
365 return false;
366 struct stat meta;
367 if (stat(pattern, &meta) == 0 && S_ISDIR(meta.st_mode))
368 return true;
369 return strchr(pattern, '*') || strchr(pattern, '[') || strchr(pattern, '{');
372 static const char *file_open_dialog(Vis *vis, const char *pattern) {
373 if (!is_file_pattern(pattern))
374 return pattern;
375 /* this is a bit of a hack, we temporarily replace the text/view of the active
376 * window such that we can use cmd_filter as is */
377 char vis_open[512];
378 static char filename[PATH_MAX];
379 Filerange range = text_range_empty();
380 Win *win = vis->win;
381 File *file = win->file;
382 Text *txt_orig = file->text;
383 View *view_orig = win->view;
384 Text *txt = text_load(NULL);
385 View *view = view_new(txt, NULL);
386 filename[0] = '\0';
387 snprintf(vis_open, sizeof(vis_open)-1, "vis-open %s", pattern ? pattern : "");
389 if (!txt || !view)
390 goto out;
391 win->view = view;
392 file->text = txt;
394 if (cmd_filter(vis, &range, CMD_OPT_NONE, (const char *[]){ "open", vis_open, NULL })) {
395 size_t len = text_size(txt);
396 if (len >= sizeof(filename))
397 len = 0;
398 if (len > 0)
399 text_bytes_get(txt, 0, --len, filename);
400 filename[len] = '\0';
403 out:
404 view_free(view);
405 text_free(txt);
406 win->view = view_orig;
407 file->text = txt_orig;
408 return filename[0] ? filename : NULL;
411 static bool openfiles(Vis *vis, const char **files) {
412 for (; *files; files++) {
413 const char *file = file_open_dialog(vis, *files);
414 if (!file)
415 return false;
416 errno = 0;
417 if (!vis_window_new(vis, file)) {
418 vis_info_show(vis, "Could not open `%s' %s", file,
419 errno ? strerror(errno) : "");
420 return false;
423 return true;
426 static bool cmd_open(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
427 if (!argv[1])
428 return vis_window_new(vis, NULL);
429 return openfiles(vis, &argv[1]);
432 static void info_unsaved_changes(Vis *vis) {
433 vis_info_show(vis, "No write since last change (add ! to override)");
436 static bool cmd_edit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
437 Win *oldwin = vis->win;
438 if (!(opt & CMD_OPT_FORCE) && !vis_window_closable(oldwin)) {
439 info_unsaved_changes(vis);
440 return false;
442 if (!argv[1])
443 return vis_window_reload(oldwin);
444 if (!openfiles(vis, &argv[1]))
445 return false;
446 if (vis->win != oldwin)
447 vis_window_close(oldwin);
448 return vis->win != oldwin;
451 static bool has_windows(Vis *vis) {
452 for (Win *win = vis->windows; win; win = win->next) {
453 if (!win->file->internal)
454 return true;
456 return false;
459 static bool cmd_quit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
460 if (!(opt & CMD_OPT_FORCE) && !vis_window_closable(vis->win)) {
461 info_unsaved_changes(vis);
462 return false;
464 vis_window_close(vis->win);
465 if (!has_windows(vis))
466 vis_exit(vis, EXIT_SUCCESS);
467 return true;
470 static bool cmd_xit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
471 if (text_modified(vis->win->file->text) && !cmd_write(vis, range, opt, argv)) {
472 if (!(opt & CMD_OPT_FORCE))
473 return false;
475 return cmd_quit(vis, range, opt, argv);
478 static bool cmd_bdelete(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
479 Text *txt = vis->win->file->text;
480 if (text_modified(txt) && !(opt & CMD_OPT_FORCE)) {
481 info_unsaved_changes(vis);
482 return false;
484 for (Win *next, *win = vis->windows; win; win = next) {
485 next = win->next;
486 if (win->file->text == txt)
487 vis_window_close(win);
489 if (!has_windows(vis))
490 vis_exit(vis, EXIT_SUCCESS);
491 return true;
494 static bool cmd_qall(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
495 for (Win *next, *win = vis->windows; win; win = next) {
496 next = win->next;
497 if (!win->file->internal && (!text_modified(win->file->text) || (opt & CMD_OPT_FORCE)))
498 vis_window_close(win);
500 if (!has_windows(vis)) {
501 vis_exit(vis, EXIT_SUCCESS);
502 return true;
503 } else {
504 info_unsaved_changes(vis);
505 return false;
509 static bool cmd_read(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
510 char cmd[255];
512 if (!argv[1]) {
513 vis_info_show(vis, "Filename or command expected");
514 return false;
517 bool iscmd = (opt & CMD_OPT_FORCE) || argv[1][0] == '!';
518 const char *arg = argv[1]+(argv[1][0] == '!');
519 snprintf(cmd, sizeof cmd, "%s%s", iscmd ? "" : "cat ", arg);
521 size_t pos = view_cursor_get(vis->win->view);
522 if (!text_range_valid(range))
523 *range = (Filerange){ .start = pos, .end = pos };
524 Filerange delete = *range;
525 range->start = range->end;
527 bool ret = cmd_filter(vis, range, opt, (const char*[]){ argv[0], "sh", "-c", cmd, NULL});
528 if (ret)
529 text_delete_range(vis->win->file->text, &delete);
530 return ret;
533 static bool cmd_substitute(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
534 char pattern[255];
535 if (!text_range_valid(range))
536 *range = text_object_line(vis->win->file->text, view_cursor_get(vis->win->view));
537 snprintf(pattern, sizeof pattern, "s%s", argv[1]);
538 return cmd_filter(vis, range, opt, (const char*[]){ argv[0], "sed", pattern, NULL});
541 static bool cmd_split(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
542 enum UiOption options = view_options_get(vis->win->view);
543 windows_arrange(vis, UI_LAYOUT_HORIZONTAL);
544 if (!argv[1])
545 return vis_window_split(vis->win);
546 bool ret = openfiles(vis, &argv[1]);
547 view_options_set(vis->win->view, options);
548 return ret;
551 static bool cmd_vsplit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
552 enum UiOption options = view_options_get(vis->win->view);
553 windows_arrange(vis, UI_LAYOUT_VERTICAL);
554 if (!argv[1])
555 return vis_window_split(vis->win);
556 bool ret = openfiles(vis, &argv[1]);
557 view_options_set(vis->win->view, options);
558 return ret;
561 static bool cmd_new(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
562 windows_arrange(vis, UI_LAYOUT_HORIZONTAL);
563 return vis_window_new(vis, NULL);
566 static bool cmd_vnew(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
567 windows_arrange(vis, UI_LAYOUT_VERTICAL);
568 return vis_window_new(vis, NULL);
571 static bool cmd_wq(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
572 if (cmd_write(vis, range, opt, argv))
573 return cmd_quit(vis, range, opt, argv);
574 return false;
577 static bool cmd_write(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
578 File *file = vis->win->file;
579 Text *text = file->text;
580 if (!text_range_valid(range))
581 *range = (Filerange){ .start = 0, .end = text_size(text) };
582 if (!argv[1])
583 argv[1] = file->name;
584 if (!argv[1]) {
585 if (file->is_stdin) {
586 if (strchr(argv[0], 'q')) {
587 ssize_t written = text_write_range(text, range, STDOUT_FILENO);
588 if (written == -1 || (size_t)written != text_range_size(range)) {
589 vis_info_show(vis, "Can not write to stdout");
590 return false;
592 /* make sure the file is marked as saved i.e. not modified */
593 text_save_range(text, range, NULL);
594 return true;
596 vis_info_show(vis, "No filename given, use 'wq' to write to stdout");
597 return false;
599 vis_info_show(vis, "Filename expected");
600 return false;
603 if (argv[1][0] == '!') {
604 argv[1]++;
605 return cmd_pipe(vis, range, opt, argv);
608 for (const char **name = &argv[1]; *name; name++) {
609 struct stat meta;
610 if (!(opt & CMD_OPT_FORCE) && file->stat.st_mtime && stat(*name, &meta) == 0 &&
611 file->stat.st_mtime < meta.st_mtime) {
612 vis_info_show(vis, "WARNING: file has been changed since reading it");
613 return false;
615 if (!text_save_range(text, range, *name)) {
616 vis_info_show(vis, "Can't write `%s'", *name);
617 return false;
619 if (!file->name) {
620 vis_window_name(vis->win, *name);
621 file->name = vis->win->file->name;
623 if (strcmp(file->name, *name) == 0)
624 file->stat = text_stat(text);
625 if (vis->event && vis->event->file_save)
626 vis->event->file_save(vis, file);
628 return true;
631 static bool cmd_saveas(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
632 if (cmd_write(vis, range, opt, argv)) {
633 vis_window_name(vis->win, argv[1]);
634 vis->win->file->stat = text_stat(vis->win->file->text);
635 return true;
637 return false;
640 int vis_pipe(Vis *vis, void *context, Filerange *range, const char *argv[],
641 ssize_t (*read_stdout)(void *context, char *data, size_t len),
642 ssize_t (*read_stderr)(void *context, char *data, size_t len)) {
644 /* if an invalid range was given, stdin (i.e. key board input) is passed
645 * through the external command. */
646 Text *text = vis->win->file->text;
647 View *view = vis->win->view;
648 int pin[2], pout[2], perr[2], status = -1;
649 bool interactive = !text_range_valid(range);
650 size_t pos = view_cursor_get(view);
651 Filerange rout = *range;
652 if (interactive)
653 rout = (Filerange){ .start = pos, .end = pos };
655 if (pipe(pin) == -1)
656 return -1;
657 if (pipe(pout) == -1) {
658 close(pin[0]);
659 close(pin[1]);
660 return -1;
663 if (pipe(perr) == -1) {
664 close(pin[0]);
665 close(pin[1]);
666 close(pout[0]);
667 close(pout[1]);
668 return -1;
671 vis->ui->terminal_save(vis->ui);
672 pid_t pid = fork();
674 if (pid == -1) {
675 close(pin[0]);
676 close(pin[1]);
677 close(pout[0]);
678 close(pout[1]);
679 close(perr[0]);
680 close(perr[1]);
681 vis_info_show(vis, "fork failure: %s", strerror(errno));
682 return -1;
683 } else if (pid == 0) { /* child i.e filter */
684 if (!interactive)
685 dup2(pin[0], STDIN_FILENO);
686 close(pin[0]);
687 close(pin[1]);
688 dup2(pout[1], STDOUT_FILENO);
689 close(pout[1]);
690 close(pout[0]);
691 if (!interactive)
692 dup2(perr[1], STDERR_FILENO);
693 close(perr[0]);
694 close(perr[1]);
695 if (!argv[2])
696 execl("/bin/sh", "sh", "-c", argv[1], NULL);
697 else
698 execvp(argv[1], (char**)argv+1);
699 vis_info_show(vis, "exec failure: %s", strerror(errno));
700 exit(EXIT_FAILURE);
703 vis->cancel_filter = false;
705 close(pin[0]);
706 close(pout[1]);
707 close(perr[1]);
709 fcntl(pout[0], F_SETFL, O_NONBLOCK);
710 fcntl(perr[0], F_SETFL, O_NONBLOCK);
713 fd_set rfds, wfds;
715 do {
716 if (vis->cancel_filter) {
717 kill(-pid, SIGTERM);
718 break;
721 FD_ZERO(&rfds);
722 FD_ZERO(&wfds);
723 if (pin[1] != -1)
724 FD_SET(pin[1], &wfds);
725 if (pout[0] != -1)
726 FD_SET(pout[0], &rfds);
727 if (perr[0] != -1)
728 FD_SET(perr[0], &rfds);
730 if (select(FD_SETSIZE, &rfds, &wfds, NULL, NULL) == -1) {
731 if (errno == EINTR)
732 continue;
733 vis_info_show(vis, "Select failure");
734 break;
737 if (pin[1] != -1 && FD_ISSET(pin[1], &wfds)) {
738 Filerange junk = rout;
739 if (junk.end > junk.start + PIPE_BUF)
740 junk.end = junk.start + PIPE_BUF;
741 ssize_t len = text_write_range(text, &junk, pin[1]);
742 if (len > 0) {
743 rout.start += len;
744 if (text_range_size(&rout) == 0) {
745 close(pout[1]);
746 pout[1] = -1;
748 } else {
749 close(pin[1]);
750 pin[1] = -1;
751 if (len == -1)
752 vis_info_show(vis, "Error writing to external command");
756 if (pout[0] != -1 && FD_ISSET(pout[0], &rfds)) {
757 char buf[BUFSIZ];
758 ssize_t len = read(pout[0], buf, sizeof buf);
759 if (len > 0) {
760 if (read_stdout)
761 (*read_stdout)(context, buf, len);
762 } else if (len == 0) {
763 close(pout[0]);
764 pout[0] = -1;
765 } else if (errno != EINTR && errno != EWOULDBLOCK) {
766 vis_info_show(vis, "Error reading from filter stdout");
767 close(pout[0]);
768 pout[0] = -1;
772 if (perr[0] != -1 && FD_ISSET(perr[0], &rfds)) {
773 char buf[BUFSIZ];
774 ssize_t len = read(perr[0], buf, sizeof buf);
775 if (len > 0) {
776 if (read_stderr)
777 (*read_stderr)(context, buf, len);
778 } else if (len == 0) {
779 close(perr[0]);
780 perr[0] = -1;
781 } else if (errno != EINTR && errno != EWOULDBLOCK) {
782 vis_info_show(vis, "Error reading from filter stderr");
783 close(perr[0]);
784 perr[0] = -1;
788 } while (pin[1] != -1 || pout[0] != -1 || perr[0] != -1);
790 if (pin[1] != -1)
791 close(pin[1]);
792 if (pout[0] != -1)
793 close(pout[0]);
794 if (perr[0] != -1)
795 close(perr[0]);
797 for (pid_t died; (died = waitpid(pid, &status, 0)) != -1 && pid != died;);
799 vis->ui->terminal_restore(vis->ui);
801 return status;
804 static ssize_t read_stdout(void *context, char *data, size_t len) {
805 Filter *filter = context;
806 text_insert(filter->txt, filter->pos, data, len);
807 filter->pos += len;
808 return len;
811 static ssize_t read_stderr(void *context, char *data, size_t len) {
812 Filter *filter = context;
813 buffer_append(&filter->err, data, len);
814 return len;
817 static bool cmd_filter(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
818 Text *txt = vis->win->file->text;
819 View *view = vis->win->view;
821 Filter filter = {
822 .vis = vis,
823 .txt = vis->win->file->text,
824 .pos = range->end != EPOS ? range->end : view_cursor_get(view),
827 buffer_init(&filter.err);
829 /* The general idea is the following:
831 * 1) take a snapshot
832 * 2) write [range.start, range.end] to exteneral command
833 * 3) read the output of the external command and insert it after the range
834 * 4) depending on the exit status of the external command
835 * - on success: delete original range
836 * - on failure: revert to previous snapshot
838 * 2) and 3) happend in small junks
841 text_snapshot(txt);
843 int status = vis_pipe(vis, &filter, range, argv, read_stdout, read_stderr);
845 if (status == 0) {
846 if (text_range_valid(range)) {
847 text_delete_range(txt, range);
848 view_cursor_to(view, range->start);
850 text_snapshot(txt);
851 } else {
852 /* make sure we have somehting to undo */
853 text_insert(txt, filter.pos, " ", 1);
854 text_undo(txt);
857 if (vis->cancel_filter)
858 vis_info_show(vis, "Command cancelled");
859 else if (status == 0)
860 vis_info_show(vis, "Command succeded");
861 else if (filter.err.len > 0)
862 vis_info_show(vis, "Command failed: %s", filter.err.data);
863 else
864 vis_info_show(vis, "Command failed");
866 buffer_release(&filter.err);
868 return !vis->cancel_filter && status == 0;
871 static ssize_t read_stdout_new(void *context, char *data, size_t len) {
872 Filter *filter = context;
874 if (!filter->txt && vis_window_new(filter->vis, NULL))
875 filter->txt = filter->vis->win->file->text;
877 if (filter->txt) {
878 text_insert(filter->txt, filter->pos, data, len);
879 filter->pos += len;
881 return len;
884 static bool cmd_pipe(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
885 Text *txt = vis->win->file->text;
886 if (!text_range_valid(range))
887 *range = (Filerange){ .start = 0, .end = text_size(txt) };
889 Filter filter = {
890 .vis = vis,
891 .txt = NULL,
892 .pos = 0,
895 buffer_init(&filter.err);
897 int status = vis_pipe(vis, &filter, range, argv, read_stdout_new, read_stderr);
899 if (vis->cancel_filter)
900 vis_info_show(vis, "Command cancelled");
901 else if (status == 0)
902 vis_info_show(vis, "Command succeded");
903 else if (filter.err.len > 0)
904 vis_info_show(vis, "Command failed: %s", filter.err.data);
905 else
906 vis_info_show(vis, "Command failed");
908 buffer_release(&filter.err);
910 if (filter.txt)
911 text_save(filter.txt, NULL);
913 return !vis->cancel_filter && status == 0;
916 static bool cmd_earlier_later(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
917 Text *txt = vis->win->file->text;
918 char *unit = "";
919 long count = 1;
920 size_t pos = EPOS;
921 if (argv[1]) {
922 errno = 0;
923 count = strtol(argv[1], &unit, 10);
924 if (errno || unit == argv[1] || count < 0) {
925 vis_info_show(vis, "Invalid number");
926 return false;
929 if (*unit) {
930 while (*unit && isspace((unsigned char)*unit))
931 unit++;
932 switch (*unit) {
933 case 'd': count *= 24; /* fall through */
934 case 'h': count *= 60; /* fall through */
935 case 'm': count *= 60; /* fall through */
936 case 's': break;
937 default:
938 vis_info_show(vis, "Unknown time specifier (use: s,m,h or d)");
939 return false;
942 if (argv[0][0] == 'e')
943 count = -count; /* earlier, move back in time */
945 pos = text_restore(txt, text_state(txt) + count);
949 if (!*unit) {
950 if (argv[0][0] == 'e')
951 pos = text_earlier(txt, count);
952 else
953 pos = text_later(txt, count);
956 time_t state = text_state(txt);
957 char buf[32];
958 strftime(buf, sizeof buf, "State from %H:%M", localtime(&state));
959 vis_info_show(vis, "%s", buf);
961 return pos != EPOS;
964 static bool print_keybinding(const char *key, void *value, void *data) {
965 Text *txt = data;
966 KeyBinding *binding = value;
967 const char *desc = binding->alias;
968 if (!desc && binding->action)
969 desc = binding->action->help;
970 return text_appendf(txt, " %-15s\t%s\n", key, desc ? desc : "");
973 static void print_mode(Mode *mode, Text *txt) {
974 if (!map_empty(mode->bindings))
975 text_appendf(txt, "\n %s\n\n", mode->name);
976 map_iterate(mode->bindings, print_keybinding, txt);
979 static bool print_action(const char *key, void *value, void *data) {
980 Text *txt = data;
981 KeyAction *action = value;
982 return text_appendf(txt, " %-30s\t%s\n", key, action->help);
985 static bool cmd_help(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
986 if (!vis_window_new(vis, NULL))
987 return false;
989 Text *txt = vis->win->file->text;
991 text_appendf(txt, "vis %s, compiled " __DATE__ " " __TIME__ "\n\n", VERSION);
993 text_appendf(txt, " Modes\n\n");
994 for (int i = 0; i < LENGTH(vis_modes); i++) {
995 Mode *mode = &vis_modes[i];
996 if (mode->help)
997 text_appendf(txt, " %-15s\t%s\n", mode->name, mode->help);
1000 print_mode(&vis_modes[VIS_MODE_NORMAL], txt);
1001 print_mode(&vis_modes[VIS_MODE_OPERATOR_PENDING], txt);
1002 print_mode(&vis_modes[VIS_MODE_VISUAL], txt);
1003 print_mode(&vis_modes[VIS_MODE_INSERT], txt);
1005 text_appendf(txt, "\n :-Commands\n\n");
1006 for (Command *cmd = cmds; cmd && cmd->name[0]; cmd++)
1007 text_appendf(txt, " %s\n", cmd->name[0]);
1009 text_appendf(txt, "\n Key binding actions\n\n");
1010 map_iterate(vis->actions, print_action, txt);
1012 text_save(txt, NULL);
1013 return true;
1016 static enum VisMode str2vismode(const char *mode) {
1017 const char *modes[] = {
1018 [VIS_MODE_NORMAL] = "normal",
1019 [VIS_MODE_OPERATOR_PENDING] = "operator-pending",
1020 [VIS_MODE_VISUAL] = "visual",
1021 [VIS_MODE_VISUAL_LINE] = "visual-line",
1022 [VIS_MODE_INSERT] = "insert",
1023 [VIS_MODE_REPLACE] = "replace",
1026 for (size_t i = 0; i < LENGTH(modes); i++) {
1027 if (mode && modes[i] && strcmp(mode, modes[i]) == 0)
1028 return i;
1030 return VIS_MODE_INVALID;
1033 static bool cmd_map(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
1034 bool local = strstr(argv[0], "-") != NULL;
1035 enum VisMode mode = str2vismode(argv[1]);
1036 const char *lhs = argv[2];
1037 const char *rhs = argv[3];
1039 if (mode == VIS_MODE_INVALID || !lhs || !rhs) {
1040 vis_info_show(vis, "usage: map mode lhs rhs\n");
1041 return false;
1044 KeyBinding *binding = calloc(1, sizeof *binding);
1045 if (!binding)
1046 return false;
1047 if (rhs[0] == '<') {
1048 const char *next = vis_keys_next(vis, rhs);
1049 if (next && next[-1] == '>') {
1050 const char *start = rhs + 1;
1051 const char *end = next - 1;
1052 char key[64];
1053 if (end > start && end - start - 1 < (ptrdiff_t)sizeof key) {
1054 memcpy(key, start, end - start);
1055 key[end - start] = '\0';
1056 binding->action = map_get(vis->actions, key);
1061 if (!binding->action) {
1062 binding->alias = strdup(rhs);
1063 if (!binding->alias) {
1064 free(binding);
1065 return false;
1069 bool mapped;
1070 if (local)
1071 mapped = vis_window_mode_map(vis->win, mode, lhs, binding);
1072 else
1073 mapped = vis_mode_map(vis, mode, lhs, binding);
1075 if (!mapped && opt & CMD_OPT_FORCE) {
1076 if (local) {
1077 mapped = vis_window_mode_unmap(vis->win, mode, lhs) &&
1078 vis_window_mode_map(vis->win, mode, lhs, binding);
1079 } else {
1080 mapped = vis_mode_unmap(vis, mode, lhs) &&
1081 vis_mode_map(vis, mode, lhs, binding);
1085 if (!mapped)
1086 free(binding);
1087 return mapped;
1090 static bool cmd_unmap(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
1091 bool local = strstr(argv[0], "-") != NULL;
1092 enum VisMode mode = str2vismode(argv[1]);
1093 const char *lhs = argv[2];
1095 if (mode == VIS_MODE_INVALID || !lhs) {
1096 vis_info_show(vis, "usage: unmap mode lhs rhs\n");
1097 return false;
1100 if (local)
1101 return vis_window_mode_unmap(vis->win, mode, lhs);
1102 else
1103 return vis_mode_unmap(vis, mode, lhs);
1106 static Filepos parse_pos(Win *win, char **cmd) {
1107 size_t pos = EPOS;
1108 View *view = win->view;
1109 Text *txt = win->file->text;
1110 Mark *marks = win->file->marks;
1111 switch (**cmd) {
1112 case '.':
1113 pos = text_line_begin(txt, view_cursor_get(view));
1114 (*cmd)++;
1115 break;
1116 case '$':
1117 pos = text_size(txt);
1118 (*cmd)++;
1119 break;
1120 case '\'':
1121 (*cmd)++;
1122 if ('a' <= **cmd && **cmd <= 'z')
1123 pos = text_mark_get(txt, marks[**cmd - 'a']);
1124 else if (**cmd == '<')
1125 pos = text_mark_get(txt, marks[VIS_MARK_SELECTION_START]);
1126 else if (**cmd == '>')
1127 pos = text_mark_get(txt, marks[VIS_MARK_SELECTION_END]);
1128 (*cmd)++;
1129 break;
1130 case '/':
1131 (*cmd)++;
1132 char *pattern_end = strchr(*cmd, '/');
1133 if (!pattern_end)
1134 return EPOS;
1135 *pattern_end++ = '\0';
1136 Regex *regex = text_regex_new();
1137 if (!regex)
1138 return EPOS;
1139 if (!text_regex_compile(regex, *cmd, 0)) {
1140 *cmd = pattern_end;
1141 pos = text_search_forward(txt, view_cursor_get(view), regex);
1143 text_regex_free(regex);
1144 break;
1145 case '+':
1146 case '-':
1148 CursorPos curspos = view_cursor_getpos(view);
1149 long long line = curspos.line + strtoll(*cmd, cmd, 10);
1150 if (line < 0)
1151 line = 0;
1152 pos = text_pos_by_lineno(txt, line);
1153 break;
1155 default:
1156 if ('0' <= **cmd && **cmd <= '9')
1157 pos = text_pos_by_lineno(txt, strtoul(*cmd, cmd, 10));
1158 break;
1161 return pos;
1164 static Filerange parse_range(Win *win, char **cmd) {
1165 Text *txt = win->file->text;
1166 Filerange r = text_range_empty();
1167 Mark *marks = win->file->marks;
1168 char start = **cmd;
1169 switch (**cmd) {
1170 case '%':
1171 r.start = 0;
1172 r.end = text_size(txt);
1173 (*cmd)++;
1174 break;
1175 case '*':
1176 r.start = text_mark_get(txt, marks[VIS_MARK_SELECTION_START]);
1177 r.end = text_mark_get(txt, marks[VIS_MARK_SELECTION_END]);
1178 (*cmd)++;
1179 break;
1180 default:
1181 r.start = parse_pos(win, cmd);
1182 if (**cmd != ',') {
1183 if (start == '.')
1184 r.end = text_line_next(txt, r.start);
1185 return r;
1187 (*cmd)++;
1188 r.end = parse_pos(win, cmd);
1189 break;
1191 return r;
1194 static Command *lookup_cmd(Vis *vis, const char *name) {
1195 if (!vis->cmds) {
1196 if (!(vis->cmds = map_new()))
1197 return NULL;
1199 for (Command *cmd = cmds; cmd && cmd->name[0]; cmd++) {
1200 for (const char **name = cmd->name; *name; name++)
1201 map_put(vis->cmds, *name, cmd);
1204 return map_closest(vis->cmds, name);
1207 bool vis_cmd(Vis *vis, const char *cmdline) {
1208 enum CmdOpt opt = CMD_OPT_NONE;
1209 while (*cmdline == ':')
1210 cmdline++;
1211 size_t len = strlen(cmdline);
1212 char *line = malloc(len+2);
1213 if (!line)
1214 return false;
1215 strncpy(line, cmdline, len+1);
1217 for (char *end = line + len - 1; end >= line && isspace((unsigned char)*end); end--)
1218 *end = '\0';
1220 char *name = line;
1222 Filerange range = parse_range(vis->win, &name);
1223 if (!text_range_valid(&range)) {
1224 /* if only one position was given, jump to it */
1225 if (range.start != EPOS && !*name) {
1226 view_cursor_to(vis->win->view, range.start);
1227 free(line);
1228 return true;
1231 if (name != line) {
1232 vis_info_show(vis, "Invalid range\n");
1233 free(line);
1234 return false;
1237 /* skip leading white space */
1238 while (*name == ' ')
1239 name++;
1240 char *param = name;
1241 while (*param && (isalpha((unsigned char)*param) || *param == '-' || *param == '|'))
1242 param++;
1244 if (*param == '!') {
1245 if (param != name) {
1246 opt |= CMD_OPT_FORCE;
1247 *param = ' ';
1248 } else {
1249 param++;
1253 memmove(param+1, param, strlen(param)+1);
1254 *param++ = '\0'; /* separate command name from parameters */
1256 Command *cmd = lookup_cmd(vis, name);
1257 if (!cmd) {
1258 vis_info_show(vis, "Not an editor command");
1259 free(line);
1260 return false;
1263 char *s = param;
1264 const char *argv[32] = { name };
1265 for (int i = 1; i < LENGTH(argv); i++) {
1266 while (s && isspace((unsigned char)*s))
1267 s++;
1268 if (s && !*s)
1269 s = NULL;
1270 argv[i] = s;
1271 if (!(cmd->opt & CMD_OPT_ARGS)) {
1272 /* remove trailing spaces */
1273 if (s) {
1274 while (*s) s++;
1275 while (*(--s) == ' ') *s = '\0';
1277 s = NULL;
1279 if (s) {
1280 while (*s && !isspace((unsigned char)*s))
1281 s++;
1282 if (*s)
1283 *s++ = '\0';
1285 /* strip out a single '!' argument to make ":q !" work */
1286 if (argv[i] && !strcmp(argv[i], "!")) {
1287 opt |= CMD_OPT_FORCE;
1288 i--;
1292 cmd->cmd(vis, &range, opt, argv);
1293 free(line);
1294 return true;