10 #include <sys/select.h>
11 #include <sys/types.h>
15 #include "text-util.h"
16 #include "text-motions.h"
17 #include "text-objects.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 */
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 */
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)
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
)) {
151 for (const char **f
= (const char*[]){"0", "false", "no", "off", NULL
}; *f
; f
++) {
152 if (!strcasecmp(s
, *f
)) {
160 static bool cmd_set(Vis
*vis
, Filerange
*range
, enum CmdOpt cmdopt
, const char *argv
[]) {
163 const char *names
[3];
180 OPTION_NUMBER_RELATIVE
,
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
},
201 if (!(vis
->options
= map_new()))
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
]))
213 vis_info_show(vis
, "Expecting: set option [value]");
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
)
230 opt
= map_closest(vis
->options
, argv
[1]);
232 vis_info_show(vis
, "Unknown option: `%s'", argv
[1]);
237 case OPTION_TYPE_STRING
:
238 if (!opt
->optional
&& !argv
[2]) {
239 vis_info_show(vis
, "Expecting string option value");
244 case OPTION_TYPE_BOOL
:
247 } else if (!parse_bool(argv
[2], &arg
.b
)) {
248 vis_info_show(vis
, "Expecting boolean option value not: `%s'", argv
[2]);
254 case OPTION_TYPE_NUMBER
:
256 vis_info_show(vis
, "Expecting number");
259 /* TODO: error checking? long type */
260 arg
.i
= strtoul(argv
[2], NULL
, 10);
264 switch (opt
->index
) {
265 case OPTION_EXPANDTAB
:
266 vis
->expandtab
= arg
.b
;
268 case OPTION_AUTOINDENT
:
269 vis
->autoindent
= arg
.b
;
271 case OPTION_TABWIDTH
:
272 tabwidth_set(vis
, arg
.i
);
276 const char *syntax
= view_syntax_get(vis
->win
->view
);
278 vis_info_show(vis
, "Syntax definition in use: `%s'", syntax
);
280 vis_info_show(vis
, "No syntax definition in use");
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]);
293 vis_info_show(vis
, "Expecting: spaces, tabs, newlines");
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) {
307 } else if (strstr(*args
, keys
[i
]) == *args
) {
309 const char *v
= *args
+ strlen(keys
[i
]);
310 if (*v
== '=' && parse_bool(v
+1, &show
)) {
319 view_options_set(vis
->win
->view
, flags
);
321 case OPTION_NUMBER
: {
322 enum UiOption opt
= view_options_get(vis
->win
->view
);
324 opt
&= ~UI_OPTION_LINE_NUMBERS_RELATIVE
;
325 opt
|= UI_OPTION_LINE_NUMBERS_ABSOLUTE
;
327 opt
&= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE
;
329 view_options_set(vis
->win
->view
, opt
);
332 case OPTION_NUMBER_RELATIVE
: {
333 enum UiOption opt
= view_options_get(vis
->win
->view
);
335 opt
&= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE
;
336 opt
|= UI_OPTION_LINE_NUMBERS_RELATIVE
;
338 opt
&= ~UI_OPTION_LINE_NUMBERS_RELATIVE
;
340 view_options_set(vis
->win
->view
, opt
);
343 case OPTION_CURSOR_LINE
: {
344 enum UiOption opt
= view_options_get(vis
->win
->view
);
346 opt
|= UI_OPTION_CURSOR_LINE
;
348 opt
&= ~UI_OPTION_CURSOR_LINE
;
349 view_options_set(vis
->win
->view
, opt
);
353 if (!vis_theme_load(vis
, arg
.s
)) {
354 vis_info_show(vis
, "Failed to load theme: `%s'", arg
.s
);
358 case OPTION_COLOR_COLUMN
:
359 view_colorcolumn_set(vis
->win
->view
, arg
.i
);
366 static bool is_file_pattern(const char *pattern
) {
370 if (stat(pattern
, &meta
) == 0 && S_ISDIR(meta
.st_mode
))
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
))
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 */
381 static char filename
[PATH_MAX
];
382 Filerange range
= text_range_empty();
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
);
390 snprintf(vis_open
, sizeof(vis_open
)-1, "vis-open %s", pattern
? pattern
: "");
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
))
402 text_bytes_get(txt
, 0, --len
, filename
);
403 filename
[len
] = '\0';
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
);
420 if (!vis_window_new(vis
, file
)) {
421 vis_info_show(vis
, "Could not open `%s' %s", file
,
422 errno
? strerror(errno
) : "");
429 static bool cmd_open(Vis
*vis
, Filerange
*range
, enum CmdOpt opt
, const char *argv
[]) {
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
);
446 return vis_window_reload(oldwin
);
447 if (!openfiles(vis
, &argv
[1]))
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
)
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
);
467 vis_window_close(vis
->win
);
468 if (!has_windows(vis
))
469 vis_exit(vis
, EXIT_SUCCESS
);
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
))
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
);
487 for (Win
*next
, *win
= vis
->windows
; win
; win
= next
) {
489 if (win
->file
->text
== txt
)
490 vis_window_close(win
);
492 if (!has_windows(vis
))
493 vis_exit(vis
, EXIT_SUCCESS
);
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
) {
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
);
507 info_unsaved_changes(vis
);
512 static bool cmd_read(Vis
*vis
, Filerange
*range
, enum CmdOpt opt
, const char *argv
[]) {
516 vis_info_show(vis
, "Filename or command expected");
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
});
532 text_delete_range(vis
->win
->file
->text
, &delete);
536 static bool cmd_substitute(Vis
*vis
, Filerange
*range
, enum CmdOpt opt
, const char *argv
[]) {
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
);
548 return vis_window_split(vis
->win
);
549 bool ret
= openfiles(vis
, &argv
[1]);
550 view_options_set(vis
->win
->view
, options
);
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
);
558 return vis_window_split(vis
->win
);
559 bool ret
= openfiles(vis
, &argv
[1]);
560 view_options_set(vis
->win
->view
, options
);
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
);
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
) };
586 argv
[1] = file
->name
;
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");
595 /* make sure the file is marked as saved i.e. not modified */
596 text_save_range(text
, range
, NULL
);
599 vis_info_show(vis
, "No filename given, use 'wq' to write to stdout");
602 vis_info_show(vis
, "Filename expected");
606 if (argv
[1][0] == '!') {
608 return cmd_pipe(vis
, range
, opt
, argv
);
611 for (const char **name
= &argv
[1]; *name
; name
++) {
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");
618 if (!text_save_range(text
, range
, *name
)) {
619 vis_info_show(vis
, "Can't write `%s'", *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
);
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
);
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
;
656 rout
= (Filerange
){ .start
= pos
, .end
= pos
};
660 if (pipe(pout
) == -1) {
666 if (pipe(perr
) == -1) {
674 vis
->ui
->terminal_save(vis
->ui
);
684 vis_info_show(vis
, "fork failure: %s", strerror(errno
));
686 } else if (pid
== 0) { /* child i.e filter */
688 dup2(pin
[0], STDIN_FILENO
);
691 dup2(pout
[1], STDOUT_FILENO
);
695 dup2(perr
[1], STDERR_FILENO
);
699 execl("/bin/sh", "sh", "-c", argv
[1], NULL
);
701 execvp(argv
[1], (char**)argv
+1);
702 vis_info_show(vis
, "exec failure: %s", strerror(errno
));
706 vis
->cancel_filter
= false;
712 fcntl(pout
[0], F_SETFL
, O_NONBLOCK
);
713 fcntl(perr
[0], F_SETFL
, O_NONBLOCK
);
719 if (vis
->cancel_filter
) {
727 FD_SET(pin
[1], &wfds
);
729 FD_SET(pout
[0], &rfds
);
731 FD_SET(perr
[0], &rfds
);
733 if (select(FD_SETSIZE
, &rfds
, &wfds
, NULL
, NULL
) == -1) {
736 vis_info_show(vis
, "Select failure");
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]);
747 if (text_range_size(&rout
) == 0) {
755 vis_info_show(vis
, "Error writing to external command");
759 if (pout
[0] != -1 && FD_ISSET(pout
[0], &rfds
)) {
761 ssize_t len
= read(pout
[0], buf
, sizeof buf
);
764 (*read_stdout
)(context
, buf
, len
);
765 } else if (len
== 0) {
768 } else if (errno
!= EINTR
&& errno
!= EWOULDBLOCK
) {
769 vis_info_show(vis
, "Error reading from filter stdout");
775 if (perr
[0] != -1 && FD_ISSET(perr
[0], &rfds
)) {
777 ssize_t len
= read(perr
[0], buf
, sizeof buf
);
780 (*read_stderr
)(context
, buf
, len
);
781 } else if (len
== 0) {
784 } else if (errno
!= EINTR
&& errno
!= EWOULDBLOCK
) {
785 vis_info_show(vis
, "Error reading from filter stderr");
791 } while (pin
[1] != -1 || pout
[0] != -1 || perr
[0] != -1);
800 for (pid_t died
; (died
= waitpid(pid
, &status
, 0)) != -1 && pid
!= died
;);
802 vis
->ui
->terminal_restore(vis
->ui
);
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
);
814 static ssize_t
read_stderr(void *context
, char *data
, size_t len
) {
815 Filter
*filter
= context
;
816 buffer_append(&filter
->err
, data
, 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
;
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:
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
846 int status
= vis_pipe(vis
, &filter
, range
, argv
, read_stdout
, read_stderr
);
849 if (text_range_valid(range
)) {
850 text_delete_range(txt
, range
);
851 view_cursor_to(view
, range
->start
);
855 /* make sure we have somehting to undo */
856 text_insert(txt
, filter
.pos
, " ", 1);
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
);
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
;
881 text_insert(filter
->txt
, filter
->pos
, data
, 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
) };
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
);
909 vis_info_show(vis
, "Command failed");
911 buffer_release(&filter
.err
);
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
;
926 count
= strtol(argv
[1], &unit
, 10);
927 if (errno
|| unit
== argv
[1] || count
< 0) {
928 vis_info_show(vis
, "Invalid number");
933 while (*unit
&& isspace((unsigned char)*unit
))
936 case 'd': count
*= 24; /* fall through */
937 case 'h': count
*= 60; /* fall through */
938 case 'm': count
*= 60; /* fall through */
941 vis_info_show(vis
, "Unknown time specifier (use: s,m,h or d)");
945 if (argv
[0][0] == 'e')
946 count
= -count
; /* earlier, move back in time */
948 pos
= text_restore(txt
, text_state(txt
) + count
);
953 if (argv
[0][0] == 'e')
954 pos
= text_earlier(txt
, count
);
956 pos
= text_later(txt
, count
);
959 time_t state
= text_state(txt
);
961 strftime(buf
, sizeof buf
, "State from %H:%M", localtime(&state
));
962 vis_info_show(vis
, "%s", buf
);
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
) {
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
) {
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
))
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
];
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
);
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)
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];
1051 if (!latin
|| !nonlatin
) {
1052 vis_info_show(vis
, "usage: langmap <non-latin keys> <latin keys>");
1056 while (*latin
&& *nonlatin
) {
1057 size_t i
= 0, j
= 0;
1058 char latin_key
[8], nonlatin_key
[8];
1060 if (i
< sizeof(latin_key
)-1)
1061 latin_key
[i
++] = *latin
;
1063 } while (!ISUTF8(*latin
));
1065 if (j
< sizeof(nonlatin_key
)-1)
1066 nonlatin_key
[j
++] = *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
));
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");
1088 KeyBinding
*binding
= calloc(1, sizeof *binding
);
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;
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
) {
1115 mapped
= vis_window_mode_map(vis
->win
, mode
, lhs
, binding
);
1117 mapped
= vis_mode_map(vis
, mode
, lhs
, binding
);
1119 if (!mapped
&& opt
& CMD_OPT_FORCE
) {
1121 mapped
= vis_window_mode_unmap(vis
->win
, mode
, lhs
) &&
1122 vis_window_mode_map(vis
->win
, mode
, lhs
, binding
);
1124 mapped
= vis_mode_unmap(vis
, mode
, lhs
) &&
1125 vis_mode_map(vis
, mode
, lhs
, binding
);
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");
1145 return vis_window_mode_unmap(vis
->win
, mode
, lhs
);
1147 return vis_mode_unmap(vis
, mode
, lhs
);
1150 static Filepos
parse_pos(Win
*win
, char **cmd
) {
1152 View
*view
= win
->view
;
1153 Text
*txt
= win
->file
->text
;
1154 Mark
*marks
= win
->file
->marks
;
1157 pos
= text_line_begin(txt
, view_cursor_get(view
));
1161 pos
= text_size(txt
);
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
]);
1176 char *pattern_end
= strchr(*cmd
, '/');
1179 *pattern_end
++ = '\0';
1180 Regex
*regex
= text_regex_new();
1183 if (!text_regex_compile(regex
, *cmd
, 0)) {
1185 pos
= text_search_forward(txt
, view_cursor_get(view
), regex
);
1187 text_regex_free(regex
);
1192 CursorPos curspos
= view_cursor_getpos(view
);
1193 long long line
= curspos
.line
+ strtoll(*cmd
, cmd
, 10);
1196 pos
= text_pos_by_lineno(txt
, line
);
1200 if ('0' <= **cmd
&& **cmd
<= '9')
1201 pos
= text_pos_by_lineno(txt
, strtoul(*cmd
, cmd
, 10));
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
;
1216 r
.end
= text_size(txt
);
1220 r
.start
= text_mark_get(txt
, marks
[VIS_MARK_SELECTION_START
]);
1221 r
.end
= text_mark_get(txt
, marks
[VIS_MARK_SELECTION_END
]);
1225 r
.start
= parse_pos(win
, cmd
);
1228 r
.end
= text_line_next(txt
, r
.start
);
1232 r
.end
= parse_pos(win
, cmd
);
1238 static const Command
*lookup_cmd(Vis
*vis
, const char *name
) {
1240 if (!(vis
->cmds
= map_new()))
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
== ':')
1255 size_t len
= strlen(cmdline
);
1256 char *line
= malloc(len
+2);
1259 strncpy(line
, cmdline
, len
+1);
1261 for (char *end
= line
+ len
- 1; end
>= line
&& isspace((unsigned char)*end
); end
--)
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
);
1276 vis_info_show(vis
, "Invalid range\n");
1281 /* skip leading white space */
1282 while (*name
== ' ')
1285 while (*param
&& (isalpha((unsigned char)*param
) || *param
== '-' || *param
== '|'))
1288 if (*param
== '!') {
1289 if (param
!= name
) {
1290 opt
|= CMD_OPT_FORCE
;
1297 memmove(param
+1, param
, strlen(param
)+1);
1298 *param
++ = '\0'; /* separate command name from parameters */
1300 const Command
*cmd
= lookup_cmd(vis
, name
);
1302 vis_info_show(vis
, "Not an editor command");
1308 const char *argv
[32] = { name
};
1309 for (int i
= 1; i
< LENGTH(argv
); i
++) {
1310 while (s
&& isspace((unsigned char)*s
))
1315 if (!(cmd
->opt
& CMD_OPT_ARGS
)) {
1316 /* remove trailing spaces */
1319 while (*(--s
) == ' ') *s
= '\0';
1324 while (*s
&& !isspace((unsigned char)*s
))
1329 /* strip out a single '!' argument to make ":q !" work */
1330 if (argv
[i
] && !strcmp(argv
[i
], "!")) {
1331 opt
|= CMD_OPT_FORCE
;
1336 cmd
->cmd(vis
, &range
, opt
, argv
);