2 * Heavily inspired (and partially based upon) the X11 version of
3 * Rob Pike's sam text editor originally written for Plan 9.
5 * Copyright © 2016 Marc André Tanner <mat at brain-dump.org>
6 * Copyright © 1998 by Lucent Technologies
8 * Permission to use, copy, modify, and distribute this software for any
9 * purpose without fee is hereby granted, provided that this entire notice
10 * is included in all copies of any software which is or includes a copy
11 * or modification of this software and in all copies of the supporting
12 * documentation for such software.
14 * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
15 * WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR LUCENT TECHNOLOGIES MAKE ANY
16 * REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY
17 * OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.
30 #include "text-motions.h"
31 #include "text-objects.h"
32 #include "text-regex.h"
37 typedef struct Address Address
;
38 typedef struct Command Command
;
39 typedef struct CommandDef CommandDef
;
41 typedef struct { /* used to keep context when dealing with external proceses */
42 Vis
*vis
; /* editor instance */
43 Text
*txt
; /* text into which received data will be inserted */
44 size_t pos
; /* position at which to insert new data */
48 char type
; /* # (char) l (line) g (goto line) / ? . $ + - , ; % */
49 Regex
*regex
; /* NULL denotes default for x, y, X, and Y commands */
50 size_t number
; /* line or character number */
51 Address
*left
; /* left hand side of a compound address , ; */
52 Address
*right
; /* either right hand side of a compound address or next address */
56 const char *argv
[MAX_ARGV
];/* [0]=cmd-name, [1..MAX_ARGV-2]=arguments, last element always NULL */
57 Address
*address
; /* range of text for command */
58 Regex
*regex
; /* regex to match, used by x, y, g, v, X, Y */
59 const CommandDef
*cmddef
; /* which command is this? */
60 int count
; /* command count if any */
61 char flags
; /* command specific flags */
62 Command
*cmd
; /* target of x, y, g, v, X, Y, { */
63 Command
*next
; /* next command in {} group */
67 const char *name
; /* command name */
68 const char *help
; /* short, one-line help text */
70 CMD_NONE
= 0, /* standalone command without any arguments */
71 CMD_CMD
= 1 << 0, /* does the command take a sub/target command? */
72 CMD_REGEX
= 1 << 1, /* regex after command? */
73 CMD_REGEX_DEFAULT
= 1 << 2, /* is the regex optional i.e. can we use a default? */
74 CMD_COUNT
= 1 << 3, /* does the command support a count as in s2/../? */
75 CMD_TEXT
= 1 << 4, /* does the command need a text to insert? */
76 CMD_ADDRESS_NONE
= 1 << 5, /* is it an error to specify an address for the command? */
77 CMD_ADDRESS_POS
= 1 << 6, /* no address implies an empty range at current cursor position */
78 CMD_ADDRESS_LINE
= 1 << 7, /* if no address is given, use the current line */
79 CMD_ADDRESS_AFTER
= 1 << 8, /* if no address is given, begin at the start of the next line */
80 CMD_SHELL
= 1 << 9, /* command needs a shell command as argument */
81 CMD_FORCE
= 1 << 10, /* can the command be forced with ! */
82 CMD_ARGV
= 1 << 11, /* whether shell like argument splitted is desired */
83 CMD_ONCE
= 1 << 12, /* command should only be executed once, not for every selection */
85 const char *defcmd
; /* name of a default target command */
86 bool (*func
)(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*); /* command implementation */
90 static bool cmd_insert(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
91 static bool cmd_append(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
92 static bool cmd_change(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
93 static bool cmd_delete(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
94 static bool cmd_guard(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
95 static bool cmd_extract(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
96 static bool cmd_select(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
97 static bool cmd_print(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
98 static bool cmd_files(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
99 static bool cmd_pipein(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
100 static bool cmd_pipeout(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
101 static bool cmd_filter(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
102 static bool cmd_launch(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
103 static bool cmd_substitute(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
104 static bool cmd_write(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
105 static bool cmd_read(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
106 static bool cmd_edit(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
107 static bool cmd_quit(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
108 static bool cmd_cd(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
110 static bool cmd_set(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
111 static bool cmd_open(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
112 static bool cmd_bdelete(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
113 static bool cmd_qall(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
114 static bool cmd_split(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
115 static bool cmd_vsplit(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
116 static bool cmd_new(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
117 static bool cmd_vnew(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
118 static bool cmd_wq(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
119 static bool cmd_earlier_later(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
120 static bool cmd_help(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
121 static bool cmd_map(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
122 static bool cmd_unmap(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
123 static bool cmd_langmap(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
124 static bool cmd_user(Vis
*, Win
*, Command
*, const char *argv
[], Cursor
*, Filerange
*);
126 /* command recognized at the ':'-prompt. commands are found using a unique
127 * prefix match. that is if a command should be available under an abbreviation
128 * which is a prefix for another command it has to be added as an alias. the
129 * long human readable name should always come first */
130 static const CommandDef cmds
[] = {
131 /* name, help flags, default command, implementation */
132 { "a", "Append text after range", CMD_TEXT
, NULL
, cmd_append
},
133 { "c", "Change text in range", CMD_TEXT
, NULL
, cmd_change
},
134 { "d", "Delete text in range", CMD_NONE
, NULL
, cmd_delete
},
135 { "g", "If range contains regexp, run command", CMD_CMD
|CMD_REGEX
, "p", cmd_guard
},
136 { "i", "Insert text before range", CMD_TEXT
, NULL
, cmd_insert
},
137 { "p", "Create selection covering range", CMD_NONE
, NULL
, cmd_print
},
138 { "s", "Substitute text for regexp in range", CMD_SHELL
, NULL
, cmd_substitute
},
139 { "v", "If range does not contain regexp, run command", CMD_CMD
|CMD_REGEX
, "p", cmd_guard
},
140 { "x", "Set range and run command on each match", CMD_CMD
|CMD_REGEX
|CMD_REGEX_DEFAULT
, "p", cmd_extract
},
141 { "y", "As `x` but select unmatched text", CMD_CMD
|CMD_REGEX
, "p", cmd_extract
},
142 { "X", "Run command on files whose name matches", CMD_CMD
|CMD_REGEX
|CMD_REGEX_DEFAULT
, NULL
, cmd_files
},
143 { "Y", "As `X` but select unmatched files", CMD_CMD
|CMD_REGEX
, NULL
, cmd_files
},
144 { ">", "Send range to stdin of command", CMD_SHELL
|CMD_ADDRESS_LINE
, NULL
, cmd_pipeout
},
145 { "<", "Replace range by stdout of command", CMD_SHELL
|CMD_ADDRESS_POS
, NULL
, cmd_pipein
},
146 { "|", "Pipe range through command", CMD_SHELL
|CMD_ADDRESS_POS
, NULL
, cmd_filter
},
147 { "!", "Run the command", CMD_SHELL
|CMD_ONCE
, NULL
, cmd_launch
},
148 { "w", "Write range to named file", CMD_ARGV
|CMD_FORCE
|CMD_ADDRESS_NONE
|CMD_ONCE
, NULL
, cmd_write
},
149 { "r", "Replace range by contents of file", CMD_ARGV
|CMD_ADDRESS_AFTER
, NULL
, cmd_read
},
150 { "{", "Start of command group", CMD_NONE
, NULL
, NULL
},
151 { "}", "End of command group" , CMD_NONE
, NULL
, NULL
},
152 { "e", "Edit file", CMD_ARGV
|CMD_FORCE
|CMD_ONCE
, NULL
, cmd_edit
},
153 { "q", "Quit the current window", CMD_FORCE
|CMD_ONCE
, NULL
, cmd_quit
},
154 { "cd", "Change directory", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_cd
},
155 /* vi(m) related commands */
156 { "bdelete", "Unload file", CMD_FORCE
|CMD_ONCE
, NULL
, cmd_bdelete
},
157 { "help", "Show this help", CMD_ONCE
, NULL
, cmd_help
},
158 { "map", "Map key binding `:map <mode> <lhs> <rhs>`", CMD_ARGV
|CMD_FORCE
|CMD_ONCE
, NULL
, cmd_map
},
159 { "map-window", "As `map` but window local", CMD_ARGV
|CMD_FORCE
|CMD_ONCE
, NULL
, cmd_map
},
160 { "unmap", "Unmap key binding `:unmap <mode> <lhs>`", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_unmap
},
161 { "unmap-window", "As `unmap` but window local", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_unmap
},
162 { "langmap", "Map keyboard layout `:langmap <locale-keys> <latin-keys>`", CMD_ARGV
|CMD_FORCE
|CMD_ONCE
, NULL
, cmd_langmap
},
163 { "new", "Create new window", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_new
},
164 { "open", "Open file", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_open
},
165 { "qall", "Exit vis", CMD_FORCE
|CMD_ONCE
, NULL
, cmd_qall
},
166 { "set", "Set option", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_set
},
167 { "split", "Horizontally split window", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_split
},
168 { "vnew", "As `:new` but split vertically", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_vnew
},
169 { "vsplit", "Vertically split window", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_vsplit
},
170 { "wq", "Write file and quit", CMD_ARGV
|CMD_FORCE
|CMD_ADDRESS_NONE
|CMD_ONCE
, NULL
, cmd_wq
},
171 { "earlier", "Go to older text state", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_earlier_later
},
172 { "later", "Go to newer text state", CMD_ARGV
|CMD_ONCE
, NULL
, cmd_earlier_later
},
173 { NULL
, NULL
, CMD_NONE
, NULL
, NULL
},
176 static const CommandDef cmddef_select
=
177 { NULL
, NULL
, CMD_NONE
, NULL
, cmd_select
};
179 static const CommandDef cmddef_user
=
180 { NULL
, NULL
, CMD_ARGV
|CMD_FORCE
|CMD_ONCE
, NULL
, cmd_user
};
182 /* :set command options */
184 const char *names
[3]; /* name and optional alias */
189 OPTION_TYPE_UNSIGNED
,
192 OPTION_FLAG_OPTIONAL
= 1 << 0, /* value is optional */
193 OPTION_FLAG_WINDOW
= 1 << 1, /* option requires an active window */
195 const char *help
; /* short, one line help text */
206 OPTION_NUMBER_RELATIVE
,
212 static const OptionDef options
[] = {
213 [OPTION_AUTOINDENT
] = { { "autoindent", "ai" }, OPTION_TYPE_BOOL
},
214 [OPTION_EXPANDTAB
] = { { "expandtab", "et" }, OPTION_TYPE_BOOL
},
215 [OPTION_TABWIDTH
] = { { "tabwidth", "tw" }, OPTION_TYPE_NUMBER
},
216 [OPTION_THEME
] = { { "theme" }, OPTION_TYPE_STRING
, },
217 [OPTION_SYNTAX
] = { { "syntax" }, OPTION_TYPE_STRING
, OPTION_FLAG_WINDOW
|OPTION_FLAG_OPTIONAL
},
218 [OPTION_SHOW
] = { { "show" }, OPTION_TYPE_STRING
, OPTION_FLAG_WINDOW
},
219 [OPTION_NUMBER
] = { { "numbers", "nu" }, OPTION_TYPE_BOOL
, OPTION_FLAG_WINDOW
},
220 [OPTION_NUMBER_RELATIVE
] = { { "relativenumbers", "rnu" }, OPTION_TYPE_BOOL
, OPTION_FLAG_WINDOW
},
221 [OPTION_CURSOR_LINE
] = { { "cursorline", "cul" }, OPTION_TYPE_BOOL
, OPTION_FLAG_WINDOW
},
222 [OPTION_COLOR_COLUMN
] = { { "colorcolumn", "cc" }, OPTION_TYPE_NUMBER
, OPTION_FLAG_WINDOW
},
223 [OPTION_HORIZON
] = { { "horizon" }, OPTION_TYPE_UNSIGNED
, OPTION_FLAG_WINDOW
},
226 bool sam_init(Vis
*vis
) {
227 if (!(vis
->cmds
= map_new()))
230 for (const CommandDef
*cmd
= cmds
; cmd
&& cmd
->name
; cmd
++)
231 ret
&= map_put(vis
->cmds
, cmd
->name
, cmd
);
235 const char *sam_error(enum SamError err
) {
236 static const char *error_msg
[] = {
237 [SAM_ERR_OK
] = "Success",
238 [SAM_ERR_MEMORY
] = "Out of memory",
239 [SAM_ERR_ADDRESS
] = "Bad address",
240 [SAM_ERR_NO_ADDRESS
] = "Command takes no address",
241 [SAM_ERR_UNMATCHED_BRACE
] = "Unmatched `}'",
242 [SAM_ERR_REGEX
] = "Bad regular expression",
243 [SAM_ERR_TEXT
] = "Bad text",
244 [SAM_ERR_COMMAND
] = "Unknown command",
245 [SAM_ERR_EXECUTE
] = "Error executing command",
248 return err
< LENGTH(error_msg
) ? error_msg
[err
] : NULL
;
251 static Address
*address_new(void) {
252 return calloc(1, sizeof(Address
));
255 static void address_free(Address
*addr
) {
258 text_regex_free(addr
->regex
);
259 address_free(addr
->left
);
260 address_free(addr
->right
);
264 static void skip_spaces(const char **s
) {
265 while (**s
== ' ' || **s
== '\t')
269 static char *parse_until(const char **s
, const char *until
, const char *escchars
) {
272 size_t len
= strlen(until
);
273 bool escaped
= false;
275 for (; **s
&& (!memchr(until
, **s
, len
) || escaped
); (*s
)++) {
276 if (!escaped
&& **s
== '\\') {
298 bool delim
= memchr(until
, c
, len
);
299 bool esc
= escchars
&& memchr(escchars
, c
, strlen(escchars
));
301 buffer_append(&buf
, "\\", 1);
307 if (!buffer_append(&buf
, &c
, 1)) {
308 buffer_release(&buf
);
313 if (buffer_length(&buf
))
314 buffer_append(&buf
, "\0", 1);
319 static char *parse_delimited_text(const char **s
) {
320 char delim
[2] = { **s
, '\0' };
324 char *text
= parse_until(s
, delim
, NULL
);
330 static char *parse_text(const char **s
) {
333 return parse_delimited_text(s
);
337 const char *start
= *s
+ 1;
340 for ((*s
)++; **s
&& (!dot
|| **s
!= '\n'); (*s
)++)
343 if (!dot
|| !buffer_put(&buf
, start
, *s
- start
- 1) ||
344 !buffer_append(&buf
, "\0", 1)) {
345 buffer_release(&buf
);
352 static char *parse_shellcmd(const char **s
) {
354 return parse_until(s
, "\n", NULL
);
357 static void parse_argv(const char **s
, const char *argv
[], size_t maxarg
) {
358 for (size_t i
= 0; i
< maxarg
; i
++) {
360 if (**s
== '"' || **s
== '\'')
361 argv
[i
] = parse_delimited_text(s
);
363 argv
[i
] = parse_until(s
, " \t\n", "\'\"");
367 static char *parse_cmdname(const char **s
) {
372 while (**s
&& **s
!= ' ' && (!ispunct((unsigned char)**s
) || **s
== '-'))
373 buffer_append(&buf
, (*s
)++, 1);
375 if (buffer_length(&buf
))
376 buffer_append(&buf
, "\0", 1);
381 static Regex
*parse_regex(Vis
*vis
, const char **s
) {
382 char *pattern
= parse_delimited_text(s
);
385 Regex
*regex
= vis_regex(vis
, *pattern
? pattern
: NULL
);
390 static int parse_number(const char **s
) {
392 int number
= strtoull(*s
, &end
, 10);
399 static Address
*address_parse_simple(Vis
*vis
, const char **s
, enum SamError
*err
) {
412 case '#': /* character #n */
414 addr
.number
= parse_number(s
);
416 case '0': case '1': case '2': case '3': case '4': /* line n */
417 case '5': case '6': case '7': case '8': case '9':
419 addr
.number
= parse_number(s
);
421 case '/': /* regexp forwards */
422 case '?': /* regexp backwards */
423 addr
.regex
= parse_regex(vis
, s
);
425 *err
= SAM_ERR_REGEX
;
429 case '$': /* end of file */
440 if ((addr
.right
= address_parse_simple(vis
, s
, err
))) {
441 switch (addr
.right
->type
) {
449 if (addr
.type
!= '+' && addr
.type
!= '-') {
450 Address
*plus
= address_new();
452 address_free(addr
.right
);
456 plus
->right
= addr
.right
;
463 Address
*ret
= address_new();
465 address_free(addr
.right
);
472 static Address
*address_parse_compound(Vis
*vis
, const char **s
, enum SamError
*err
) {
473 Address addr
= { 0 }, *left
= address_parse_simple(vis
, s
, err
), *right
= NULL
;
477 case ',': /* a1,a2 */
478 case ';': /* a1;a2 */
480 right
= address_parse_compound(vis
, s
, err
);
481 if (right
&& (right
->type
== ',' || right
->type
== ';') && !right
->left
) {
482 *err
= SAM_ERR_ADDRESS
;
493 Address
*ret
= address_new();
505 static Command
*command_new(const char *name
) {
506 Command
*cmd
= calloc(1, sizeof(Command
));
509 if (name
&& !(cmd
->argv
[0] = strdup(name
))) {
516 static void command_free(Command
*cmd
) {
520 for (Command
*c
= cmd
->cmd
, *next
; c
; c
= next
) {
525 for (const char **args
= cmd
->argv
; *args
; args
++)
527 address_free(cmd
->address
);
528 text_regex_free(cmd
->regex
);
532 static const CommandDef
*command_lookup(Vis
*vis
, const char *name
) {
533 return map_closest(vis
->cmds
, name
);
536 static Command
*command_parse(Vis
*vis
, const char **s
, int level
, enum SamError
*err
) {
538 *err
= SAM_ERR_COMMAND
;
541 Command
*cmd
= command_new(NULL
);
545 cmd
->address
= address_parse_compound(vis
, s
, err
);
548 cmd
->argv
[0] = parse_cmdname(s
);
551 char name
[2] = { **s
? **s
: 'p', '\0' };
554 if (!(cmd
->argv
[0] = strdup(name
)))
558 const CommandDef
*cmddef
= command_lookup(vis
, cmd
->argv
[0]);
560 *err
= SAM_ERR_COMMAND
;
564 cmd
->cmddef
= cmddef
;
566 if (strcmp(cmd
->argv
[0], "{") == 0) {
567 Command
*prev
= NULL
, *next
;
572 next
= command_parse(vis
, s
, level
+1, err
);
577 } while ((prev
= next
));
578 } else if (strcmp(cmd
->argv
[0], "}") == 0) {
580 *err
= SAM_ERR_UNMATCHED_BRACE
;
587 if (cmddef
->flags
& CMD_ADDRESS_NONE
&& cmd
->address
) {
588 *err
= SAM_ERR_NO_ADDRESS
;
592 if (cmddef
->flags
& CMD_COUNT
)
593 cmd
->count
= parse_number(s
);
595 if (cmddef
->flags
& CMD_FORCE
&& **s
== '!') {
600 if (cmddef
->flags
& CMD_REGEX
) {
601 if ((cmddef
->flags
& CMD_REGEX_DEFAULT
) && (!**s
|| **s
== ' ')) {
603 } else if (!(cmd
->regex
= parse_regex(vis
, s
))) {
604 *err
= SAM_ERR_REGEX
;
609 if (cmddef
->flags
& CMD_SHELL
&& !(cmd
->argv
[1] = parse_shellcmd(s
))) {
610 *err
= SAM_ERR_SHELL
;
614 if (cmddef
->flags
& CMD_TEXT
&& !(cmd
->argv
[1] = parse_text(s
))) {
619 if (cmddef
->flags
& CMD_ARGV
) {
620 parse_argv(s
, &cmd
->argv
[1], MAX_ARGV
-2);
621 cmd
->argv
[MAX_ARGV
-1] = NULL
;
624 if (cmddef
->flags
& CMD_CMD
) {
626 if (cmddef
->defcmd
&& (**s
== '\n' || **s
== '\0')) {
629 if (!(cmd
->cmd
= command_new(cmddef
->defcmd
)))
631 cmd
->cmd
->cmddef
= command_lookup(vis
, cmddef
->defcmd
);
633 if (!(cmd
->cmd
= command_parse(vis
, s
, level
, err
)))
635 if (strcmp(cmd
->argv
[0], "X") == 0 || strcmp(cmd
->argv
[0], "Y") == 0) {
636 Command
*sel
= command_new("s");
640 sel
->cmddef
= &cmddef_select
;
652 static Command
*sam_parse(Vis
*vis
, const char *cmd
, enum SamError
*err
) {
653 const char **s
= &cmd
;
654 Command
*c
= command_parse(vis
, s
, 0, err
);
657 Command
*sel
= command_new("s");
663 sel
->cmddef
= &cmddef_select
;
667 static Filerange
address_line_evaluate(Address
*addr
, File
*file
, Filerange
*range
, int sign
) {
668 Text
*txt
= file
->text
;
669 size_t offset
= addr
->number
!= 0 ? addr
->number
: 1;
670 size_t start
= range
->start
, end
= range
->end
, line
;
673 if (end
> 0 && text_byte_get(txt
, end
-1, &c
) && c
== '\n')
675 line
= text_lineno_by_pos(txt
, end
);
676 line
= text_pos_by_lineno(txt
, line
+ offset
);
677 } else if (sign
< 0) {
678 line
= text_lineno_by_pos(txt
, start
);
679 line
= offset
< line
? text_pos_by_lineno(txt
, line
- offset
) : 0;
681 line
= text_pos_by_lineno(txt
, addr
->number
);
684 if (addr
->type
== 'g')
685 return text_range_new(line
, line
);
687 return text_range_new(line
, text_line_next(txt
, line
));
690 static Filerange
address_evaluate(Address
*addr
, File
*file
, Filerange
*range
, int sign
) {
691 Filerange ret
= text_range_empty();
694 switch (addr
->type
) {
697 ret
.start
= ret
.end
= range
->end
+ addr
->number
;
699 ret
.start
= ret
.end
= range
->start
- addr
->number
;
701 ret
= text_range_new(addr
->number
, addr
->number
);
705 ret
= address_line_evaluate(addr
, file
, range
, sign
);
708 sign
= sign
== 0 ? -1 : -sign
;
712 ret
= text_object_search_forward(file
->text
, range
->end
, addr
->regex
);
714 ret
= text_object_search_backward(file
->text
, range
->start
, addr
->regex
);
718 size_t size
= text_size(file
->text
);
719 ret
= text_range_new(size
, size
);
727 sign
= addr
->type
== '+' ? +1 : -1;
728 if (!addr
->right
|| addr
->right
->type
== '+' || addr
->right
->type
== '-')
729 ret
= address_line_evaluate(addr
, file
, range
, sign
);
734 Filerange left
, right
;
736 left
= address_evaluate(addr
->left
, file
, range
, 0);
738 left
= text_range_new(0, 0);
740 if (addr
->type
== ';')
744 right
= address_evaluate(addr
->right
, file
, range
, 0);
746 size_t size
= text_size(file
->text
);
747 right
= text_range_new(size
, size
);
749 /* TODO: enforce strict ordering? */
750 return text_range_union(&left
, &right
);
753 return text_range_new(0, text_size(file
->text
));
755 if (text_range_valid(&ret
))
757 } while ((addr
= addr
->right
));
762 static bool sam_execute(Vis
*vis
, Win
*win
, Command
*cmd
, Cursor
*cur
, Filerange
*range
) {
764 if (cmd
->address
&& win
)
765 *range
= address_evaluate(cmd
->address
, win
->file
, range
, 0);
767 switch (cmd
->argv
[0][0]) {
774 Text
*txt
= win
->file
->text
;
776 Filerange group
= *range
;
778 for (Command
*c
= cmd
->cmd
; c
&& ret
; c
= c
->next
) {
779 if (!text_range_valid(&group
))
782 start
= text_mark_set(txt
, group
.start
);
783 end
= text_mark_set(txt
, group
.end
);
785 ret
&= sam_execute(vis
, win
, c
, NULL
, &group
);
787 size_t s
= text_mark_get(txt
, start
);
788 /* hack to make delete work, only update if still valid */
791 group
.end
= text_mark_get(txt
, end
);
796 ret
= cmd
->cmddef
->func(vis
, win
, cmd
, cmd
->argv
, cur
, range
);
802 enum SamError
sam_cmd(Vis
*vis
, const char *s
) {
803 enum SamError err
= SAM_ERR_OK
;
807 Command
*cmd
= sam_parse(vis
, s
, &err
);
809 if (err
== SAM_ERR_OK
)
810 err
= SAM_ERR_MEMORY
;
814 Filerange range
= text_range_empty();
815 sam_execute(vis
, vis
->win
, cmd
, NULL
, &range
);
818 bool completed
= true;
819 for (Cursor
*c
= view_cursors(vis
->win
->view
); c
; c
= view_cursors_next(c
)) {
820 Filerange sel
= view_cursors_selection_get(c
);
821 if (text_range_valid(&sel
)) {
826 vis_mode_switch(vis
, completed
? VIS_MODE_NORMAL
: VIS_MODE_VISUAL
);
832 static bool cmd_insert(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
835 size_t len
= strlen(argv
[1]);
836 bool ret
= text_insert(win
->file
->text
, range
->start
, argv
[1], len
);
838 *range
= text_range_new(range
->start
, range
->start
+ len
);
842 static bool cmd_append(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
845 size_t len
= strlen(argv
[1]);
846 bool ret
= text_insert(win
->file
->text
, range
->end
, argv
[1], len
);
848 *range
= text_range_new(range
->end
, range
->end
+ len
);
852 static bool cmd_change(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
855 Text
*txt
= win
->file
->text
;
856 size_t len
= strlen(argv
[1]);
857 bool ret
= text_delete(txt
, range
->start
, text_range_size(range
)) &&
858 text_insert(txt
, range
->start
, argv
[1], len
);
860 *range
= text_range_new(range
->start
, range
->start
+ len
);
864 static bool cmd_delete(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
867 bool ret
= text_delete(win
->file
->text
, range
->start
, text_range_size(range
));
869 *range
= text_range_new(range
->start
, range
->start
);
873 static bool cmd_guard(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
876 bool match
= !text_search_range_forward(win
->file
->text
, range
->start
,
877 text_range_size(range
), cmd
->regex
, 0, NULL
, 0);
878 if (match
^ (argv
[0][0] == 'v'))
879 return sam_execute(vis
, win
, cmd
->cmd
, cur
, range
);
880 view_cursors_dispose(cur
);
884 static bool cmd_extract(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
888 Text
*txt
= win
->file
->text
;
891 size_t start
= range
->start
, end
= range
->end
, last_start
= EPOS
;
893 while (start
< end
) {
894 bool found
= text_search_range_forward(txt
, start
,
895 end
- start
, cmd
->regex
, 1, match
,
896 start
> range
->start
? REG_NOTBOL
: 0) == 0;
897 Filerange r
= text_range_empty();
899 if (argv
[0][0] == 'x')
900 r
= text_range_new(match
[0].start
, match
[0].end
);
902 r
= text_range_new(start
, match
[0].start
);
903 if (match
[0].start
== match
[0].end
) {
904 if (last_start
== match
[0].start
) {
908 /* in Plan 9's regexp library ^ matches the beginning
909 * of a line, however in POSIX with REG_NEWLINE ^
910 * matches the zero-length string immediately after a
911 * newline. Try filtering out the last such match at EOF.
913 if (end
== match
[0].start
&& start
> range
->start
)
916 start
= match
[0].end
;
918 if (argv
[0][0] == 'y')
919 r
= text_range_new(start
, end
);
923 if (text_range_valid(&r
)) {
924 Mark mark_start
= text_mark_set(txt
, start
);
925 Mark mark_end
= text_mark_set(txt
, end
);
926 ret
&= sam_execute(vis
, win
, cmd
->cmd
, NULL
, &r
);
927 last_start
= start
= text_mark_get(txt
, mark_start
);
928 if (start
== EPOS
&& last_start
!= r
.end
)
929 last_start
= start
= r
.end
;
930 end
= text_mark_get(txt
, mark_end
);
931 if (start
== EPOS
|| end
== EPOS
) {
938 size_t start
= range
->start
, end
= range
->end
;
939 while (start
< end
) {
940 size_t next
= text_line_next(txt
, start
);
943 Filerange r
= text_range_new(start
, next
);
944 if (start
== next
|| !text_range_valid(&r
))
947 Mark mark_start
= text_mark_set(txt
, start
);
948 Mark mark_end
= text_mark_set(txt
, end
);
949 ret
&= sam_execute(vis
, win
, cmd
->cmd
, NULL
, &r
);
950 start
= text_mark_get(txt
, mark_start
);
953 end
= text_mark_get(txt
, mark_end
);
961 view_cursors_dispose(cur
);
965 static bool cmd_select(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
966 Filerange sel
= text_range_empty();
968 return sam_execute(vis
, NULL
, cmd
->cmd
, NULL
, &sel
);
970 View
*view
= win
->view
;
971 Text
*txt
= win
->file
->text
;
972 bool multiple_cursors
= view_cursors_multiple(view
);
973 Cursor
*primary
= view_cursors_primary_get(view
);
975 for (Cursor
*c
= view_cursors(view
), *next
; c
&& ret
; c
= next
) {
976 next
= view_cursors_next(c
);
977 size_t pos
= view_cursors_pos(c
);
978 if (vis
->mode
->visual
) {
979 sel
= view_cursors_selection_get(c
);
980 } else if (cmd
->cmd
->address
) {
981 /* convert a single line range to a goto line motion */
982 if (!multiple_cursors
&& cmd
->cmd
->cmddef
->func
== cmd_print
) {
983 Address
*addr
= cmd
->cmd
->address
;
984 switch (addr
->type
) {
990 if (addr
&& addr
->type
== 'l' && !addr
->right
)
995 sel
= text_range_new(pos
, pos
);
996 } else if (cmd
->cmd
->cmddef
->flags
& CMD_ADDRESS_POS
) {
997 sel
= text_range_new(pos
, pos
);
998 } else if (cmd
->cmd
->cmddef
->flags
& CMD_ADDRESS_LINE
) {
999 sel
= text_object_line(txt
, pos
);
1000 } else if (cmd
->cmd
->cmddef
->flags
& CMD_ADDRESS_AFTER
) {
1001 size_t next_line
= text_line_next(txt
, pos
);
1002 sel
= text_range_new(next_line
, next_line
);
1003 } else if (multiple_cursors
) {
1004 sel
= text_object_line(txt
, pos
);
1006 sel
= text_range_new(0, text_size(txt
));
1008 if (text_range_valid(&sel
))
1009 ret
&= sam_execute(vis
, win
, cmd
->cmd
, c
, &sel
);
1010 if (cmd
->cmd
->cmddef
->flags
& CMD_ONCE
)
1014 if (vis
->win
&& vis
->win
->view
== view
&& primary
!= view_cursors_primary_get(view
))
1015 view_cursors_primary_set(view_cursors(view
));
1019 static bool cmd_print(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
1020 if (!win
|| !text_range_valid(range
))
1022 View
*view
= win
->view
;
1023 Text
*txt
= win
->file
->text
;
1024 size_t pos
= range
->end
;
1025 if (range
->start
!= range
->end
)
1026 pos
= text_char_prev(txt
, pos
);
1028 view_cursors_to(cur
, pos
);
1030 cur
= view_cursors_new_force(view
, pos
);
1032 if (range
->start
!= range
->end
)
1033 view_cursors_selection_set(cur
, range
);
1035 view_cursors_selection_clear(cur
);
1040 static bool cmd_files(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
1042 for (Win
*win
= vis
->windows
; win
; win
= win
->next
) {
1043 if (win
->file
->internal
)
1045 bool match
= !cmd
->regex
|| (win
->file
->name
&&
1046 text_regex_match(cmd
->regex
, win
->file
->name
, 0));
1047 if (match
^ (argv
[0][0] == 'Y'))
1048 ret
&= sam_execute(vis
, win
, cmd
->cmd
, NULL
, range
);
1053 static bool cmd_substitute(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
1057 if (buffer_put0(&buf
, "s") && buffer_append0(&buf
, argv
[1]))
1058 ret
= cmd_filter(vis
, win
, cmd
, (const char*[]){ argv
[0], "sed", buf
.data
, NULL
}, cur
, range
);
1059 buffer_release(&buf
);
1063 static bool cmd_write(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*r
) {
1066 File
*file
= win
->file
;
1067 Text
*text
= file
->text
;
1068 bool noname
= !argv
[1];
1070 argv
[1] = file
->name
? strdup(file
->name
) : NULL
;
1072 if (!file
->is_stdin
) {
1073 vis_info_show(vis
, "Filename expected");
1076 if (!strchr(argv
[0], 'q')) {
1077 vis_info_show(vis
, "No filename given, use 'wq' to write to stdout");
1081 for (Cursor
*c
= view_cursors(win
->view
); c
; c
= view_cursors_next(c
)) {
1082 Filerange range
= view_cursors_selection_get(c
);
1083 bool all
= !text_range_valid(&range
);
1085 range
= text_range_new(0, text_size(text
));
1086 ssize_t written
= text_write_range(text
, &range
, STDOUT_FILENO
);
1087 if (written
== -1 || (size_t)written
!= text_range_size(&range
)) {
1088 vis_info_show(vis
, "Can not write to stdout");
1095 /* make sure the file is marked as saved i.e. not modified */
1096 text_save(text
, NULL
);
1100 if (noname
&& cmd
->flags
!= '!' && vis
->mode
->visual
) {
1101 vis_info_show(vis
, "WARNING: file will be reduced to active selection");
1105 for (const char **name
= &argv
[1]; *name
; name
++) {
1107 if (cmd
->flags
!= '!' && file
->stat
.st_mtime
&& stat(*name
, &meta
) == 0 &&
1108 file
->stat
.st_mtime
< meta
.st_mtime
) {
1109 vis_info_show(vis
, "WARNING: file has been changed since reading it");
1113 TextSave
*ctx
= text_save_begin(text
, *name
);
1115 vis_info_show(vis
, "Can't write `%s': %s", *name
, strerror(errno
));
1119 bool failure
= false;
1121 for (Cursor
*c
= view_cursors(win
->view
); c
; c
= view_cursors_next(c
)) {
1122 Filerange range
= view_cursors_selection_get(c
);
1123 bool all
= !text_range_valid(&range
);
1125 range
= text_range_new(0, text_size(text
));
1127 ssize_t written
= text_save_write_range(ctx
, &range
);
1128 failure
= (written
== -1 || (size_t)written
!= text_range_size(&range
));
1130 text_save_cancel(ctx
);
1138 if (failure
|| !text_save_commit(ctx
)) {
1139 vis_info_show(vis
, "Can't write `%s': %s", *name
, strerror(errno
));
1144 file_name_set(file
, *name
);
1145 if (strcmp(file
->name
, *name
) == 0)
1146 file
->stat
= text_stat(text
);
1147 if (!file
->internal
&& vis
->event
&& vis
->event
->file_save
)
1148 vis
->event
->file_save(vis
, file
);
1153 static bool cmd_read(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
1155 vis_info_show(vis
, "Filename expected");
1159 const char *args
[MAX_ARGV
] = { argv
[0], "cat", "--" };
1160 for (int i
= 3; i
< MAX_ARGV
-2; i
++)
1161 args
[i
] = argv
[i
-2];
1162 args
[MAX_ARGV
-1] = NULL
;
1163 return cmd_pipein(vis
, win
, cmd
, (const char**)args
, cur
, range
);
1166 static ssize_t
read_text(void *context
, char *data
, size_t len
) {
1167 Filter
*filter
= context
;
1168 text_insert(filter
->txt
, filter
->pos
, data
, len
);
1173 static ssize_t
read_buffer(void *context
, char *data
, size_t len
) {
1174 buffer_append(context
, data
, len
);
1178 static bool cmd_filter(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
1181 Text
*txt
= win
->file
->text
;
1190 buffer_init(&buferr
);
1192 int status
= vis_pipe(vis
, range
, false, &argv
[1], &filter
, read_text
, &buferr
, read_buffer
);
1195 text_delete_range(txt
, range
);
1196 range
->end
= filter
.pos
- text_range_size(range
);
1198 view_cursors_to(cur
, range
->start
);
1200 text_delete(txt
, range
->end
, filter
.pos
- range
->end
);
1203 if (vis
->cancel_filter
)
1204 vis_info_show(vis
, "Command cancelled");
1205 else if (status
== 0)
1206 ; //vis_info_show(vis, "Command succeded");
1208 vis_info_show(vis
, "Command failed %s", buffer_content0(&buferr
));
1210 buffer_release(&buferr
);
1212 return !vis
->cancel_filter
&& status
== 0;
1215 static bool cmd_launch(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
1216 Filerange empty
= text_range_new(cur
? view_cursors_pos(cur
) : range
->start
, EPOS
);
1217 return cmd_filter(vis
, win
, cmd
, argv
, cur
, &empty
);
1220 static bool cmd_pipein(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
1223 Filerange filter_range
= text_range_new(range
->end
, range
->end
);
1224 bool ret
= cmd_filter(vis
, win
, cmd
, argv
, cur
, &filter_range
);
1226 text_delete_range(win
->file
->text
, range
);
1227 range
->end
= range
->start
+ text_range_size(&filter_range
);
1229 view_cursors_to(cur
, range
->start
);
1234 static bool cmd_pipeout(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
1238 buffer_init(&buferr
);
1240 int status
= vis_pipe(vis
, range
, false, (const char*[]){ argv
[1], NULL
}, NULL
, NULL
, &buferr
, read_buffer
);
1242 if (status
== 0 && cur
)
1243 view_cursors_to(cur
, range
->start
);
1245 if (vis
->cancel_filter
)
1246 vis_info_show(vis
, "Command cancelled");
1247 else if (status
== 0)
1248 ; //vis_info_show(vis, "Command succeded");
1250 vis_info_show(vis
, "Command failed %s", buffer_content0(&buferr
));
1252 buffer_release(&buferr
);
1254 return !vis
->cancel_filter
&& status
== 0;
1257 static bool cmd_cd(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Cursor
*cur
, Filerange
*range
) {
1258 const char *dir
= argv
[1];
1260 dir
= getenv("HOME");
1261 return dir
&& chdir(dir
) == 0;
1264 #include "vis-cmds.c"