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
[]);
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)
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
)) {
148 for (const char **f
= (const char*[]){"0", "false", "no", "off", NULL
}; *f
; f
++) {
149 if (!strcasecmp(s
, *f
)) {
157 static bool cmd_set(Vis
*vis
, Filerange
*range
, enum CmdOpt cmdopt
, const char *argv
[]) {
160 const char *names
[3];
177 OPTION_NUMBER_RELATIVE
,
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
},
198 if (!(vis
->options
= map_new()))
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
]))
210 vis_info_show(vis
, "Expecting: set option [value]");
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
)
227 opt
= map_closest(vis
->options
, argv
[1]);
229 vis_info_show(vis
, "Unknown option: `%s'", argv
[1]);
234 case OPTION_TYPE_STRING
:
235 if (!opt
->optional
&& !argv
[2]) {
236 vis_info_show(vis
, "Expecting string option value");
241 case OPTION_TYPE_BOOL
:
244 } else if (!parse_bool(argv
[2], &arg
.b
)) {
245 vis_info_show(vis
, "Expecting boolean option value not: `%s'", argv
[2]);
251 case OPTION_TYPE_NUMBER
:
253 vis_info_show(vis
, "Expecting number");
256 /* TODO: error checking? long type */
257 arg
.i
= strtoul(argv
[2], NULL
, 10);
261 switch (opt
->index
) {
262 case OPTION_EXPANDTAB
:
263 vis
->expandtab
= arg
.b
;
265 case OPTION_AUTOINDENT
:
266 vis
->autoindent
= arg
.b
;
268 case OPTION_TABWIDTH
:
269 tabwidth_set(vis
, arg
.i
);
273 const char *syntax
= view_syntax_get(vis
->win
->view
);
275 vis_info_show(vis
, "Syntax definition in use: `%s'", syntax
);
277 vis_info_show(vis
, "No syntax definition in use");
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]);
290 vis_info_show(vis
, "Expecting: spaces, tabs, newlines");
293 char *keys
[] = { "spaces", "tabs", "newlines" };
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) {
304 } else if (strstr(*args
, keys
[i
]) == *args
) {
306 const char *v
= *args
+ strlen(keys
[i
]);
307 if (*v
== '=' && parse_bool(v
+1, &show
)) {
316 view_options_set(vis
->win
->view
, flags
);
318 case OPTION_NUMBER
: {
319 enum UiOption opt
= view_options_get(vis
->win
->view
);
321 opt
&= ~UI_OPTION_LINE_NUMBERS_RELATIVE
;
322 opt
|= UI_OPTION_LINE_NUMBERS_ABSOLUTE
;
324 opt
&= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE
;
326 view_options_set(vis
->win
->view
, opt
);
329 case OPTION_NUMBER_RELATIVE
: {
330 enum UiOption opt
= view_options_get(vis
->win
->view
);
332 opt
&= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE
;
333 opt
|= UI_OPTION_LINE_NUMBERS_RELATIVE
;
335 opt
&= ~UI_OPTION_LINE_NUMBERS_RELATIVE
;
337 view_options_set(vis
->win
->view
, opt
);
340 case OPTION_CURSOR_LINE
: {
341 enum UiOption opt
= view_options_get(vis
->win
->view
);
343 opt
|= UI_OPTION_CURSOR_LINE
;
345 opt
&= ~UI_OPTION_CURSOR_LINE
;
346 view_options_set(vis
->win
->view
, opt
);
350 if (!vis_theme_load(vis
, arg
.s
)) {
351 vis_info_show(vis
, "Failed to load theme: `%s'", arg
.s
);
355 case OPTION_COLOR_COLUMN
:
356 view_colorcolumn_set(vis
->win
->view
, arg
.i
);
363 static bool is_file_pattern(const char *pattern
) {
367 if (stat(pattern
, &meta
) == 0 && S_ISDIR(meta
.st_mode
))
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
))
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 */
378 static char filename
[PATH_MAX
];
379 Filerange range
= text_range_empty();
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
);
387 snprintf(vis_open
, sizeof(vis_open
)-1, "vis-open %s", pattern
? pattern
: "");
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
))
399 text_bytes_get(txt
, 0, --len
, filename
);
400 filename
[len
] = '\0';
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
);
417 if (!vis_window_new(vis
, file
)) {
418 vis_info_show(vis
, "Could not open `%s' %s", file
,
419 errno
? strerror(errno
) : "");
426 static bool cmd_open(Vis
*vis
, Filerange
*range
, enum CmdOpt opt
, const char *argv
[]) {
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
);
443 return vis_window_reload(oldwin
);
444 if (!openfiles(vis
, &argv
[1]))
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
)
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
);
464 vis_window_close(vis
->win
);
465 if (!has_windows(vis
))
466 vis_exit(vis
, EXIT_SUCCESS
);
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
))
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
);
484 for (Win
*next
, *win
= vis
->windows
; win
; win
= next
) {
486 if (win
->file
->text
== txt
)
487 vis_window_close(win
);
489 if (!has_windows(vis
))
490 vis_exit(vis
, EXIT_SUCCESS
);
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
) {
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
);
504 info_unsaved_changes(vis
);
509 static bool cmd_read(Vis
*vis
, Filerange
*range
, enum CmdOpt opt
, const char *argv
[]) {
513 vis_info_show(vis
, "Filename or command expected");
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
});
529 text_delete_range(vis
->win
->file
->text
, &delete);
533 static bool cmd_substitute(Vis
*vis
, Filerange
*range
, enum CmdOpt opt
, const char *argv
[]) {
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
);
545 return vis_window_split(vis
->win
);
546 bool ret
= openfiles(vis
, &argv
[1]);
547 view_options_set(vis
->win
->view
, options
);
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
);
555 return vis_window_split(vis
->win
);
556 bool ret
= openfiles(vis
, &argv
[1]);
557 view_options_set(vis
->win
->view
, options
);
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
);
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
) };
583 argv
[1] = file
->name
;
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");
592 /* make sure the file is marked as saved i.e. not modified */
593 text_save_range(text
, range
, NULL
);
596 vis_info_show(vis
, "No filename given, use 'wq' to write to stdout");
599 vis_info_show(vis
, "Filename expected");
603 if (argv
[1][0] == '!') {
605 return cmd_pipe(vis
, range
, opt
, argv
);
608 for (const char **name
= &argv
[1]; *name
; name
++) {
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");
615 if (!text_save_range(text
, range
, *name
)) {
616 vis_info_show(vis
, "Can't write `%s'", *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
);
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
);
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
;
653 rout
= (Filerange
){ .start
= pos
, .end
= pos
};
657 if (pipe(pout
) == -1) {
663 if (pipe(perr
) == -1) {
671 vis
->ui
->terminal_save(vis
->ui
);
681 vis_info_show(vis
, "fork failure: %s", strerror(errno
));
683 } else if (pid
== 0) { /* child i.e filter */
685 dup2(pin
[0], STDIN_FILENO
);
688 dup2(pout
[1], STDOUT_FILENO
);
692 dup2(perr
[1], STDERR_FILENO
);
696 execl("/bin/sh", "sh", "-c", argv
[1], NULL
);
698 execvp(argv
[1], (char**)argv
+1);
699 vis_info_show(vis
, "exec failure: %s", strerror(errno
));
703 vis
->cancel_filter
= false;
709 fcntl(pout
[0], F_SETFL
, O_NONBLOCK
);
710 fcntl(perr
[0], F_SETFL
, O_NONBLOCK
);
716 if (vis
->cancel_filter
) {
724 FD_SET(pin
[1], &wfds
);
726 FD_SET(pout
[0], &rfds
);
728 FD_SET(perr
[0], &rfds
);
730 if (select(FD_SETSIZE
, &rfds
, &wfds
, NULL
, NULL
) == -1) {
733 vis_info_show(vis
, "Select failure");
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]);
744 if (text_range_size(&rout
) == 0) {
752 vis_info_show(vis
, "Error writing to external command");
756 if (pout
[0] != -1 && FD_ISSET(pout
[0], &rfds
)) {
758 ssize_t len
= read(pout
[0], buf
, sizeof buf
);
761 (*read_stdout
)(context
, buf
, len
);
762 } else if (len
== 0) {
765 } else if (errno
!= EINTR
&& errno
!= EWOULDBLOCK
) {
766 vis_info_show(vis
, "Error reading from filter stdout");
772 if (perr
[0] != -1 && FD_ISSET(perr
[0], &rfds
)) {
774 ssize_t len
= read(perr
[0], buf
, sizeof buf
);
777 (*read_stderr
)(context
, buf
, len
);
778 } else if (len
== 0) {
781 } else if (errno
!= EINTR
&& errno
!= EWOULDBLOCK
) {
782 vis_info_show(vis
, "Error reading from filter stderr");
788 } while (pin
[1] != -1 || pout
[0] != -1 || perr
[0] != -1);
797 for (pid_t died
; (died
= waitpid(pid
, &status
, 0)) != -1 && pid
!= died
;);
799 vis
->ui
->terminal_restore(vis
->ui
);
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
);
811 static ssize_t
read_stderr(void *context
, char *data
, size_t len
) {
812 Filter
*filter
= context
;
813 buffer_append(&filter
->err
, data
, 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
;
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:
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
843 int status
= vis_pipe(vis
, &filter
, range
, argv
, read_stdout
, read_stderr
);
846 if (text_range_valid(range
)) {
847 text_delete_range(txt
, range
);
848 view_cursor_to(view
, range
->start
);
852 /* make sure we have somehting to undo */
853 text_insert(txt
, filter
.pos
, " ", 1);
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
);
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
;
878 text_insert(filter
->txt
, filter
->pos
, data
, 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
) };
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
);
906 vis_info_show(vis
, "Command failed");
908 buffer_release(&filter
.err
);
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
;
923 count
= strtol(argv
[1], &unit
, 10);
924 if (errno
|| unit
== argv
[1] || count
< 0) {
925 vis_info_show(vis
, "Invalid number");
930 while (*unit
&& isspace((unsigned char)*unit
))
933 case 'd': count
*= 24; /* fall through */
934 case 'h': count
*= 60; /* fall through */
935 case 'm': count
*= 60; /* fall through */
938 vis_info_show(vis
, "Unknown time specifier (use: s,m,h or d)");
942 if (argv
[0][0] == 'e')
943 count
= -count
; /* earlier, move back in time */
945 pos
= text_restore(txt
, text_state(txt
) + count
);
950 if (argv
[0][0] == 'e')
951 pos
= text_earlier(txt
, count
);
953 pos
= text_later(txt
, count
);
956 time_t state
= text_state(txt
);
958 strftime(buf
, sizeof buf
, "State from %H:%M", localtime(&state
));
959 vis_info_show(vis
, "%s", buf
);
964 static bool print_keybinding(const char *key
, void *value
, void *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
) {
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
))
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
];
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
);
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)
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");
1044 KeyBinding
*binding
= calloc(1, sizeof *binding
);
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;
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
) {
1071 mapped
= vis_window_mode_map(vis
->win
, mode
, lhs
, binding
);
1073 mapped
= vis_mode_map(vis
, mode
, lhs
, binding
);
1075 if (!mapped
&& opt
& CMD_OPT_FORCE
) {
1077 mapped
= vis_window_mode_unmap(vis
->win
, mode
, lhs
) &&
1078 vis_window_mode_map(vis
->win
, mode
, lhs
, binding
);
1080 mapped
= vis_mode_unmap(vis
, mode
, lhs
) &&
1081 vis_mode_map(vis
, mode
, lhs
, binding
);
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");
1101 return vis_window_mode_unmap(vis
->win
, mode
, lhs
);
1103 return vis_mode_unmap(vis
, mode
, lhs
);
1106 static Filepos
parse_pos(Win
*win
, char **cmd
) {
1108 View
*view
= win
->view
;
1109 Text
*txt
= win
->file
->text
;
1110 Mark
*marks
= win
->file
->marks
;
1113 pos
= text_line_begin(txt
, view_cursor_get(view
));
1117 pos
= text_size(txt
);
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
]);
1132 char *pattern_end
= strchr(*cmd
, '/');
1135 *pattern_end
++ = '\0';
1136 Regex
*regex
= text_regex_new();
1139 if (!text_regex_compile(regex
, *cmd
, 0)) {
1141 pos
= text_search_forward(txt
, view_cursor_get(view
), regex
);
1143 text_regex_free(regex
);
1148 CursorPos curspos
= view_cursor_getpos(view
);
1149 long long line
= curspos
.line
+ strtoll(*cmd
, cmd
, 10);
1152 pos
= text_pos_by_lineno(txt
, line
);
1156 if ('0' <= **cmd
&& **cmd
<= '9')
1157 pos
= text_pos_by_lineno(txt
, strtoul(*cmd
, cmd
, 10));
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
;
1172 r
.end
= text_size(txt
);
1176 r
.start
= text_mark_get(txt
, marks
[VIS_MARK_SELECTION_START
]);
1177 r
.end
= text_mark_get(txt
, marks
[VIS_MARK_SELECTION_END
]);
1181 r
.start
= parse_pos(win
, cmd
);
1184 r
.end
= text_line_next(txt
, r
.start
);
1188 r
.end
= parse_pos(win
, cmd
);
1194 static Command
*lookup_cmd(Vis
*vis
, const char *name
) {
1196 if (!(vis
->cmds
= map_new()))
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
== ':')
1211 size_t len
= strlen(cmdline
);
1212 char *line
= malloc(len
+2);
1215 strncpy(line
, cmdline
, len
+1);
1217 for (char *end
= line
+ len
- 1; end
>= line
&& isspace((unsigned char)*end
); end
--)
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
);
1232 vis_info_show(vis
, "Invalid range\n");
1237 /* skip leading white space */
1238 while (*name
== ' ')
1241 while (*param
&& (isalpha((unsigned char)*param
) || *param
== '-' || *param
== '|'))
1244 if (*param
== '!') {
1245 if (param
!= name
) {
1246 opt
|= CMD_OPT_FORCE
;
1253 memmove(param
+1, param
, strlen(param
)+1);
1254 *param
++ = '\0'; /* separate command name from parameters */
1256 Command
*cmd
= lookup_cmd(vis
, name
);
1258 vis_info_show(vis
, "Not an editor command");
1264 const char *argv
[32] = { name
};
1265 for (int i
= 1; i
< LENGTH(argv
); i
++) {
1266 while (s
&& isspace((unsigned char)*s
))
1271 if (!(cmd
->opt
& CMD_OPT_ARGS
)) {
1272 /* remove trailing spaces */
1275 while (*(--s
) == ' ') *s
= '\0';
1280 while (*s
&& !isspace((unsigned char)*s
))
1285 /* strip out a single '!' argument to make ":q !" work */
1286 if (argv
[i
] && !strcmp(argv
[i
], "!")) {
1287 opt
|= CMD_OPT_FORCE
;
1292 cmd
->cmd(vis
, &range
, opt
, argv
);