lexers/pkgbuild: match functions with parentheses
[vis.git] / sam.c
blob038d2dd64347918c8c15ec7fe6249677e214f934
1 /*
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.
19 #include <string.h>
20 #include <strings.h>
21 #include <stdio.h>
22 #include <ctype.h>
23 #include <errno.h>
24 #include <unistd.h>
25 #include <limits.h>
26 #include "sam.h"
27 #include "vis-core.h"
28 #include "buffer.h"
29 #include "text.h"
30 #include "text-motions.h"
31 #include "text-objects.h"
32 #include "text-regex.h"
33 #include "util.h"
35 #define MAX_ARGV 8
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 */
45 } Filter;
47 struct Address {
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 */
55 struct Command {
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 */
66 struct CommandDef {
67 const char *name; /* command name */
68 const char *help; /* short, one-line help text */
69 enum {
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 */
84 } flags;
85 const char *defcmd; /* name of a default target command */
86 bool (*func)(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*); /* command implementation */
89 /* sam commands */
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*);
109 /* vi(m) commands */
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 */
183 typedef struct {
184 const char *names[3]; /* name and optional alias */
185 enum {
186 OPTION_TYPE_STRING,
187 OPTION_TYPE_BOOL,
188 OPTION_TYPE_NUMBER,
189 OPTION_TYPE_UNSIGNED,
190 } type;
191 enum {
192 OPTION_FLAG_OPTIONAL = 1 << 0, /* value is optional */
193 OPTION_FLAG_WINDOW = 1 << 1, /* option requires an active window */
194 } flags;
195 const char *help; /* short, one line help text */
196 } OptionDef;
198 enum {
199 OPTION_AUTOINDENT,
200 OPTION_EXPANDTAB,
201 OPTION_TABWIDTH,
202 OPTION_THEME,
203 OPTION_SYNTAX,
204 OPTION_SHOW,
205 OPTION_NUMBER,
206 OPTION_NUMBER_RELATIVE,
207 OPTION_CURSOR_LINE,
208 OPTION_COLOR_COLUMN,
209 OPTION_HORIZON,
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()))
228 return false;
229 bool ret = true;
230 for (const CommandDef *cmd = cmds; cmd && cmd->name; cmd++)
231 ret &= map_put(vis->cmds, cmd->name, cmd);
232 return ret;
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) {
256 if (!addr)
257 return;
258 text_regex_free(addr->regex);
259 address_free(addr->left);
260 address_free(addr->right);
261 free(addr);
264 static void skip_spaces(const char **s) {
265 while (**s == ' ' || **s == '\t')
266 (*s)++;
269 static char *parse_until(const char **s, const char *until, const char *escchars) {
270 Buffer buf;
271 buffer_init(&buf);
272 size_t len = strlen(until);
273 bool escaped = false;
275 for (; **s && (!memchr(until, **s, len) || escaped); (*s)++) {
276 if (!escaped && **s == '\\') {
277 escaped = true;
278 continue;
281 char c = **s;
283 if (escaped) {
284 escaped = false;
285 switch (c) {
286 case '\n':
287 continue;
288 case 'n':
289 c = '\n';
290 break;
291 case 't':
292 c = '\t';
293 break;
294 case '\\':
295 break;
296 default:
298 bool delim = memchr(until, c, len);
299 bool esc = escchars && memchr(escchars, c, strlen(escchars));
300 if (!delim && !esc)
301 buffer_append(&buf, "\\", 1);
302 break;
307 if (!buffer_append(&buf, &c, 1)) {
308 buffer_release(&buf);
309 return NULL;
313 if (buffer_length(&buf))
314 buffer_append(&buf, "\0", 1);
316 return buf.data;
319 static char *parse_delimited_text(const char **s) {
320 char delim[2] = { **s, '\0' };
321 if (!delim[0])
322 return NULL;
323 (*s)++;
324 char *text = parse_until(s, delim, NULL);
325 if (**s == delim[0])
326 (*s)++;
327 return text;
330 static char *parse_text(const char **s) {
331 skip_spaces(s);
332 if (**s != '\n')
333 return parse_delimited_text(s);
335 Buffer buf;
336 buffer_init(&buf);
337 const char *start = *s + 1;
338 bool dot = false;
340 for ((*s)++; **s && (!dot || **s != '\n'); (*s)++)
341 dot = (**s == '.');
343 if (!dot || !buffer_put(&buf, start, *s - start - 1) ||
344 !buffer_append(&buf, "\0", 1)) {
345 buffer_release(&buf);
346 return NULL;
349 return buf.data;
352 static char *parse_shellcmd(const char **s) {
353 skip_spaces(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++) {
359 skip_spaces(s);
360 if (**s == '"' || **s == '\'')
361 argv[i] = parse_delimited_text(s);
362 else
363 argv[i] = parse_until(s, " \t\n", "\'\"");
367 static char *parse_cmdname(const char **s) {
368 skip_spaces(s);
369 Buffer buf;
370 buffer_init(&buf);
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);
378 return buf.data;
381 static Regex *parse_regex(Vis *vis, const char **s) {
382 char *pattern = parse_delimited_text(s);
383 if (!pattern)
384 return NULL;
385 Regex *regex = vis_regex(vis, *pattern ? pattern : NULL);
386 free(pattern);
387 return regex;
390 static int parse_number(const char **s) {
391 char *end = NULL;
392 int number = strtoull(*s, &end, 10);
393 if (end == *s)
394 return 1;
395 *s = end;
396 return number;
399 static Address *address_parse_simple(Vis *vis, const char **s, enum SamError *err) {
401 skip_spaces(s);
403 Address addr = {
404 .type = **s,
405 .regex = NULL,
406 .number = 0,
407 .left = NULL,
408 .right = NULL,
411 switch (addr.type) {
412 case '#': /* character #n */
413 (*s)++;
414 addr.number = parse_number(s);
415 break;
416 case '0': case '1': case '2': case '3': case '4': /* line n */
417 case '5': case '6': case '7': case '8': case '9':
418 addr.type = 'l';
419 addr.number = parse_number(s);
420 break;
421 case '/': /* regexp forwards */
422 case '?': /* regexp backwards */
423 addr.regex = parse_regex(vis, s);
424 if (!addr.regex) {
425 *err = SAM_ERR_REGEX;
426 return NULL;
428 break;
429 case '$': /* end of file */
430 case '.':
431 case '+':
432 case '-':
433 case '%':
434 (*s)++;
435 break;
436 default:
437 return NULL;
440 if ((addr.right = address_parse_simple(vis, s, err))) {
441 switch (addr.right->type) {
442 case '.':
443 case '$':
444 return NULL;
445 case '#':
446 case 'l':
447 case '/':
448 case '?':
449 if (addr.type != '+' && addr.type != '-') {
450 Address *plus = address_new();
451 if (!plus) {
452 address_free(addr.right);
453 return NULL;
455 plus->type = '+';
456 plus->right = addr.right;
457 addr.right = plus;
459 break;
463 Address *ret = address_new();
464 if (!ret) {
465 address_free(addr.right);
466 return NULL;
468 *ret = addr;
469 return ret;
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;
474 skip_spaces(s);
475 addr.type = **s;
476 switch (addr.type) {
477 case ',': /* a1,a2 */
478 case ';': /* a1;a2 */
479 (*s)++;
480 right = address_parse_compound(vis, s, err);
481 if (right && (right->type == ',' || right->type == ';') && !right->left) {
482 *err = SAM_ERR_ADDRESS;
483 goto fail;
485 break;
486 default:
487 return left;
490 addr.left = left;
491 addr.right = right;
493 Address *ret = address_new();
494 if (ret) {
495 *ret = addr;
496 return ret;
499 fail:
500 address_free(left);
501 address_free(right);
502 return NULL;
505 static Command *command_new(const char *name) {
506 Command *cmd = calloc(1, sizeof(Command));
507 if (!cmd)
508 return NULL;
509 if (name && !(cmd->argv[0] = strdup(name))) {
510 free(cmd);
511 return NULL;
513 return cmd;
516 static void command_free(Command *cmd) {
517 if (!cmd)
518 return;
520 for (Command *c = cmd->cmd, *next; c; c = next) {
521 next = c->next;
522 command_free(c);
525 for (const char **args = cmd->argv; *args; args++)
526 free((void*)*args);
527 address_free(cmd->address);
528 text_regex_free(cmd->regex);
529 free(cmd);
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) {
537 if (!**s) {
538 *err = SAM_ERR_COMMAND;
539 return NULL;
541 Command *cmd = command_new(NULL);
542 if (!cmd)
543 return NULL;
545 cmd->address = address_parse_compound(vis, s, err);
546 skip_spaces(s);
548 cmd->argv[0] = parse_cmdname(s);
550 if (!cmd->argv[0]) {
551 char name[2] = { **s ? **s : 'p', '\0' };
552 if (**s)
553 (*s)++;
554 if (!(cmd->argv[0] = strdup(name)))
555 goto fail;
558 const CommandDef *cmddef = command_lookup(vis, cmd->argv[0]);
559 if (!cmddef) {
560 *err = SAM_ERR_COMMAND;
561 goto fail;
564 cmd->cmddef = cmddef;
566 if (strcmp(cmd->argv[0], "{") == 0) {
567 Command *prev = NULL, *next;
568 do {
569 skip_spaces(s);
570 if (**s == '\n')
571 (*s)++;
572 next = command_parse(vis, s, level+1, err);
573 if (prev)
574 prev->next = next;
575 else
576 cmd->cmd = next;
577 } while ((prev = next));
578 } else if (strcmp(cmd->argv[0], "}") == 0) {
579 if (level == 0) {
580 *err = SAM_ERR_UNMATCHED_BRACE;
581 goto fail;
583 command_free(cmd);
584 return NULL;
587 if (cmddef->flags & CMD_ADDRESS_NONE && cmd->address) {
588 *err = SAM_ERR_NO_ADDRESS;
589 goto fail;
592 if (cmddef->flags & CMD_COUNT)
593 cmd->count = parse_number(s);
595 if (cmddef->flags & CMD_FORCE && **s == '!') {
596 cmd->flags = '!';
597 (*s)++;
600 if (cmddef->flags & CMD_REGEX) {
601 if ((cmddef->flags & CMD_REGEX_DEFAULT) && (!**s || **s == ' ')) {
602 skip_spaces(s);
603 } else if (!(cmd->regex = parse_regex(vis, s))) {
604 *err = SAM_ERR_REGEX;
605 goto fail;
609 if (cmddef->flags & CMD_SHELL && !(cmd->argv[1] = parse_shellcmd(s))) {
610 *err = SAM_ERR_SHELL;
611 goto fail;
614 if (cmddef->flags & CMD_TEXT && !(cmd->argv[1] = parse_text(s))) {
615 *err = SAM_ERR_TEXT;
616 goto fail;
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) {
625 skip_spaces(s);
626 if (cmddef->defcmd && (**s == '\n' || **s == '\0')) {
627 if (**s == '\n')
628 (*s)++;
629 if (!(cmd->cmd = command_new(cmddef->defcmd)))
630 goto fail;
631 cmd->cmd->cmddef = command_lookup(vis, cmddef->defcmd);
632 } else {
633 if (!(cmd->cmd = command_parse(vis, s, level, err)))
634 goto fail;
635 if (strcmp(cmd->argv[0], "X") == 0 || strcmp(cmd->argv[0], "Y") == 0) {
636 Command *sel = command_new("s");
637 if (!sel)
638 goto fail;
639 sel->cmd = cmd->cmd;
640 sel->cmddef = &cmddef_select;
641 cmd->cmd = sel;
646 return cmd;
647 fail:
648 command_free(cmd);
649 return NULL;
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);
655 if (!c)
656 return NULL;
657 Command *sel = command_new("s");
658 if (!sel) {
659 command_free(c);
660 return NULL;
662 sel->cmd = c;
663 sel->cmddef = &cmddef_select;
664 return sel;
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;
671 if (sign > 0) {
672 char c;
673 if (end > 0 && text_byte_get(txt, end-1, &c) && c == '\n')
674 end--;
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;
680 } else {
681 line = text_pos_by_lineno(txt, addr->number);
684 if (addr->type == 'g')
685 return text_range_new(line, line);
686 else
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();
693 do {
694 switch (addr->type) {
695 case '#':
696 if (sign > 0)
697 ret.start = ret.end = range->end + addr->number;
698 else if (sign < 0)
699 ret.start = ret.end = range->start - addr->number;
700 else
701 ret = text_range_new(addr->number, addr->number);
702 break;
703 case 'l':
704 case 'g':
705 ret = address_line_evaluate(addr, file, range, sign);
706 break;
707 case '?':
708 sign = sign == 0 ? -1 : -sign;
709 /* fall through */
710 case '/':
711 if (sign >= 0)
712 ret = text_object_search_forward(file->text, range->end, addr->regex);
713 else
714 ret = text_object_search_backward(file->text, range->start, addr->regex);
715 break;
716 case '$':
718 size_t size = text_size(file->text);
719 ret = text_range_new(size, size);
720 break;
722 case '.':
723 ret = *range;
724 break;
725 case '+':
726 case '-':
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);
730 break;
731 case ',':
732 case ';':
734 Filerange left, right;
735 if (addr->left)
736 left = address_evaluate(addr->left, file, range, 0);
737 else
738 left = text_range_new(0, 0);
740 if (addr->type == ';')
741 range = &left;
743 if (addr->right) {
744 right = address_evaluate(addr->right, file, range, 0);
745 } else {
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);
752 case '%':
753 return text_range_new(0, text_size(file->text));
755 if (text_range_valid(&ret))
756 range = &ret;
757 } while ((addr = addr->right));
759 return ret;
762 static bool sam_execute(Vis *vis, Win *win, Command *cmd, Cursor *cur, Filerange *range) {
763 bool ret = true;
764 if (cmd->address && win)
765 *range = address_evaluate(cmd->address, win->file, range, 0);
767 switch (cmd->argv[0][0]) {
768 case '{':
770 if (!win) {
771 ret = false;
772 break;
774 Text *txt = win->file->text;
775 Mark start, end;
776 Filerange group = *range;
778 for (Command *c = cmd->cmd; c && ret; c = c->next) {
779 if (!text_range_valid(&group))
780 return false;
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 */
789 if (s != EPOS)
790 group.start = s;
791 group.end = text_mark_get(txt, end);
793 break;
795 default:
796 ret = cmd->cmddef->func(vis, win, cmd, cmd->argv, cur, range);
797 break;
799 return ret;
802 enum SamError sam_cmd(Vis *vis, const char *s) {
803 enum SamError err = SAM_ERR_OK;
804 if (!s)
805 return err;
807 Command *cmd = sam_parse(vis, s, &err);
808 if (!cmd) {
809 if (err == SAM_ERR_OK)
810 err = SAM_ERR_MEMORY;
811 return err;
814 Filerange range = text_range_empty();
815 sam_execute(vis, vis->win, cmd, NULL, &range);
817 if (vis->win) {
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)) {
822 completed = false;
823 break;
826 vis_mode_switch(vis, completed ? VIS_MODE_NORMAL : VIS_MODE_VISUAL);
828 command_free(cmd);
829 return err;
832 static bool cmd_insert(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
833 if (!win)
834 return false;
835 size_t len = strlen(argv[1]);
836 bool ret = text_insert(win->file->text, range->start, argv[1], len);
837 if (ret)
838 *range = text_range_new(range->start, range->start + len);
839 return ret;
842 static bool cmd_append(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
843 if (!win)
844 return false;
845 size_t len = strlen(argv[1]);
846 bool ret = text_insert(win->file->text, range->end, argv[1], len);
847 if (ret)
848 *range = text_range_new(range->end, range->end + len);
849 return ret;
852 static bool cmd_change(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
853 if (!win)
854 return false;
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);
859 if (ret)
860 *range = text_range_new(range->start, range->start + len);
861 return ret;
864 static bool cmd_delete(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
865 if (!win)
866 return false;
867 bool ret = text_delete(win->file->text, range->start, text_range_size(range));
868 if (ret)
869 *range = text_range_new(range->start, range->start);
870 return ret;
873 static bool cmd_guard(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
874 if (!win)
875 return false;
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);
881 return true;
884 static bool cmd_extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
885 if (!win)
886 return false;
887 bool ret = true;
888 Text *txt = win->file->text;
890 if (cmd->regex) {
891 size_t start = range->start, end = range->end, last_start = EPOS;
892 RegexMatch match[1];
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();
898 if (found) {
899 if (argv[0][0] == 'x')
900 r = text_range_new(match[0].start, match[0].end);
901 else
902 r = text_range_new(start, match[0].start);
903 if (match[0].start == match[0].end) {
904 if (last_start == match[0].start) {
905 start++;
906 continue;
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)
914 break;
916 start = match[0].end;
917 } else {
918 if (argv[0][0] == 'y')
919 r = text_range_new(start, end);
920 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) {
932 ret = false;
933 break;
937 } else {
938 size_t start = range->start, end = range->end;
939 while (start < end) {
940 size_t next = text_line_next(txt, start);
941 if (next > end)
942 next = end;
943 Filerange r = text_range_new(start, next);
944 if (start == next || !text_range_valid(&r))
945 break;
946 start = next;
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);
951 if (start == EPOS)
952 start = r.end;
953 end = text_mark_get(txt, mark_end);
954 if (end == EPOS) {
955 ret = false;
956 break;
961 view_cursors_dispose(cur);
962 return ret;
965 static bool cmd_select(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
966 Filerange sel = text_range_empty();
967 if (!win)
968 return sam_execute(vis, NULL, cmd->cmd, NULL, &sel);
969 bool ret = true;
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) {
985 case '+':
986 case '-':
987 addr = addr->right;
988 /* fall through */
989 case 'l':
990 if (addr && addr->type == 'l' && !addr->right)
991 addr->type = 'g';
992 break;
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);
1005 } else {
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)
1011 break;
1014 if (vis->win && vis->win->view == view && primary != view_cursors_primary_get(view))
1015 view_cursors_primary_set(view_cursors(view));
1016 return ret;
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))
1021 return false;
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);
1027 if (cur)
1028 view_cursors_to(cur, pos);
1029 else
1030 cur = view_cursors_new_force(view, pos);
1031 if (cur) {
1032 if (range->start != range->end)
1033 view_cursors_selection_set(cur, range);
1034 else
1035 view_cursors_selection_clear(cur);
1037 return cur != NULL;
1040 static bool cmd_files(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1041 bool ret = true;
1042 for (Win *win = vis->windows; win; win = win->next) {
1043 if (win->file->internal)
1044 continue;
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);
1050 return ret;
1053 static bool cmd_substitute(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1054 Buffer buf;
1055 buffer_init(&buf);
1056 bool ret = false;
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);
1060 return ret;
1063 static bool cmd_write(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *r) {
1064 if (!win)
1065 return false;
1066 File *file = win->file;
1067 Text *text = file->text;
1068 bool noname = !argv[1];
1069 if (!argv[1])
1070 argv[1] = file->name ? strdup(file->name) : NULL;
1071 if (!argv[1]) {
1072 if (!file->is_stdin) {
1073 vis_info_show(vis, "Filename expected");
1074 return false;
1076 if (!strchr(argv[0], 'q')) {
1077 vis_info_show(vis, "No filename given, use 'wq' to write to stdout");
1078 return false;
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);
1084 if (all)
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");
1089 return false;
1091 if (all)
1092 break;
1095 /* make sure the file is marked as saved i.e. not modified */
1096 text_save(text, NULL);
1097 return true;
1100 if (noname && cmd->flags != '!' && vis->mode->visual) {
1101 vis_info_show(vis, "WARNING: file will be reduced to active selection");
1102 return false;
1105 for (const char **name = &argv[1]; *name; name++) {
1106 struct stat meta;
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");
1110 return false;
1113 TextSave *ctx = text_save_begin(text, *name);
1114 if (!ctx) {
1115 vis_info_show(vis, "Can't write `%s': %s", *name, strerror(errno));
1116 return false;
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);
1124 if (all)
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));
1129 if (failure) {
1130 text_save_cancel(ctx);
1131 break;
1134 if (all)
1135 break;
1138 if (failure || !text_save_commit(ctx)) {
1139 vis_info_show(vis, "Can't write `%s': %s", *name, strerror(errno));
1140 return false;
1143 if (!file->name)
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);
1150 return true;
1153 static bool cmd_read(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1154 if (!argv[1]) {
1155 vis_info_show(vis, "Filename expected");
1156 return false;
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);
1169 filter->pos += len;
1170 return len;
1173 static ssize_t read_buffer(void *context, char *data, size_t len) {
1174 buffer_append(context, data, len);
1175 return len;
1178 static bool cmd_filter(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1179 if (!win)
1180 return false;
1181 Text *txt = win->file->text;
1183 Filter filter = {
1184 .vis = vis,
1185 .txt = txt,
1186 .pos = range->end,
1189 Buffer buferr;
1190 buffer_init(&buferr);
1192 int status = vis_pipe(vis, range, false, &argv[1], &filter, read_text, &buferr, read_buffer);
1194 if (status == 0) {
1195 text_delete_range(txt, range);
1196 range->end = filter.pos - text_range_size(range);
1197 if (cur)
1198 view_cursors_to(cur, range->start);
1199 } else {
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");
1207 else
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) {
1221 if (!win)
1222 return false;
1223 Filerange filter_range = text_range_new(range->end, range->end);
1224 bool ret = cmd_filter(vis, win, cmd, argv, cur, &filter_range);
1225 if (ret) {
1226 text_delete_range(win->file->text, range);
1227 range->end = range->start + text_range_size(&filter_range);
1228 if (cur)
1229 view_cursors_to(cur, range->start);
1231 return ret;
1234 static bool cmd_pipeout(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1235 if (!win)
1236 return false;
1237 Buffer buferr;
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");
1249 else
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];
1259 if (!dir)
1260 dir = getenv("HOME");
1261 return dir && chdir(dir) == 0;
1264 #include "vis-cmds.c"