vis: improve `:<` command implementation
[vis.git] / sam.c
blob19894bf80ead9a21e87694ae6640391f985e2b0b
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-2017 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 struct Change {
42 enum ChangeType {
43 TRANSCRIPT_INSERT = 1 << 0,
44 TRANSCRIPT_DELETE = 1 << 1,
45 TRANSCRIPT_CHANGE = TRANSCRIPT_INSERT|TRANSCRIPT_DELETE,
46 } type;
47 Win *win; /* window in which changed file is being displayed */
48 Cursor *cursor; /* cursor associated with this change, might be NULL */
49 Filerange range; /* inserts are denoted by zero sized range (same start/end) */
50 const char *data; /* will be free(3)-ed after transcript has been processed */
51 size_t len; /* size in bytes of the chunk pointed to by data */
52 Change *next; /* modification position increase monotonically */
55 struct Address {
56 char type; /* # (char) l (line) g (goto line) / ? . $ + - , ; % ' */
57 Regex *regex; /* NULL denotes default for x, y, X, and Y commands */
58 size_t number; /* line or character number */
59 Address *left; /* left hand side of a compound address , ; */
60 Address *right; /* either right hand side of a compound address or next address */
63 typedef struct {
64 int start, end; /* interval [n,m] */
65 bool mod; /* % every n-th match, implies n == m */
66 } Count;
68 struct Command {
69 const char *argv[MAX_ARGV];/* [0]=cmd-name, [1..MAX_ARGV-2]=arguments, last element always NULL */
70 Address *address; /* range of text for command */
71 Regex *regex; /* regex to match, used by x, y, g, v, X, Y */
72 const CommandDef *cmddef; /* which command is this? */
73 Count count; /* command count, defaults to [0,+inf] */
74 int iteration; /* current command loop iteration */
75 char flags; /* command specific flags */
76 Command *cmd; /* target of x, y, g, v, X, Y, { */
77 Command *next; /* next command in {} group */
80 struct CommandDef {
81 const char *name; /* command name */
82 VIS_HELP_DECL(const char *help;) /* short, one-line help text */
83 enum {
84 CMD_NONE = 0, /* standalone command without any arguments */
85 CMD_CMD = 1 << 0, /* does the command take a sub/target command? */
86 CMD_REGEX = 1 << 1, /* regex after command? */
87 CMD_REGEX_DEFAULT = 1 << 2, /* is the regex optional i.e. can we use a default? */
88 CMD_COUNT = 1 << 3, /* does the command support a count as in s2/../? */
89 CMD_TEXT = 1 << 4, /* does the command need a text to insert? */
90 CMD_ADDRESS_NONE = 1 << 5, /* is it an error to specify an address for the command? */
91 CMD_ADDRESS_POS = 1 << 6, /* no address implies an empty range at current cursor position */
92 CMD_ADDRESS_LINE = 1 << 7, /* if no address is given, use the current line */
93 CMD_ADDRESS_AFTER = 1 << 8, /* if no address is given, begin at the start of the next line */
94 CMD_ADDRESS_ALL = 1 << 9, /* if no address is given, apply to whole file (independent of #cursors) */
95 CMD_ADDRESS_ALL_1CURSOR = 1 << 10, /* if no address is given and only 1 cursor exists, apply to whole file */
96 CMD_SHELL = 1 << 11, /* command needs a shell command as argument */
97 CMD_FORCE = 1 << 12, /* can the command be forced with ! */
98 CMD_ARGV = 1 << 13, /* whether shell like argument splitting is desired */
99 CMD_ONCE = 1 << 14, /* command should only be executed once, not for every selection */
100 CMD_LOOP = 1 << 15, /* a looping construct like `x`, `y` */
101 CMD_GROUP = 1 << 16, /* a command group { ... } */
102 CMD_DESTRUCTIVE = 1 << 17, /* command potentially destroys window */
103 } flags;
104 const char *defcmd; /* name of a default target command */
105 bool (*func)(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*); /* command implementation */
108 /* sam commands */
109 static bool cmd_insert(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
110 static bool cmd_append(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
111 static bool cmd_change(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
112 static bool cmd_delete(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
113 static bool cmd_guard(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
114 static bool cmd_extract(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
115 static bool cmd_select(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
116 static bool cmd_print(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
117 static bool cmd_files(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
118 static bool cmd_pipein(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
119 static bool cmd_pipeout(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
120 static bool cmd_filter(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
121 static bool cmd_launch(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
122 static bool cmd_substitute(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
123 static bool cmd_write(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
124 static bool cmd_read(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
125 static bool cmd_edit(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
126 static bool cmd_quit(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
127 static bool cmd_cd(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
128 /* vi(m) commands */
129 static bool cmd_set(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
130 static bool cmd_open(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
131 static bool cmd_qall(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
132 static bool cmd_split(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
133 static bool cmd_vsplit(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
134 static bool cmd_new(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
135 static bool cmd_vnew(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
136 static bool cmd_wq(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
137 static bool cmd_earlier_later(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
138 static bool cmd_help(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
139 static bool cmd_map(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
140 static bool cmd_unmap(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
141 static bool cmd_langmap(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
142 static bool cmd_user(Vis*, Win*, Command*, const char *argv[], Cursor*, Filerange*);
144 static const CommandDef cmds[] = {
145 // name help
146 // flags, default command, implemenation
148 "a", VIS_HELP("Append text after range")
149 CMD_TEXT, NULL, cmd_append
150 }, {
151 "c", VIS_HELP("Change text in range")
152 CMD_TEXT, NULL, cmd_change
153 }, {
154 "d", VIS_HELP("Delete text in range")
155 CMD_NONE, NULL, cmd_delete
156 }, {
157 "g", VIS_HELP("If range contains regexp, run command")
158 CMD_COUNT|CMD_REGEX|CMD_CMD, "p", cmd_guard
159 }, {
160 "i", VIS_HELP("Insert text before range")
161 CMD_TEXT, NULL, cmd_insert
162 }, {
163 "p", VIS_HELP("Create selection covering range")
164 CMD_NONE, NULL, cmd_print
165 }, {
166 "s", VIS_HELP("Substitute: use x/pattern/ c/replacement/ instead")
167 CMD_SHELL|CMD_ADDRESS_LINE, NULL, cmd_substitute
168 }, {
169 "v", VIS_HELP("If range does not contain regexp, run command")
170 CMD_COUNT|CMD_REGEX|CMD_CMD, "p", cmd_guard
171 }, {
172 "x", VIS_HELP("Set range and run command on each match")
173 CMD_CMD|CMD_REGEX|CMD_REGEX_DEFAULT|CMD_ADDRESS_ALL_1CURSOR|CMD_LOOP, "p", cmd_extract
174 }, {
175 "y", VIS_HELP("As `x` but select unmatched text")
176 CMD_CMD|CMD_REGEX|CMD_ADDRESS_ALL_1CURSOR|CMD_LOOP, "p", cmd_extract
177 }, {
178 "X", VIS_HELP("Run command on files whose name matches")
179 CMD_CMD|CMD_REGEX|CMD_REGEX_DEFAULT|CMD_ADDRESS_NONE|CMD_ONCE, NULL, cmd_files
180 }, {
181 "Y", VIS_HELP("As `X` but select unmatched files")
182 CMD_CMD|CMD_REGEX|CMD_ADDRESS_NONE|CMD_ONCE, NULL, cmd_files
183 }, {
184 ">", VIS_HELP("Send range to stdin of command")
185 CMD_SHELL|CMD_ADDRESS_LINE, NULL, cmd_pipeout
186 }, {
187 "<", VIS_HELP("Replace range by stdout of command")
188 CMD_SHELL|CMD_ADDRESS_POS, NULL, cmd_pipein
189 }, {
190 "|", VIS_HELP("Pipe range through command")
191 CMD_SHELL|CMD_ADDRESS_POS, NULL, cmd_filter
192 }, {
193 "!", VIS_HELP("Run the command")
194 CMD_SHELL|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_launch
195 }, {
196 "w", VIS_HELP("Write range to named file")
197 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_ALL, NULL, cmd_write
198 }, {
199 "r", VIS_HELP("Replace range by contents of file")
200 CMD_ARGV|CMD_ADDRESS_AFTER, NULL, cmd_read
201 }, {
202 "{", VIS_HELP("Start of command group")
203 CMD_GROUP, NULL, NULL
204 }, {
205 "}", VIS_HELP("End of command group" )
206 CMD_NONE, NULL, NULL
207 }, {
208 "e", VIS_HELP("Edit file")
209 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE|CMD_DESTRUCTIVE, NULL, cmd_edit
210 }, {
211 "q", VIS_HELP("Quit the current window")
212 CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE|CMD_DESTRUCTIVE, NULL, cmd_quit
213 }, {
214 "cd", VIS_HELP("Change directory")
215 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_cd
217 /* vi(m) related commands */
219 "help", VIS_HELP("Show this help")
220 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_help
221 }, {
222 "map", VIS_HELP("Map key binding `:map <mode> <lhs> <rhs>`")
223 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_map
224 }, {
225 "map-window", VIS_HELP("As `map` but window local")
226 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_map
227 }, {
228 "unmap", VIS_HELP("Unmap key binding `:unmap <mode> <lhs>`")
229 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_unmap
230 }, {
231 "unmap-window", VIS_HELP("As `unmap` but window local")
232 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_unmap
233 }, {
234 "langmap", VIS_HELP("Map keyboard layout `:langmap <locale-keys> <latin-keys>`")
235 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_langmap
236 }, {
237 "new", VIS_HELP("Create new window")
238 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_new
239 }, {
240 "open", VIS_HELP("Open file")
241 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_open
242 }, {
243 "qall", VIS_HELP("Exit vis")
244 CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE|CMD_DESTRUCTIVE, NULL, cmd_qall
245 }, {
246 "set", VIS_HELP("Set option")
247 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_set
248 }, {
249 "split", VIS_HELP("Horizontally split window")
250 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_split
251 }, {
252 "vnew", VIS_HELP("As `:new` but split vertically")
253 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_vnew
254 }, {
255 "vsplit", VIS_HELP("Vertically split window")
256 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_vsplit
257 }, {
258 "wq", VIS_HELP("Write file and quit")
259 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_ALL|CMD_DESTRUCTIVE, NULL, cmd_wq
260 }, {
261 "earlier", VIS_HELP("Go to older text state")
262 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_earlier_later
263 }, {
264 "later", VIS_HELP("Go to newer text state")
265 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_earlier_later
267 { NULL, VIS_HELP(NULL) CMD_NONE, NULL, NULL },
270 static const CommandDef cmddef_select = {
271 NULL, VIS_HELP(NULL) CMD_NONE, NULL, cmd_select
274 /* :set command options */
275 typedef struct {
276 const char *names[3]; /* name and optional alias */
277 enum VisOption flags; /* option type, etc. */
278 VIS_HELP_DECL(const char *help;) /* short, one line help text */
279 VisOptionFunction *func; /* option handler, NULL for bulitins */
280 void *context; /* context passed to option handler function */
281 } OptionDef;
283 enum {
284 OPTION_SHELL,
285 OPTION_ESCDELAY,
286 OPTION_AUTOINDENT,
287 OPTION_EXPANDTAB,
288 OPTION_TABWIDTH,
289 OPTION_SHOW_SPACES,
290 OPTION_SHOW_TABS,
291 OPTION_SHOW_NEWLINES,
292 OPTION_NUMBER,
293 OPTION_NUMBER_RELATIVE,
294 OPTION_CURSOR_LINE,
295 OPTION_COLOR_COLUMN,
296 OPTION_SAVE_METHOD,
297 OPTION_CHANGE_256COLORS,
300 static const OptionDef options[] = {
301 [OPTION_SHELL] = {
302 { "shell" },
303 VIS_OPTION_TYPE_STRING,
304 VIS_HELP("Shell to use for external commands (default: $SHELL, /etc/passwd, /bin/sh)")
306 [OPTION_ESCDELAY] = {
307 { "escdelay" },
308 VIS_OPTION_TYPE_NUMBER,
309 VIS_HELP("Milliseconds to wait to distinguish <Escape> from terminal escape sequences")
311 [OPTION_AUTOINDENT] = {
312 { "autoindent", "ai" },
313 VIS_OPTION_TYPE_BOOL,
314 VIS_HELP("Copy leading white space from previous line")
316 [OPTION_EXPANDTAB] = {
317 { "expandtab", "et" },
318 VIS_OPTION_TYPE_BOOL,
319 VIS_HELP("Replace entered <Tab> with `tabwidth` spaces")
321 [OPTION_TABWIDTH] = {
322 { "tabwidth", "tw" },
323 VIS_OPTION_TYPE_NUMBER,
324 VIS_HELP("Number of spaces to display (and insert if `expandtab` is enabled) for a tab")
326 [OPTION_SHOW_SPACES] = {
327 { "show-spaces" },
328 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
329 VIS_HELP("Display replacement symbol instead of a space")
331 [OPTION_SHOW_TABS] = {
332 { "show-tabs" },
333 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
334 VIS_HELP("Display replacement symbol for tabs")
336 [OPTION_SHOW_NEWLINES] = {
337 { "show-newlines" },
338 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
339 VIS_HELP("Display replacement symbol for newlines")
341 [OPTION_NUMBER] = {
342 { "numbers", "nu" },
343 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
344 VIS_HELP("Display absolute line numbers")
346 [OPTION_NUMBER_RELATIVE] = {
347 { "relativenumbers", "rnu" },
348 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
349 VIS_HELP("Display relative line numbers")
351 [OPTION_CURSOR_LINE] = {
352 { "cursorline", "cul" },
353 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
354 VIS_HELP("Highlight current cursor line")
356 [OPTION_COLOR_COLUMN] = {
357 { "colorcolumn", "cc" },
358 VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
359 VIS_HELP("Highlight a fixed column")
361 [OPTION_SAVE_METHOD] = {
362 { "savemethod" },
363 VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW,
364 VIS_HELP("Save method to use for current file 'auto', 'atomic' or 'inplace'")
366 [OPTION_CHANGE_256COLORS] = {
367 { "change-256colors" },
368 VIS_OPTION_TYPE_BOOL,
369 VIS_HELP("Change 256 color palette to support 24bit colors")
373 bool sam_init(Vis *vis) {
374 if (!(vis->cmds = map_new()))
375 return false;
376 bool ret = true;
377 for (const CommandDef *cmd = cmds; cmd && cmd->name; cmd++)
378 ret &= map_put(vis->cmds, cmd->name, cmd);
380 if (!(vis->options = map_new()))
381 return false;
382 for (int i = 0; i < LENGTH(options); i++) {
383 for (const char *const *name = options[i].names; *name; name++)
384 ret &= map_put(vis->options, *name, &options[i]);
387 return ret;
390 const char *sam_error(enum SamError err) {
391 static const char *error_msg[] = {
392 [SAM_ERR_OK] = "Success",
393 [SAM_ERR_MEMORY] = "Out of memory",
394 [SAM_ERR_ADDRESS] = "Bad address",
395 [SAM_ERR_NO_ADDRESS] = "Command takes no address",
396 [SAM_ERR_UNMATCHED_BRACE] = "Unmatched `}'",
397 [SAM_ERR_REGEX] = "Bad regular expression",
398 [SAM_ERR_TEXT] = "Bad text",
399 [SAM_ERR_SHELL] = "Shell command expected",
400 [SAM_ERR_COMMAND] = "Unknown command",
401 [SAM_ERR_EXECUTE] = "Error executing command",
402 [SAM_ERR_NEWLINE] = "Newline expected",
403 [SAM_ERR_MARK] = "Invalid mark",
404 [SAM_ERR_CONFLICT] = "Conflicting changes",
405 [SAM_ERR_WRITE_CONFLICT] = "Can not write while changing",
406 [SAM_ERR_LOOP_INVALID_CMD] = "Destructive command in looping construct",
407 [SAM_ERR_GROUP_INVALID_CMD] = "Destructive command in group",
408 [SAM_ERR_COUNT] = "Invalid count",
411 size_t idx = err;
412 return idx < LENGTH(error_msg) ? error_msg[idx] : NULL;
415 static void change_free(Change *c) {
416 if (!c)
417 return;
418 free((char*)c->data);
419 free(c);
422 static Change *change_new(Transcript *t, enum ChangeType type, Filerange *range, Win *win, Cursor *cur) {
423 if (!text_range_valid(range))
424 return NULL;
425 Change **prev, *next;
426 if (t->latest && t->latest->range.end <= range->start) {
427 prev = &t->latest->next;
428 next = t->latest->next;
429 } else {
430 prev = &t->changes;
431 next = t->changes;
433 while (next && next->range.end <= range->start) {
434 prev = &next->next;
435 next = next->next;
437 if (next && next->range.start < range->end) {
438 t->error = SAM_ERR_CONFLICT;
439 return NULL;
441 Change *new = calloc(1, sizeof *new);
442 if (new) {
443 new->type = type;
444 new->range = *range;
445 new->cursor = cur;
446 new->win = win;
447 new->next = next;
448 *prev = new;
449 t->latest = new;
451 return new;
454 static void sam_transcript_init(Transcript *t) {
455 memset(t, 0, sizeof *t);
458 static bool sam_transcript_error(Transcript *t, enum SamError error) {
459 if (t->changes)
460 t->error = error;
461 return t->error;
464 static void sam_transcript_free(Transcript *t) {
465 for (Change *c = t->changes, *next; c; c = next) {
466 next = c->next;
467 change_free(c);
471 static bool sam_insert(Win *win, Cursor *cur, size_t pos, const char *data, size_t len) {
472 Filerange range = text_range_new(pos, pos);
473 Change *c = change_new(&win->file->transcript, TRANSCRIPT_INSERT, &range, win, cur);
474 if (c) {
475 c->data = data;
476 c->len = len;
478 return c;
481 static bool sam_delete(Win *win, Cursor *cur, Filerange *range) {
482 return change_new(&win->file->transcript, TRANSCRIPT_DELETE, range, win, cur);
485 static bool sam_change(Win *win, Cursor *cur, Filerange *range, const char *data, size_t len) {
486 Change *c = change_new(&win->file->transcript, TRANSCRIPT_CHANGE, range, win, cur);
487 if (c) {
488 c->data = data;
489 c->len = len;
491 return c;
494 static Address *address_new(void) {
495 Address *addr = calloc(1, sizeof *addr);
496 if (addr)
497 addr->number = EPOS;
498 return addr;
501 static void address_free(Address *addr) {
502 if (!addr)
503 return;
504 text_regex_free(addr->regex);
505 address_free(addr->left);
506 address_free(addr->right);
507 free(addr);
510 static void skip_spaces(const char **s) {
511 while (**s == ' ' || **s == '\t')
512 (*s)++;
515 static char *parse_until(const char **s, const char *until, const char *escchars, int type){
516 Buffer buf;
517 buffer_init(&buf);
518 size_t len = strlen(until);
519 bool escaped = false;
521 for (; **s && (!memchr(until, **s, len) || escaped); (*s)++) {
522 if (type != CMD_SHELL && !escaped && **s == '\\') {
523 escaped = true;
524 continue;
527 char c = **s;
529 if (escaped) {
530 escaped = false;
531 if (c == '\n')
532 continue;
533 if (c == 'n') {
534 c = '\n';
535 } else if (c == 't') {
536 c = '\t';
537 } else if (type != CMD_REGEX && type != CMD_TEXT && c == '\\') {
538 // ignore one of the back slashes
539 } else {
540 bool delim = memchr(until, c, len);
541 bool esc = escchars && memchr(escchars, c, strlen(escchars));
542 if (!delim && !esc)
543 buffer_append(&buf, "\\", 1);
547 if (!buffer_append(&buf, &c, 1)) {
548 buffer_release(&buf);
549 return NULL;
553 buffer_terminate(&buf);
555 return buffer_move(&buf);
558 static char *parse_delimited(const char **s, int type) {
559 char delim[2] = { **s, '\0' };
560 if (!delim[0] || isspace((unsigned char)delim[0]))
561 return NULL;
562 (*s)++;
563 char *chunk = parse_until(s, delim, NULL, type);
564 if (**s == delim[0])
565 (*s)++;
566 return chunk;
569 static char *parse_text(const char **s) {
570 skip_spaces(s);
571 if (**s != '\n') {
572 const char *before = *s;
573 char *text = parse_delimited(s, CMD_TEXT);
574 return (!text && *s != before) ? strdup("") : text;
577 Buffer buf;
578 buffer_init(&buf);
579 const char *start = *s + 1;
580 bool dot = false;
582 for ((*s)++; **s && (!dot || **s != '\n'); (*s)++)
583 dot = (**s == '.');
585 if (!dot || !buffer_put(&buf, start, *s - start - 1) ||
586 !buffer_append(&buf, "\0", 1)) {
587 buffer_release(&buf);
588 return NULL;
591 return buffer_move(&buf);
594 static char *parse_shellcmd(Vis *vis, const char **s) {
595 skip_spaces(s);
596 char *cmd = parse_until(s, "\n", NULL, false);
597 if (!cmd) {
598 const char *last_cmd = register_get(vis, &vis->registers[VIS_REG_SHELL], NULL);
599 return last_cmd ? strdup(last_cmd) : NULL;
601 register_put0(vis, &vis->registers[VIS_REG_SHELL], cmd);
602 return cmd;
605 static void parse_argv(const char **s, const char *argv[], size_t maxarg) {
606 for (size_t i = 0; i < maxarg; i++) {
607 skip_spaces(s);
608 if (**s == '"' || **s == '\'')
609 argv[i] = parse_delimited(s, CMD_ARGV);
610 else
611 argv[i] = parse_until(s, " \t\n", "\'\"", CMD_ARGV);
615 static bool valid_cmdname(const char *s) {
616 unsigned char c = (unsigned char)*s;
617 return c && !isspace(c) && !isdigit(c) && (!ispunct(c) || (c == '-' && valid_cmdname(s+1)));
620 static char *parse_cmdname(const char **s) {
621 Buffer buf;
622 buffer_init(&buf);
624 skip_spaces(s);
625 while (valid_cmdname(*s))
626 buffer_append(&buf, (*s)++, 1);
628 buffer_terminate(&buf);
630 return buffer_move(&buf);
633 static Regex *parse_regex(Vis *vis, const char **s) {
634 const char *before = *s;
635 char *pattern = parse_delimited(s, CMD_REGEX);
636 if (!pattern && *s == before)
637 return NULL;
638 Regex *regex = vis_regex(vis, pattern);
639 free(pattern);
640 return regex;
643 static int parse_number(const char **s) {
644 char *end = NULL;
645 int number = strtoull(*s, &end, 10);
646 if (end == *s)
647 return 0;
648 *s = end;
649 return number;
652 static enum SamError parse_count(const char **s, Count *count) {
653 count->mod = **s == '%';
655 if (count->mod) {
656 (*s)++;
657 int n = parse_number(s);
658 if (!n)
659 return SAM_ERR_COUNT;
660 count->start = n;
661 count->end = n;
662 return SAM_ERR_OK;
665 const char *before = *s;
666 if (!(count->start = parse_number(s)) && *s != before)
667 return SAM_ERR_COUNT;
668 if (**s != ',') {
669 count->end = count->start ? count->start : INT_MAX;
670 return SAM_ERR_OK;
671 } else {
672 (*s)++;
674 before = *s;
675 if (!(count->end = parse_number(s)) && *s != before)
676 return SAM_ERR_COUNT;
677 if (!count->end)
678 count->end = INT_MAX;
679 return SAM_ERR_OK;
682 static Address *address_parse_simple(Vis *vis, const char **s, enum SamError *err) {
684 skip_spaces(s);
686 Address addr = {
687 .type = **s,
688 .regex = NULL,
689 .number = EPOS,
690 .left = NULL,
691 .right = NULL,
694 switch (addr.type) {
695 case '#': /* character #n */
696 (*s)++;
697 addr.number = parse_number(s);
698 break;
699 case '0': case '1': case '2': case '3': case '4': /* line n */
700 case '5': case '6': case '7': case '8': case '9':
701 addr.type = 'l';
702 addr.number = parse_number(s);
703 break;
704 case '\'':
705 (*s)++;
706 if ((addr.number = vis_mark_from(vis, **s)) == VIS_MARK_INVALID) {
707 *err = SAM_ERR_MARK;
708 return NULL;
710 (*s)++;
711 break;
712 case '/': /* regexp forwards */
713 case '?': /* regexp backwards */
714 addr.regex = parse_regex(vis, s);
715 if (!addr.regex) {
716 *err = SAM_ERR_REGEX;
717 return NULL;
719 break;
720 case '$': /* end of file */
721 case '.':
722 case '+':
723 case '-':
724 case '%':
725 (*s)++;
726 break;
727 default:
728 return NULL;
731 if ((addr.right = address_parse_simple(vis, s, err))) {
732 switch (addr.right->type) {
733 case '.':
734 case '$':
735 return NULL;
736 case '#':
737 case 'l':
738 case '/':
739 case '?':
740 if (addr.type != '+' && addr.type != '-') {
741 Address *plus = address_new();
742 if (!plus) {
743 address_free(addr.right);
744 return NULL;
746 plus->type = '+';
747 plus->right = addr.right;
748 addr.right = plus;
750 break;
754 Address *ret = address_new();
755 if (!ret) {
756 address_free(addr.right);
757 return NULL;
759 *ret = addr;
760 return ret;
763 static Address *address_parse_compound(Vis *vis, const char **s, enum SamError *err) {
764 Address addr = { 0 }, *left = address_parse_simple(vis, s, err), *right = NULL;
765 skip_spaces(s);
766 addr.type = **s;
767 switch (addr.type) {
768 case ',': /* a1,a2 */
769 case ';': /* a1;a2 */
770 (*s)++;
771 right = address_parse_compound(vis, s, err);
772 if (right && (right->type == ',' || right->type == ';') && !right->left) {
773 *err = SAM_ERR_ADDRESS;
774 goto fail;
776 break;
777 default:
778 return left;
781 addr.left = left;
782 addr.right = right;
784 Address *ret = address_new();
785 if (ret) {
786 *ret = addr;
787 return ret;
790 fail:
791 address_free(left);
792 address_free(right);
793 return NULL;
796 static Command *command_new(const char *name) {
797 Command *cmd = calloc(1, sizeof(Command));
798 if (!cmd)
799 return NULL;
800 if (name && !(cmd->argv[0] = strdup(name))) {
801 free(cmd);
802 return NULL;
804 return cmd;
807 static void command_free(Command *cmd) {
808 if (!cmd)
809 return;
811 for (Command *c = cmd->cmd, *next; c; c = next) {
812 next = c->next;
813 command_free(c);
816 for (const char **args = cmd->argv; *args; args++)
817 free((void*)*args);
818 address_free(cmd->address);
819 text_regex_free(cmd->regex);
820 free(cmd);
823 static const CommandDef *command_lookup(Vis *vis, const char *name) {
824 return map_closest(vis->cmds, name);
827 static Command *command_parse(Vis *vis, const char **s, enum SamError *err) {
828 if (!**s) {
829 *err = SAM_ERR_COMMAND;
830 return NULL;
832 Command *cmd = command_new(NULL);
833 if (!cmd)
834 return NULL;
836 cmd->address = address_parse_compound(vis, s, err);
837 skip_spaces(s);
839 cmd->argv[0] = parse_cmdname(s);
841 if (!cmd->argv[0]) {
842 char name[2] = { **s ? **s : 'p', '\0' };
843 if (**s)
844 (*s)++;
845 if (!(cmd->argv[0] = strdup(name)))
846 goto fail;
849 const CommandDef *cmddef = command_lookup(vis, cmd->argv[0]);
850 if (!cmddef) {
851 *err = SAM_ERR_COMMAND;
852 goto fail;
855 cmd->cmddef = cmddef;
857 if (strcmp(cmd->argv[0], "{") == 0) {
858 Command *prev = NULL, *next;
859 int level = vis->nesting_level++;
860 do {
861 while (**s == ' ' || **s == '\t' || **s == '\n')
862 (*s)++;
863 next = command_parse(vis, s, err);
864 if (*err)
865 goto fail;
866 if (prev)
867 prev->next = next;
868 else
869 cmd->cmd = next;
870 } while ((prev = next));
871 if (level != vis->nesting_level) {
872 *err = SAM_ERR_UNMATCHED_BRACE;
873 goto fail;
875 } else if (strcmp(cmd->argv[0], "}") == 0) {
876 if (vis->nesting_level-- == 0) {
877 *err = SAM_ERR_UNMATCHED_BRACE;
878 goto fail;
880 command_free(cmd);
881 return NULL;
884 if (cmddef->flags & CMD_ADDRESS_NONE && cmd->address) {
885 *err = SAM_ERR_NO_ADDRESS;
886 goto fail;
889 if (cmddef->flags & CMD_FORCE && **s == '!') {
890 cmd->flags = '!';
891 (*s)++;
894 if ((cmddef->flags & CMD_COUNT) && (*err = parse_count(s, &cmd->count)))
895 goto fail;
897 if (cmddef->flags & CMD_REGEX) {
898 if ((cmddef->flags & CMD_REGEX_DEFAULT) && (!**s || **s == ' ')) {
899 skip_spaces(s);
900 } else {
901 const char *before = *s;
902 cmd->regex = parse_regex(vis, s);
903 if (!cmd->regex && (*s != before || !(cmddef->flags & CMD_COUNT))) {
904 *err = SAM_ERR_REGEX;
905 goto fail;
910 if (cmddef->flags & CMD_SHELL && !(cmd->argv[1] = parse_shellcmd(vis, s))) {
911 *err = SAM_ERR_SHELL;
912 goto fail;
915 if (cmddef->flags & CMD_TEXT && !(cmd->argv[1] = parse_text(s))) {
916 *err = SAM_ERR_TEXT;
917 goto fail;
920 if (cmddef->flags & CMD_ARGV) {
921 parse_argv(s, &cmd->argv[1], MAX_ARGV-2);
922 cmd->argv[MAX_ARGV-1] = NULL;
925 if (cmddef->flags & CMD_CMD) {
926 skip_spaces(s);
927 if (cmddef->defcmd && (**s == '\n' || **s == '}' || **s == '\0')) {
928 if (**s == '\n')
929 (*s)++;
930 if (!(cmd->cmd = command_new(cmddef->defcmd)))
931 goto fail;
932 cmd->cmd->cmddef = command_lookup(vis, cmddef->defcmd);
933 } else {
934 if (!(cmd->cmd = command_parse(vis, s, err)))
935 goto fail;
936 if (strcmp(cmd->argv[0], "X") == 0 || strcmp(cmd->argv[0], "Y") == 0) {
937 Command *sel = command_new("select");
938 if (!sel)
939 goto fail;
940 sel->cmd = cmd->cmd;
941 sel->cmddef = &cmddef_select;
942 cmd->cmd = sel;
947 return cmd;
948 fail:
949 command_free(cmd);
950 return NULL;
953 static Command *sam_parse(Vis *vis, const char *cmd, enum SamError *err) {
954 vis->nesting_level = 0;
955 const char **s = &cmd;
956 Command *c = command_parse(vis, s, err);
957 if (!c)
958 return NULL;
959 while (**s == ' ' || **s == '\t' || **s == '\n')
960 (*s)++;
961 if (**s) {
962 *err = SAM_ERR_NEWLINE;
963 command_free(c);
964 return NULL;
967 Command *sel = command_new("select");
968 if (!sel) {
969 command_free(c);
970 return NULL;
972 sel->cmd = c;
973 sel->cmddef = &cmddef_select;
974 return sel;
977 static Filerange address_line_evaluate(Address *addr, File *file, Filerange *range, int sign) {
978 Text *txt = file->text;
979 size_t offset = addr->number != EPOS ? addr->number : 1;
980 size_t start = range->start, end = range->end, line;
981 if (sign > 0) {
982 char c;
983 if (end > 0 && text_byte_get(txt, end-1, &c) && c == '\n')
984 end--;
985 line = text_lineno_by_pos(txt, end);
986 line = text_pos_by_lineno(txt, line + offset);
987 } else if (sign < 0) {
988 line = text_lineno_by_pos(txt, start);
989 line = offset < line ? text_pos_by_lineno(txt, line - offset) : 0;
990 } else {
991 if (addr->number == 0)
992 return text_range_new(0, 0);
993 line = text_pos_by_lineno(txt, addr->number);
996 if (addr->type == 'g')
997 return text_range_new(line, line);
998 else
999 return text_range_new(line, text_line_next(txt, line));
1002 static Filerange address_evaluate(Address *addr, File *file, Filerange *range, int sign) {
1003 Filerange ret = text_range_empty();
1005 do {
1006 switch (addr->type) {
1007 case '#':
1008 if (sign > 0)
1009 ret.start = ret.end = range->end + addr->number;
1010 else if (sign < 0)
1011 ret.start = ret.end = range->start - addr->number;
1012 else
1013 ret = text_range_new(addr->number, addr->number);
1014 break;
1015 case 'l':
1016 case 'g':
1017 ret = address_line_evaluate(addr, file, range, sign);
1018 break;
1019 case '\'':
1021 size_t pos = text_mark_get(file->text, file->marks[addr->number]);
1022 ret = text_range_new(pos, pos);
1023 break;
1025 case '?':
1026 sign = sign == 0 ? -1 : -sign;
1027 /* fall through */
1028 case '/':
1029 if (sign >= 0)
1030 ret = text_object_search_forward(file->text, range->end, addr->regex);
1031 else
1032 ret = text_object_search_backward(file->text, range->start, addr->regex);
1033 break;
1034 case '$':
1036 size_t size = text_size(file->text);
1037 ret = text_range_new(size, size);
1038 break;
1040 case '.':
1041 ret = *range;
1042 break;
1043 case '+':
1044 case '-':
1045 sign = addr->type == '+' ? +1 : -1;
1046 if (!addr->right || addr->right->type == '+' || addr->right->type == '-')
1047 ret = address_line_evaluate(addr, file, range, sign);
1048 break;
1049 case ',':
1050 case ';':
1052 Filerange left, right;
1053 if (addr->left)
1054 left = address_evaluate(addr->left, file, range, 0);
1055 else
1056 left = text_range_new(0, 0);
1058 if (addr->type == ';')
1059 range = &left;
1061 if (addr->right) {
1062 right = address_evaluate(addr->right, file, range, 0);
1063 } else {
1064 size_t size = text_size(file->text);
1065 right = text_range_new(size, size);
1067 /* TODO: enforce strict ordering? */
1068 return text_range_union(&left, &right);
1070 case '%':
1071 return text_range_new(0, text_size(file->text));
1073 if (text_range_valid(&ret))
1074 range = &ret;
1075 } while ((addr = addr->right));
1077 return ret;
1080 static bool count_evaluate(Command *cmd) {
1081 Count *count = &cmd->count;
1082 if (count->mod)
1083 return count->start ? cmd->iteration % count->start == 0 : true;
1084 return count->start <= cmd->iteration && cmd->iteration <= count->end;
1087 static bool sam_execute(Vis *vis, Win *win, Command *cmd, Cursor *cur, Filerange *range) {
1088 bool ret = true;
1089 if (cmd->address && win)
1090 *range = address_evaluate(cmd->address, win->file, range, 0);
1092 cmd->iteration++;
1093 switch (cmd->argv[0][0]) {
1094 case '{':
1096 for (Command *c = cmd->cmd; c && ret; c = c->next)
1097 ret &= sam_execute(vis, win, c, NULL, range);
1098 view_cursors_dispose_force(cur);
1099 break;
1101 default:
1102 ret = cmd->cmddef->func(vis, win, cmd, cmd->argv, cur, range);
1103 break;
1105 return ret;
1108 static enum SamError validate(Command *cmd, bool loop, bool group) {
1109 if (cmd->cmddef->flags & CMD_DESTRUCTIVE) {
1110 if (loop)
1111 return SAM_ERR_LOOP_INVALID_CMD;
1112 if (group)
1113 return SAM_ERR_GROUP_INVALID_CMD;
1116 group |= (cmd->cmddef->flags & CMD_GROUP);
1117 loop |= (cmd->cmddef->flags & CMD_LOOP);
1118 for (Command *c = cmd->cmd; c; c = c->next) {
1119 enum SamError err = validate(c, loop, group);
1120 if (err != SAM_ERR_OK)
1121 return err;
1123 return SAM_ERR_OK;
1126 static enum SamError command_validate(Command *cmd) {
1127 return validate(cmd, false, false);
1130 static bool count_negative(Command *cmd) {
1131 if (cmd->count.start < 0 || cmd->count.end < 0)
1132 return true;
1133 for (Command *c = cmd->cmd; c; c = c->next) {
1134 if (c->cmddef->func != cmd_extract && c->cmddef->func != cmd_select) {
1135 if (count_negative(c))
1136 return true;
1139 return false;
1142 static void count_init(Command *cmd, int max) {
1143 Count *count = &cmd->count;
1144 cmd->iteration = 0;
1145 if (count->start < 0)
1146 count->start += max;
1147 if (count->end < 0)
1148 count->end += max;
1149 for (Command *c = cmd->cmd; c; c = c->next) {
1150 if (c->cmddef->func != cmd_extract && c->cmddef->func != cmd_select)
1151 count_init(c, max);
1155 enum SamError sam_cmd(Vis *vis, const char *s) {
1156 enum SamError err = SAM_ERR_OK;
1157 if (!s)
1158 return err;
1160 Command *cmd = sam_parse(vis, s, &err);
1161 if (!cmd) {
1162 if (err == SAM_ERR_OK)
1163 err = SAM_ERR_MEMORY;
1164 return err;
1167 err = command_validate(cmd);
1168 if (err != SAM_ERR_OK) {
1169 command_free(cmd);
1170 return err;
1173 for (File *file = vis->files; file; file = file->next) {
1174 if (file->internal)
1175 continue;
1176 sam_transcript_init(&file->transcript);
1179 bool visual = vis->mode->visual;
1180 size_t primary_pos = vis->win ? view_cursor_get(vis->win->view) : EPOS;
1181 Filerange range = text_range_empty();
1182 sam_execute(vis, vis->win, cmd, NULL, &range);
1184 for (File *file = vis->files; file; file = file->next) {
1185 if (file->internal)
1186 continue;
1187 Transcript *t = &file->transcript;
1188 if (t->error != SAM_ERR_OK) {
1189 err = t->error;
1190 sam_transcript_free(t);
1191 continue;
1193 vis_file_snapshot(vis, file);
1194 ptrdiff_t delta = 0;
1195 for (Change *c = t->changes; c; c = c->next) {
1196 c->range.start += delta;
1197 c->range.end += delta;
1198 if (c->type & TRANSCRIPT_DELETE) {
1199 text_delete_range(file->text, &c->range);
1200 delta -= text_range_size(&c->range);
1201 if (c->cursor && c->type == TRANSCRIPT_DELETE) {
1202 if (visual)
1203 view_cursors_dispose_force(c->cursor);
1204 else
1205 view_cursors_to(c->cursor, c->range.start);
1208 if (c->type & TRANSCRIPT_INSERT) {
1209 text_insert(file->text, c->range.start, c->data, c->len);
1210 delta += c->len;
1211 Filerange sel = text_range_new(c->range.start,
1212 c->range.start+c->len);
1213 if (c->cursor) {
1214 if (visual) {
1215 view_cursors_selection_set(c->cursor, &sel);
1216 view_cursors_selection_sync(c->cursor);
1217 } else {
1218 if (memchr(c->data, '\n', c->len))
1219 view_cursors_to(c->cursor, sel.start);
1220 else
1221 view_cursors_to(c->cursor, sel.end);
1223 } else if (visual) {
1224 Cursor *cursor = view_cursors_new(c->win->view, sel.start);
1225 if (cursor) {
1226 view_cursors_selection_set(cursor, &sel);
1227 view_cursors_selection_sync(cursor);
1232 sam_transcript_free(&file->transcript);
1233 vis_file_snapshot(vis, file);
1236 if (vis->win) {
1237 if (primary_pos != EPOS && view_cursor_disposed(vis->win->view))
1238 view_cursor_to(vis->win->view, primary_pos);
1239 view_cursors_primary_set(view_cursors(vis->win->view));
1240 bool completed = true;
1241 for (Cursor *c = view_cursors(vis->win->view); c; c = view_cursors_next(c)) {
1242 Filerange sel = view_cursors_selection_get(c);
1243 if (text_range_valid(&sel)) {
1244 completed = false;
1245 break;
1248 vis_mode_switch(vis, completed ? VIS_MODE_NORMAL : VIS_MODE_VISUAL);
1250 command_free(cmd);
1251 return err;
1254 /* process text input, substitute register content for backreferences etc. */
1255 Buffer text(Vis *vis, const char *text) {
1256 Buffer buf;
1257 buffer_init(&buf);
1258 for (size_t len = strcspn(text, "\\&"); *text; len = strcspn(++text, "\\&")) {
1259 buffer_append(&buf, text, len);
1260 text += len;
1261 enum VisRegister regid = VIS_REG_INVALID;
1262 switch (text[0]) {
1263 case '&':
1264 regid = VIS_REG_AMPERSAND;
1265 break;
1266 case '\\':
1267 if ('1' <= text[1] && text[1] <= '9') {
1268 regid = VIS_REG_1 + text[1] - '1';
1269 text++;
1270 } else if (text[1] == '\\' || text[1] == '&') {
1271 text++;
1273 break;
1274 case '\0':
1275 goto out;
1278 const char *data;
1279 size_t reglen = 0;
1280 if (regid != VIS_REG_INVALID) {
1281 data = register_get(vis, &vis->registers[regid], &reglen);
1282 } else {
1283 data = text;
1284 reglen = 1;
1286 buffer_append(&buf, data, reglen);
1288 out:
1289 return buf;
1292 static bool cmd_insert(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1293 if (!win)
1294 return false;
1295 Buffer buf = text(vis, argv[1]);
1296 size_t len = buffer_length(&buf);
1297 char *data = buffer_move(&buf);
1298 bool ret = sam_insert(win, cur, range->start, data, len);
1299 if (!ret)
1300 free(data);
1301 return ret;
1304 static bool cmd_append(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1305 if (!win)
1306 return false;
1307 Buffer buf = text(vis, argv[1]);
1308 size_t len = buffer_length(&buf);
1309 char *data = buffer_move(&buf);
1310 bool ret = sam_insert(win, cur, range->end, data, len);
1311 if (!ret)
1312 free(data);
1313 return ret;
1316 static bool cmd_change(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1317 if (!win)
1318 return false;
1319 Buffer buf = text(vis, argv[1]);
1320 size_t len = buffer_length(&buf);
1321 char *data = buffer_move(&buf);
1322 bool ret = sam_change(win, cur, range, data, len);
1323 if (!ret)
1324 free(data);
1325 return ret;
1328 static bool cmd_delete(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1329 return win && sam_delete(win, cur, range);
1332 static bool cmd_guard(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1333 if (!win)
1334 return false;
1335 bool match = !cmd->regex || !text_search_range_forward(win->file->text, range->start,
1336 text_range_size(range), cmd->regex, 0, NULL, 0);
1337 if ((count_evaluate(cmd) && match) ^ (argv[0][0] == 'v'))
1338 return sam_execute(vis, win, cmd->cmd, cur, range);
1339 view_cursors_dispose_force(cur);
1340 return true;
1343 static int extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range, bool simulate) {
1344 bool ret = true;
1345 int count = 0;
1346 Text *txt = win->file->text;
1348 if (cmd->regex) {
1349 bool trailing_match = false;
1350 size_t start = range->start, end = range->end, last_start = EPOS;
1351 size_t nsub = 1 + text_regex_nsub(cmd->regex);
1352 if (nsub > 10)
1353 nsub = 10;
1354 RegexMatch match[nsub];
1355 while (start < end || trailing_match) {
1356 trailing_match = false;
1357 bool found = text_search_range_forward(txt, start,
1358 end - start, cmd->regex, nsub, match,
1359 start > range->start ? REG_NOTBOL : 0) == 0;
1360 Filerange r = text_range_empty();
1361 if (found) {
1362 if (argv[0][0] == 'x')
1363 r = text_range_new(match[0].start, match[0].end);
1364 else
1365 r = text_range_new(start, match[0].start);
1366 if (match[0].start == match[0].end) {
1367 if (last_start == match[0].start) {
1368 start++;
1369 continue;
1371 /* in Plan 9's regexp library ^ matches the beginning
1372 * of a line, however in POSIX with REG_NEWLINE ^
1373 * matches the zero-length string immediately after a
1374 * newline. Try filtering out the last such match at EOF.
1376 if (end == match[0].start && start > range->start)
1377 break;
1379 start = match[0].end;
1380 if (start == end)
1381 trailing_match = true;
1382 } else {
1383 if (argv[0][0] == 'y')
1384 r = text_range_new(start, end);
1385 start = end;
1388 if (text_range_valid(&r)) {
1389 if (found) {
1390 for (size_t i = 0; i < nsub; i++) {
1391 Register *reg = &vis->registers[VIS_REG_AMPERSAND+i];
1392 register_put_range(vis, reg, txt, &match[i]);
1395 if (simulate)
1396 count++;
1397 else
1398 ret &= sam_execute(vis, win, cmd->cmd, NULL, &r);
1399 last_start = start;
1402 } else {
1403 size_t start = range->start, end = range->end;
1404 while (start < end) {
1405 size_t next = text_line_next(txt, start);
1406 if (next > end)
1407 next = end;
1408 Filerange r = text_range_new(start, next);
1409 if (start == next || !text_range_valid(&r))
1410 break;
1411 if (simulate)
1412 count++;
1413 else
1414 ret &= sam_execute(vis, win, cmd->cmd, NULL, &r);
1415 start = next;
1419 if (!simulate)
1420 view_cursors_dispose_force(cur);
1421 return simulate ? count : ret;
1424 static bool cmd_extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1425 if (!win)
1426 return false;
1427 int matches = 0;
1428 if (count_negative(cmd->cmd))
1429 matches = extract(vis, win, cmd, argv, cur, range, true);
1430 count_init(cmd->cmd, matches+1);
1431 return extract(vis, win, cmd, argv, cur, range, false);
1434 static bool cmd_select(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1435 Filerange sel = text_range_empty();
1436 if (!win)
1437 return sam_execute(vis, NULL, cmd->cmd, NULL, &sel);
1438 bool ret = true;
1439 View *view = win->view;
1440 Text *txt = win->file->text;
1441 bool multiple_cursors = view_cursors_multiple(view);
1442 Cursor *primary = view_cursors_primary_get(view);
1444 if (vis->mode->visual)
1445 count_init(cmd->cmd, view_cursors_count(view)+1);
1447 for (Cursor *c = view_cursors(view), *next; c && ret; c = next) {
1448 next = view_cursors_next(c);
1449 size_t pos = view_cursors_pos(c);
1450 if (vis->mode->visual) {
1451 sel = view_cursors_selection_get(c);
1452 } else if (cmd->cmd->address) {
1453 /* convert a single line range to a goto line motion */
1454 if (!multiple_cursors && cmd->cmd->cmddef->func == cmd_print) {
1455 Address *addr = cmd->cmd->address;
1456 switch (addr->type) {
1457 case '+':
1458 case '-':
1459 addr = addr->right;
1460 /* fall through */
1461 case 'l':
1462 if (addr && addr->type == 'l' && !addr->right)
1463 addr->type = 'g';
1464 break;
1467 sel = text_range_new(pos, pos);
1468 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_POS) {
1469 sel = text_range_new(pos, pos);
1470 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_LINE) {
1471 sel = text_object_line(txt, pos);
1472 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_AFTER) {
1473 size_t next_line = text_line_next(txt, pos);
1474 sel = text_range_new(next_line, next_line);
1475 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_ALL) {
1476 sel = text_range_new(0, text_size(txt));
1477 } else if (!multiple_cursors && (cmd->cmd->cmddef->flags & CMD_ADDRESS_ALL_1CURSOR)) {
1478 sel = text_range_new(0, text_size(txt));
1479 } else {
1480 sel = text_range_new(pos, text_char_next(txt, pos));
1482 if (!text_range_valid(&sel))
1483 sel = text_range_new(0, 0);
1484 ret &= sam_execute(vis, win, cmd->cmd, c, &sel);
1485 if (cmd->cmd->cmddef->flags & CMD_ONCE)
1486 break;
1489 if (vis->win && vis->win->view == view && primary != view_cursors_primary_get(view))
1490 view_cursors_primary_set(view_cursors(view));
1491 return ret;
1494 static bool cmd_print(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1495 if (!win || !text_range_valid(range))
1496 return false;
1497 View *view = win->view;
1498 Text *txt = win->file->text;
1499 size_t pos = range->end;
1500 if (range->start != range->end)
1501 pos = text_char_prev(txt, pos);
1502 if (cur)
1503 view_cursors_to(cur, pos);
1504 else
1505 cur = view_cursors_new_force(view, pos);
1506 if (cur) {
1507 if (range->start != range->end)
1508 view_cursors_selection_set(cur, range);
1509 else
1510 view_cursors_selection_clear(cur);
1512 return cur != NULL;
1515 static bool cmd_files(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1516 bool ret = true;
1517 for (Win *win = vis->windows; win; win = win->next) {
1518 if (win->file->internal)
1519 continue;
1520 bool match = !cmd->regex || (win->file->name &&
1521 text_regex_match(cmd->regex, win->file->name, 0));
1522 if (match ^ (argv[0][0] == 'Y')) {
1523 Filerange def = text_range_new(0, 0);
1524 ret &= sam_execute(vis, win, cmd->cmd, NULL, &def);
1527 return ret;
1530 static bool cmd_substitute(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1531 vis_info_show(vis, "Use :x/pattern/ c/replacement/ instead");
1532 return false;
1535 static bool cmd_write(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *r) {
1536 if (!win)
1537 return false;
1539 File *file = win->file;
1540 if (sam_transcript_error(&file->transcript, SAM_ERR_WRITE_CONFLICT))
1541 return false;
1543 Text *text = file->text;
1544 const char *filename = argv[1];
1545 if (!filename)
1546 filename = file->name;
1547 if (!filename) {
1548 if (file->fd == -1) {
1549 vis_info_show(vis, "Filename expected");
1550 return false;
1552 if (!strchr(argv[0], 'q')) {
1553 vis_info_show(vis, "No filename given, use 'wq' to write to stdout");
1554 return false;
1557 if (!vis_event_emit(vis, VIS_EVENT_FILE_SAVE_PRE, file, (char*)NULL) && cmd->flags != '!') {
1558 vis_info_show(vis, "Rejected write to stdout by pre-save hook");
1559 return false;
1562 for (Cursor *c = view_cursors(win->view); c; c = view_cursors_next(c)) {
1563 Filerange range = view_cursors_selection_get(c);
1564 bool invalid_range = !text_range_valid(&range);
1565 if (invalid_range)
1566 range = *r;
1567 ssize_t written = text_write_range(text, &range, file->fd);
1568 if (written == -1 || (size_t)written != text_range_size(&range)) {
1569 vis_info_show(vis, "Can not write to stdout");
1570 return false;
1572 if (invalid_range)
1573 break;
1576 /* make sure the file is marked as saved i.e. not modified */
1577 text_save(text, NULL);
1578 vis_event_emit(vis, VIS_EVENT_FILE_SAVE_POST, file, (char*)NULL);
1579 return true;
1582 if (!argv[1] && cmd->flags != '!') {
1583 if (vis->mode->visual) {
1584 vis_info_show(vis, "WARNING: file will be reduced to active selection");
1585 return false;
1587 Filerange all = text_range_new(0, text_size(text));
1588 if (!text_range_equal(r, &all)) {
1589 vis_info_show(vis, "WARNING: file will be reduced to provided range");
1590 return false;
1594 for (const char **name = argv[1] ? &argv[1] : (const char*[]){ filename, NULL }; *name; name++) {
1595 struct stat meta;
1596 if (cmd->flags != '!' && file->stat.st_mtime && stat(*name, &meta) == 0 &&
1597 file->stat.st_mtime < meta.st_mtime) {
1598 vis_info_show(vis, "WARNING: file has been changed since reading it");
1599 return false;
1602 if (!vis_event_emit(vis, VIS_EVENT_FILE_SAVE_PRE, file, *name) && cmd->flags != '!') {
1603 vis_info_show(vis, "Rejected write to `%s' by pre-save hook", *name);
1604 return false;
1607 TextSave *ctx = text_save_begin(text, *name, file->save_method);
1608 if (!ctx) {
1609 const char *msg = errno ? strerror(errno) : "try changing `:set savemethod`";
1610 vis_info_show(vis, "Can't write `%s': %s", *name, msg);
1611 return false;
1614 bool failure = false;
1616 for (Cursor *c = view_cursors(win->view); c; c = view_cursors_next(c)) {
1617 Filerange range = view_cursors_selection_get(c);
1618 bool invalid_range = !text_range_valid(&range);
1619 if (invalid_range)
1620 range = *r;
1622 ssize_t written = text_save_write_range(ctx, &range);
1623 failure = (written == -1 || (size_t)written != text_range_size(&range));
1624 if (failure) {
1625 text_save_cancel(ctx);
1626 break;
1629 if (invalid_range)
1630 break;
1633 if (failure || !text_save_commit(ctx)) {
1634 vis_info_show(vis, "Can't write `%s': %s", *name, strerror(errno));
1635 return false;
1638 if (!file->name)
1639 file_name_set(file, *name);
1640 if (strcmp(file->name, *name) == 0)
1641 file->stat = text_stat(text);
1642 vis_event_emit(vis, VIS_EVENT_FILE_SAVE_POST, file, *name);
1644 return true;
1647 static ssize_t read_buffer(void *context, char *data, size_t len) {
1648 buffer_append(context, data, len);
1649 return len;
1652 static bool cmd_filter(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1653 if (!win)
1654 return false;
1656 Buffer bufout, buferr;
1657 buffer_init(&bufout);
1658 buffer_init(&buferr);
1660 int status = vis_pipe(vis, win->file, range, &argv[1], &bufout, read_buffer, &buferr, read_buffer);
1662 if (vis->interrupted) {
1663 vis_info_show(vis, "Command cancelled");
1664 } else if (status == 0) {
1665 size_t len = buffer_length(&bufout);
1666 char *data = buffer_move(&bufout);
1667 if (!sam_change(win, cur, range, data, len))
1668 free(data);
1669 } else {
1670 vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
1673 buffer_release(&bufout);
1674 buffer_release(&buferr);
1676 return !vis->interrupted && status == 0;
1679 static bool cmd_launch(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1680 Filerange invalid = text_range_new(cur ? view_cursors_pos(cur) : range->start, EPOS);
1681 return cmd_filter(vis, win, cmd, argv, cur, &invalid);
1684 static bool cmd_pipein(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1685 if (!win)
1686 return false;
1687 Filerange filter_range = text_range_new(range->end, range->end);
1688 bool ret = cmd_filter(vis, win, cmd, argv, cur, &filter_range);
1689 if (ret)
1690 ret = sam_delete(win, NULL, range);
1691 return ret;
1694 static bool cmd_pipeout(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1695 if (!win)
1696 return false;
1697 Buffer buferr;
1698 buffer_init(&buferr);
1700 int status = vis_pipe(vis, win->file, range, (const char*[]){ argv[1], NULL }, NULL, NULL, &buferr, read_buffer);
1702 if (status == 0 && cur)
1703 view_cursors_to(cur, range->start);
1705 if (vis->interrupted)
1706 vis_info_show(vis, "Command cancelled");
1707 else if (status != 0)
1708 vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
1710 buffer_release(&buferr);
1712 return !vis->interrupted && status == 0;
1715 static bool cmd_cd(Vis *vis, Win *win, Command *cmd, const char *argv[], Cursor *cur, Filerange *range) {
1716 const char *dir = argv[1];
1717 if (!dir)
1718 dir = getenv("HOME");
1719 return dir && chdir(dir) == 0;
1722 #include "vis-cmds.c"