vis
a vi-like editor based on Plan 9's structural regular expressions
git clone https://9o.is/git/vis.git
sam.c
(53269B)
1 /*
2 * Heavily inspired (and partially based upon) the X11 version of
3 * Rob Pike's sam text editor originally written for Plan 9.
4 *
5 * Copyright © 2016-2020 Marc André Tanner <mat at brain-dump.org>
6 * Copyright © 1998 by Lucent Technologies
7 *
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.
13 *
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.
18 */
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 <fcntl.h>
27 #include "sam.h"
28 #include "vis-core.h"
29 #include "buffer.h"
30 #include "text.h"
31 #include "text-motions.h"
32 #include "text-objects.h"
33 #include "text-regex.h"
34 #include "util.h"
35
36 #define MAX_ARGV 8
37
38 typedef struct Address Address;
39 typedef struct Command Command;
40 typedef struct CommandDef CommandDef;
41
42 struct Change {
43 enum ChangeType {
44 TRANSCRIPT_INSERT = 1 << 0,
45 TRANSCRIPT_DELETE = 1 << 1,
46 TRANSCRIPT_CHANGE = TRANSCRIPT_INSERT|TRANSCRIPT_DELETE,
47 } type;
48 Win *win; /* window in which changed file is being displayed */
49 Selection *sel; /* selection associated with this change, might be NULL */
50 Filerange range; /* inserts are denoted by zero sized range (same start/end) */
51 const char *data; /* will be free(3)-ed after transcript has been processed */
52 size_t len; /* size in bytes of the chunk pointed to by data */
53 Change *next; /* modification position increase monotonically */
54 int count; /* how often should data be inserted? */
55 };
56
57 struct Address {
58 char type; /* # (char) l (line) g (goto line) / ? . $ + - , ; % ' */
59 Regex *regex; /* NULL denotes default for x, y, X, and Y commands */
60 size_t number; /* line or character number */
61 Address *left; /* left hand side of a compound address , ; */
62 Address *right; /* either right hand side of a compound address or next address */
63 };
64
65 typedef struct {
66 int start, end; /* interval [n,m] */
67 bool mod; /* % every n-th match, implies n == m */
68 } Count;
69
70 struct Command {
71 const char *argv[MAX_ARGV];/* [0]=cmd-name, [1..MAX_ARGV-2]=arguments, last element always NULL */
72 Address *address; /* range of text for command */
73 Regex *regex; /* regex to match, used by x, y, g, v, X, Y */
74 const CommandDef *cmddef; /* which command is this? */
75 Count count; /* command count, defaults to [0,+inf] */
76 int iteration; /* current command loop iteration */
77 char flags; /* command specific flags */
78 Command *cmd; /* target of x, y, g, v, X, Y, { */
79 Command *next; /* next command in {} group */
80 };
81
82 struct CommandDef {
83 const char *name; /* command name */
84 VIS_HELP_DECL(const char *help;) /* short, one-line help text */
85 enum {
86 CMD_NONE = 0, /* standalone command without any arguments */
87 CMD_CMD = 1 << 0, /* does the command take a sub/target command? */
88 CMD_REGEX = 1 << 1, /* regex after command? */
89 CMD_REGEX_DEFAULT = 1 << 2, /* is the regex optional i.e. can we use a default? */
90 CMD_COUNT = 1 << 3, /* does the command support a count as in s2/../? */
91 CMD_TEXT = 1 << 4, /* does the command need a text to insert? */
92 CMD_ADDRESS_NONE = 1 << 5, /* is it an error to specify an address for the command? */
93 CMD_ADDRESS_POS = 1 << 6, /* no address implies an empty range at current cursor position */
94 CMD_ADDRESS_LINE = 1 << 7, /* if no address is given, use the current line */
95 CMD_ADDRESS_AFTER = 1 << 8, /* if no address is given, begin at the start of the next line */
96 CMD_ADDRESS_ALL = 1 << 9, /* if no address is given, apply to whole file (independent of #cursors) */
97 CMD_ADDRESS_ALL_1CURSOR = 1 << 10, /* if no address is given and only 1 cursor exists, apply to whole file */
98 CMD_SHELL = 1 << 11, /* command needs a shell command as argument */
99 CMD_FORCE = 1 << 12, /* can the command be forced with ! */
100 CMD_ARGV = 1 << 13, /* whether shell like argument splitting is desired */
101 CMD_ONCE = 1 << 14, /* command should only be executed once, not for every selection */
102 CMD_LOOP = 1 << 15, /* a looping construct like `x`, `y` */
103 CMD_GROUP = 1 << 16, /* a command group { ... } */
104 CMD_DESTRUCTIVE = 1 << 17, /* command potentially destroys window */
105 } flags;
106 const char *defcmd; /* name of a default target command */
107 bool (*func)(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*); /* command implementation */
108 };
109
110 /* sam commands */
111 static bool cmd_insert(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
112 static bool cmd_append(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
113 static bool cmd_change(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
114 static bool cmd_delete(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
115 static bool cmd_guard(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
116 static bool cmd_extract(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
117 static bool cmd_select(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
118 static bool cmd_print(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
119 static bool cmd_files(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
120 static bool cmd_pipein(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
121 static bool cmd_pipeout(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
122 static bool cmd_filter(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
123 static bool cmd_launch(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
124 static bool cmd_substitute(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
125 static bool cmd_write(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
126 static bool cmd_read(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
127 static bool cmd_edit(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
128 static bool cmd_quit(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
129 static bool cmd_cd(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
130 /* vi(m) commands */
131 static bool cmd_set(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
132 static bool cmd_open(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
133 static bool cmd_qall(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
134 static bool cmd_split(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
135 static bool cmd_vsplit(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
136 static bool cmd_new(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
137 static bool cmd_vnew(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
138 static bool cmd_wq(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
139 static bool cmd_earlier_later(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
140 static bool cmd_help(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
141 static bool cmd_map(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
142 static bool cmd_unmap(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
143 static bool cmd_langmap(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
144 static bool cmd_user(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
145
146 static const CommandDef cmds[] = {
147 // name help
148 // flags, default command, implementation
149 {
150 "a", VIS_HELP("Append text after range")
151 CMD_TEXT, NULL, cmd_append
152 }, {
153 "c", VIS_HELP("Change text in range")
154 CMD_TEXT, NULL, cmd_change
155 }, {
156 "d", VIS_HELP("Delete text in range")
157 CMD_NONE, NULL, cmd_delete
158 }, {
159 "g", VIS_HELP("If range contains regexp, run command")
160 CMD_COUNT|CMD_REGEX|CMD_CMD, "p", cmd_guard
161 }, {
162 "i", VIS_HELP("Insert text before range")
163 CMD_TEXT, NULL, cmd_insert
164 }, {
165 "p", VIS_HELP("Create selection covering range")
166 CMD_NONE, NULL, cmd_print
167 }, {
168 "s", VIS_HELP("Substitute: use x/pattern/ c/replacement/ instead")
169 CMD_SHELL|CMD_ADDRESS_LINE, NULL, cmd_substitute
170 }, {
171 "v", VIS_HELP("If range does not contain regexp, run command")
172 CMD_COUNT|CMD_REGEX|CMD_CMD, "p", cmd_guard
173 }, {
174 "x", VIS_HELP("Set range and run command on each match")
175 CMD_CMD|CMD_REGEX|CMD_REGEX_DEFAULT|CMD_ADDRESS_ALL_1CURSOR|CMD_LOOP, "p", cmd_extract
176 }, {
177 "y", VIS_HELP("As `x` but select unmatched text")
178 CMD_CMD|CMD_REGEX|CMD_ADDRESS_ALL_1CURSOR|CMD_LOOP, "p", cmd_extract
179 }, {
180 "X", VIS_HELP("Run command on files whose name matches")
181 CMD_CMD|CMD_REGEX|CMD_REGEX_DEFAULT|CMD_ADDRESS_NONE|CMD_ONCE, NULL, cmd_files
182 }, {
183 "Y", VIS_HELP("As `X` but select unmatched files")
184 CMD_CMD|CMD_REGEX|CMD_ADDRESS_NONE|CMD_ONCE, NULL, cmd_files
185 }, {
186 ">", VIS_HELP("Send range to stdin of command")
187 CMD_SHELL|CMD_ADDRESS_LINE, NULL, cmd_pipeout
188 }, {
189 "<", VIS_HELP("Replace range by stdout of command")
190 CMD_SHELL|CMD_ADDRESS_POS, NULL, cmd_pipein
191 }, {
192 "|", VIS_HELP("Pipe range through command")
193 CMD_SHELL, NULL, cmd_filter
194 }, {
195 "!", VIS_HELP("Run the command")
196 CMD_SHELL|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_launch
197 }, {
198 "w", VIS_HELP("Write range to named file")
199 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_ALL, NULL, cmd_write
200 }, {
201 "r", VIS_HELP("Replace range by contents of file")
202 CMD_ARGV|CMD_ADDRESS_AFTER, NULL, cmd_read
203 }, {
204 "{", VIS_HELP("Start of command group")
205 CMD_GROUP, NULL, NULL
206 }, {
207 "}", VIS_HELP("End of command group" )
208 CMD_NONE, NULL, NULL
209 }, {
210 "e", VIS_HELP("Edit file")
211 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE|CMD_DESTRUCTIVE, NULL, cmd_edit
212 }, {
213 "q", VIS_HELP("Quit the current window")
214 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE|CMD_DESTRUCTIVE, NULL, cmd_quit
215 }, {
216 "cd", VIS_HELP("Change directory")
217 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_cd
218 },
219 /* vi(m) related commands */
220 {
221 "help", VIS_HELP("Show this help")
222 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_help
223 }, {
224 "map", VIS_HELP("Map key binding `:map <mode> <lhs> <rhs>`")
225 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_map
226 }, {
227 "map-window", VIS_HELP("As `map` but window local")
228 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_map
229 }, {
230 "unmap", VIS_HELP("Unmap key binding `:unmap <mode> <lhs>`")
231 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_unmap
232 }, {
233 "unmap-window", VIS_HELP("As `unmap` but window local")
234 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_unmap
235 }, {
236 "langmap", VIS_HELP("Map keyboard layout `:langmap <locale-keys> <latin-keys>`")
237 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_langmap
238 }, {
239 "new", VIS_HELP("Create new window")
240 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_new
241 }, {
242 "open", VIS_HELP("Open file")
243 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_open
244 }, {
245 "qall", VIS_HELP("Exit vis")
246 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE|CMD_DESTRUCTIVE, NULL, cmd_qall
247 }, {
248 "set", VIS_HELP("Set option")
249 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_set
250 }, {
251 "split", VIS_HELP("Horizontally split window")
252 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_split
253 }, {
254 "vnew", VIS_HELP("As `:new` but split vertically")
255 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_vnew
256 }, {
257 "vsplit", VIS_HELP("Vertically split window")
258 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_vsplit
259 }, {
260 "wq", VIS_HELP("Write file and quit")
261 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_ALL|CMD_DESTRUCTIVE, NULL, cmd_wq
262 }, {
263 "earlier", VIS_HELP("Go to older text state")
264 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_earlier_later
265 }, {
266 "later", VIS_HELP("Go to newer text state")
267 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_earlier_later
268 },
269 { NULL, VIS_HELP(NULL) CMD_NONE, NULL, NULL },
270 };
271
272 static const CommandDef cmddef_select = {
273 NULL, VIS_HELP(NULL) CMD_NONE, NULL, cmd_select
274 };
275
276 /* :set command options */
277 typedef struct {
278 const char *names[3]; /* name and optional alias */
279 enum VisOption flags; /* option type, etc. */
280 VIS_HELP_DECL(const char *help;) /* short, one line help text */
281 VisOptionFunction *func; /* option handler, NULL for builtins */
282 void *context; /* context passed to option handler function */
283 } OptionDef;
284
285 enum {
286 OPTION_SHELL,
287 OPTION_ESCDELAY,
288 OPTION_AUTOINDENT,
289 OPTION_EXPANDTAB,
290 OPTION_TABWIDTH,
291 OPTION_SHOW_SPACES,
292 OPTION_SHOW_TABS,
293 OPTION_SHOW_NEWLINES,
294 OPTION_SHOW_EOF,
295 OPTION_STATUSBAR,
296 OPTION_NUMBER,
297 OPTION_NUMBER_RELATIVE,
298 OPTION_CURSOR_LINE,
299 OPTION_COLOR_COLUMN,
300 OPTION_SAVE_METHOD,
301 OPTION_LOAD_METHOD,
302 OPTION_CHANGE_256COLORS,
303 OPTION_LAYOUT,
304 OPTION_IGNORECASE,
305 OPTION_BREAKAT,
306 OPTION_WRAP_COLUMN,
307 };
308
309 static const OptionDef options[] = {
310 [OPTION_SHELL] = {
311 { "shell" },
312 VIS_OPTION_TYPE_STRING,
313 VIS_HELP("Shell to use for external commands (default: $SHELL, /etc/passwd, /bin/sh)")
314 },
315 [OPTION_ESCDELAY] = {
316 { "escdelay" },
317 VIS_OPTION_TYPE_NUMBER,
318 VIS_HELP("Milliseconds to wait to distinguish <Escape> from terminal escape sequences")
319 },
320 [OPTION_AUTOINDENT] = {
321 { "autoindent", "ai" },
322 VIS_OPTION_TYPE_BOOL,
323 VIS_HELP("Copy leading white space from previous line")
324 },
325 [OPTION_EXPANDTAB] = {
326 { "expandtab", "et" },
327 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
328 VIS_HELP("Replace entered <Tab> with `tabwidth` spaces")
329 },
330 [OPTION_TABWIDTH] = {
331 { "tabwidth", "tw" },
332 VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
333 VIS_HELP("Number of spaces to display (and insert if `expandtab` is enabled) for a tab")
334 },
335 [OPTION_SHOW_SPACES] = {
336 { "showspaces" },
337 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
338 VIS_HELP("Display replacement symbol instead of a space")
339 },
340 [OPTION_SHOW_TABS] = {
341 { "showtabs" },
342 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
343 VIS_HELP("Display replacement symbol for tabs")
344 },
345 [OPTION_SHOW_NEWLINES] = {
346 { "shownewlines" },
347 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
348 VIS_HELP("Display replacement symbol for newlines")
349 },
350 [OPTION_SHOW_EOF] = {
351 { "showeof" },
352 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
353 VIS_HELP("Display replacement symbol for lines after the end of the file")
354 },
355 [OPTION_STATUSBAR] = {
356 { "statusbar", "sb" },
357 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
358 VIS_HELP("Display status bar")
359 },
360 [OPTION_NUMBER] = {
361 { "numbers", "nu" },
362 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
363 VIS_HELP("Display absolute line numbers")
364 },
365 [OPTION_NUMBER_RELATIVE] = {
366 { "relativenumbers", "rnu" },
367 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
368 VIS_HELP("Display relative line numbers")
369 },
370 [OPTION_CURSOR_LINE] = {
371 { "cursorline", "cul" },
372 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
373 VIS_HELP("Highlight current cursor line")
374 },
375 [OPTION_COLOR_COLUMN] = {
376 { "colorcolumn", "cc" },
377 VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
378 VIS_HELP("Highlight a fixed column")
379 },
380 [OPTION_SAVE_METHOD] = {
381 { "savemethod" },
382 VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW,
383 VIS_HELP("Save method to use for current file 'auto', 'atomic' or 'inplace'")
384 },
385 [OPTION_LOAD_METHOD] = {
386 { "loadmethod" },
387 VIS_OPTION_TYPE_STRING,
388 VIS_HELP("How to load existing files 'auto', 'read' or 'mmap'")
389 },
390 [OPTION_CHANGE_256COLORS] = {
391 { "change256colors" },
392 VIS_OPTION_TYPE_BOOL,
393 VIS_HELP("Change 256 color palette to support 24bit colors")
394 },
395 [OPTION_LAYOUT] = {
396 { "layout" },
397 VIS_OPTION_TYPE_STRING,
398 VIS_HELP("Vertical or horizontal window layout")
399 },
400 [OPTION_IGNORECASE] = {
401 { "ignorecase", "ic" },
402 VIS_OPTION_TYPE_BOOL,
403 VIS_HELP("Ignore case when searching")
404 },
405 [OPTION_BREAKAT] = {
406 { "breakat", "brk" },
407 VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW,
408 VIS_HELP("Characters which might cause a word wrap")
409 },
410 [OPTION_WRAP_COLUMN] = {
411 { "wrapcolumn", "wc" },
412 VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
413 VIS_HELP("Wrap lines at minimum of window width and wrapcolumn")
414 },
415 };
416
417 bool sam_init(Vis *vis) {
418 if (!(vis->cmds = map_new()))
419 return false;
420 bool ret = true;
421 for (const CommandDef *cmd = cmds; cmd && cmd->name; cmd++)
422 ret &= map_put(vis->cmds, cmd->name, cmd);
423
424 if (!(vis->options = map_new()))
425 return false;
426 for (int i = 0; i < LENGTH(options); i++) {
427 for (const char *const *name = options[i].names; *name; name++)
428 ret &= map_put(vis->options, *name, &options[i]);
429 }
430
431 return ret;
432 }
433
434 const char *sam_error(enum SamError err) {
435 static const char *error_msg[] = {
436 [SAM_ERR_OK] = "Success",
437 [SAM_ERR_MEMORY] = "Out of memory",
438 [SAM_ERR_ADDRESS] = "Bad address",
439 [SAM_ERR_NO_ADDRESS] = "Command takes no address",
440 [SAM_ERR_UNMATCHED_BRACE] = "Unmatched `}'",
441 [SAM_ERR_REGEX] = "Bad regular expression",
442 [SAM_ERR_TEXT] = "Bad text",
443 [SAM_ERR_SHELL] = "Shell command expected",
444 [SAM_ERR_COMMAND] = "Unknown command",
445 [SAM_ERR_EXECUTE] = "Error executing command",
446 [SAM_ERR_NEWLINE] = "Newline expected",
447 [SAM_ERR_MARK] = "Invalid mark",
448 [SAM_ERR_CONFLICT] = "Conflicting changes",
449 [SAM_ERR_WRITE_CONFLICT] = "Can not write while changing",
450 [SAM_ERR_LOOP_INVALID_CMD] = "Destructive command in looping construct",
451 [SAM_ERR_GROUP_INVALID_CMD] = "Destructive command in group",
452 [SAM_ERR_COUNT] = "Invalid count",
453 };
454
455 size_t idx = err;
456 return idx < LENGTH(error_msg) ? error_msg[idx] : NULL;
457 }
458
459 static void change_free(Change *c) {
460 if (!c)
461 return;
462 free((char*)c->data);
463 free(c);
464 }
465
466 static Change *change_new(Transcript *t, enum ChangeType type, Filerange *range, Win *win, Selection *sel) {
467 if (!text_range_valid(range))
468 return NULL;
469 Change **prev, *next;
470 if (t->latest && t->latest->range.end <= range->start) {
471 prev = &t->latest->next;
472 next = t->latest->next;
473 } else {
474 prev = &t->changes;
475 next = t->changes;
476 }
477 while (next && next->range.end <= range->start) {
478 prev = &next->next;
479 next = next->next;
480 }
481 if (next && next->range.start < range->end) {
482 t->error = SAM_ERR_CONFLICT;
483 return NULL;
484 }
485 Change *new = calloc(1, sizeof *new);
486 if (new) {
487 new->type = type;
488 new->range = *range;
489 new->sel = sel;
490 new->win = win;
491 new->next = next;
492 *prev = new;
493 t->latest = new;
494 }
495 return new;
496 }
497
498 static void sam_transcript_init(Transcript *t) {
499 memset(t, 0, sizeof *t);
500 }
501
502 static bool sam_transcript_error(Transcript *t, enum SamError error) {
503 if (t->changes)
504 t->error = error;
505 return t->error;
506 }
507
508 static void sam_transcript_free(Transcript *t) {
509 for (Change *c = t->changes, *next; c; c = next) {
510 next = c->next;
511 change_free(c);
512 }
513 }
514
515 static bool sam_insert(Win *win, Selection *sel, size_t pos, const char *data, size_t len, int count) {
516 Filerange range = text_range_new(pos, pos);
517 Change *c = change_new(&win->file->transcript, TRANSCRIPT_INSERT, &range, win, sel);
518 if (c) {
519 c->data = data;
520 c->len = len;
521 c->count = count;
522 }
523 return c;
524 }
525
526 static bool sam_delete(Win *win, Selection *sel, Filerange *range) {
527 return change_new(&win->file->transcript, TRANSCRIPT_DELETE, range, win, sel);
528 }
529
530 static bool sam_change(Win *win, Selection *sel, Filerange *range, const char *data, size_t len, int count) {
531 Change *c = change_new(&win->file->transcript, TRANSCRIPT_CHANGE, range, win, sel);
532 if (c) {
533 c->data = data;
534 c->len = len;
535 c->count = count;
536 }
537 return c;
538 }
539
540 static Address *address_new(void) {
541 Address *addr = calloc(1, sizeof *addr);
542 if (addr)
543 addr->number = EPOS;
544 return addr;
545 }
546
547 static void address_free(Address *addr) {
548 if (!addr)
549 return;
550 text_regex_free(addr->regex);
551 address_free(addr->left);
552 address_free(addr->right);
553 free(addr);
554 }
555
556 static void skip_spaces(const char **s) {
557 while (**s == ' ' || **s == '\t')
558 (*s)++;
559 }
560
561 static char *parse_until(const char **s, const char *until, const char *escchars, int type){
562 Buffer buf = {0};
563 size_t len = strlen(until);
564 bool escaped = false;
565
566 for (; **s && (!memchr(until, **s, len) || escaped); (*s)++) {
567 if (type != CMD_SHELL && !escaped && **s == '\\') {
568 escaped = true;
569 continue;
570 }
571
572 char c = **s;
573
574 if (escaped) {
575 escaped = false;
576 if (c == '\n')
577 continue;
578 if (c == 'n') {
579 c = '\n';
580 } else if (c == 't') {
581 c = '\t';
582 } else if (type != CMD_REGEX && type != CMD_TEXT && c == '\\') {
583 // ignore one of the back slashes
584 } else {
585 bool delim = memchr(until, c, len);
586 bool esc = escchars && memchr(escchars, c, strlen(escchars));
587 if (!delim && !esc)
588 buffer_append(&buf, "\\", 1);
589 }
590 }
591
592 if (!buffer_append(&buf, &c, 1)) {
593 buffer_release(&buf);
594 return NULL;
595 }
596 }
597
598 buffer_terminate(&buf);
599
600 return buf.data;
601 }
602
603 static char *parse_delimited(const char **s, int type) {
604 char delim[2] = { **s, '\0' };
605 if (!delim[0] || isspace((unsigned char)delim[0]))
606 return NULL;
607 (*s)++;
608 char *chunk = parse_until(s, delim, NULL, type);
609 if (**s == delim[0])
610 (*s)++;
611 return chunk;
612 }
613
614 static int parse_number(const char **s) {
615 char *end = NULL;
616 int number = strtoull(*s, &end, 10);
617 if (end == *s)
618 return 0;
619 *s = end;
620 return number;
621 }
622
623 static char *parse_text(const char **s, Count *count) {
624 skip_spaces(s);
625 const char *before = *s;
626 count->start = parse_number(s);
627 if (*s == before)
628 count->start = 1;
629 if (**s != '\n') {
630 before = *s;
631 char *text = parse_delimited(s, CMD_TEXT);
632 return (!text && *s != before) ? strdup("") : text;
633 }
634
635 Buffer buf = {0};
636 const char *start = *s + 1;
637 bool dot = false;
638
639 for ((*s)++; **s && (!dot || **s != '\n'); (*s)++)
640 dot = (**s == '.');
641
642 if (!dot || !buffer_put(&buf, start, *s - start - 1) ||
643 !buffer_append(&buf, "\0", 1)) {
644 buffer_release(&buf);
645 return NULL;
646 }
647
648 return buf.data;
649 }
650
651 static char *parse_shellcmd(Vis *vis, const char **s) {
652 skip_spaces(s);
653 char *cmd = parse_until(s, "\n", NULL, false);
654 if (!cmd) {
655 const char *last_cmd = register_get(vis, &vis->registers[VIS_REG_SHELL], NULL);
656 return last_cmd ? strdup(last_cmd) : NULL;
657 }
658 register_put0(vis, &vis->registers[VIS_REG_SHELL], cmd);
659 return cmd;
660 }
661
662 static void parse_argv(const char **s, const char *argv[], size_t maxarg) {
663 for (size_t i = 0; i < maxarg; i++) {
664 skip_spaces(s);
665 if (**s == '"' || **s == '\'')
666 argv[i] = parse_delimited(s, CMD_ARGV);
667 else
668 argv[i] = parse_until(s, " \t\n", "\'\"", CMD_ARGV);
669 }
670 }
671
672 static bool valid_cmdname(const char *s) {
673 unsigned char c = (unsigned char)*s;
674 return c && !isspace(c) && !isdigit(c) && (!ispunct(c) || c == '_' || (c == '-' && valid_cmdname(s+1)));
675 }
676
677 static char *parse_cmdname(const char **s) {
678 Buffer buf = {0};
679
680 skip_spaces(s);
681 while (valid_cmdname(*s))
682 buffer_append(&buf, (*s)++, 1);
683
684 buffer_terminate(&buf);
685
686 return buf.data;
687 }
688
689 static Regex *parse_regex(Vis *vis, const char **s) {
690 const char *before = *s;
691 char *pattern = parse_delimited(s, CMD_REGEX);
692 if (!pattern && *s == before)
693 return NULL;
694 Regex *regex = vis_regex(vis, pattern);
695 free(pattern);
696 return regex;
697 }
698
699 static enum SamError parse_count(const char **s, Count *count) {
700 count->mod = **s == '%';
701
702 if (count->mod) {
703 (*s)++;
704 int n = parse_number(s);
705 if (!n)
706 return SAM_ERR_COUNT;
707 count->start = n;
708 count->end = n;
709 return SAM_ERR_OK;
710 }
711
712 const char *before = *s;
713 if (!(count->start = parse_number(s)) && *s != before)
714 return SAM_ERR_COUNT;
715 if (**s != ',') {
716 count->end = count->start ? count->start : INT_MAX;
717 return SAM_ERR_OK;
718 } else {
719 (*s)++;
720 }
721 before = *s;
722 if (!(count->end = parse_number(s)) && *s != before)
723 return SAM_ERR_COUNT;
724 if (!count->end)
725 count->end = INT_MAX;
726 return SAM_ERR_OK;
727 }
728
729 static Address *address_parse_simple(Vis *vis, const char **s, enum SamError *err) {
730
731 skip_spaces(s);
732
733 Address addr = {
734 .type = **s,
735 .regex = NULL,
736 .number = EPOS,
737 .left = NULL,
738 .right = NULL,
739 };
740
741 switch (addr.type) {
742 case '#': /* character #n */
743 (*s)++;
744 addr.number = parse_number(s);
745 break;
746 case '0': case '1': case '2': case '3': case '4': /* line n */
747 case '5': case '6': case '7': case '8': case '9':
748 addr.type = 'l';
749 addr.number = parse_number(s);
750 break;
751 case '\'':
752 (*s)++;
753 if ((addr.number = vis_mark_from(vis, **s)) == VIS_MARK_INVALID) {
754 *err = SAM_ERR_MARK;
755 return NULL;
756 }
757 (*s)++;
758 break;
759 case '/': /* regexp forwards */
760 case '?': /* regexp backwards */
761 addr.regex = parse_regex(vis, s);
762 if (!addr.regex) {
763 *err = SAM_ERR_REGEX;
764 return NULL;
765 }
766 break;
767 case '$': /* end of file */
768 case '.':
769 case '+':
770 case '-':
771 case '%':
772 (*s)++;
773 break;
774 default:
775 return NULL;
776 }
777
778 if ((addr.right = address_parse_simple(vis, s, err))) {
779 switch (addr.right->type) {
780 case '.':
781 case '$':
782 return NULL;
783 case '#':
784 case 'l':
785 case '/':
786 case '?':
787 if (addr.type != '+' && addr.type != '-') {
788 Address *plus = address_new();
789 if (!plus) {
790 address_free(addr.right);
791 return NULL;
792 }
793 plus->type = '+';
794 plus->right = addr.right;
795 addr.right = plus;
796 }
797 break;
798 }
799 }
800
801 Address *ret = address_new();
802 if (!ret) {
803 address_free(addr.right);
804 return NULL;
805 }
806 *ret = addr;
807 return ret;
808 }
809
810 static Address *address_parse_compound(Vis *vis, const char **s, enum SamError *err) {
811 Address addr = { 0 }, *left = address_parse_simple(vis, s, err), *right = NULL;
812 skip_spaces(s);
813 addr.type = **s;
814 switch (addr.type) {
815 case ',': /* a1,a2 */
816 case ';': /* a1;a2 */
817 (*s)++;
818 right = address_parse_compound(vis, s, err);
819 if (right && (right->type == ',' || right->type == ';') && !right->left) {
820 *err = SAM_ERR_ADDRESS;
821 goto fail;
822 }
823 break;
824 default:
825 return left;
826 }
827
828 addr.left = left;
829 addr.right = right;
830
831 Address *ret = address_new();
832 if (ret) {
833 *ret = addr;
834 return ret;
835 }
836
837 fail:
838 address_free(left);
839 address_free(right);
840 return NULL;
841 }
842
843 static Command *command_new(const char *name) {
844 Command *cmd = calloc(1, sizeof(Command));
845 if (!cmd)
846 return NULL;
847 if (name && !(cmd->argv[0] = strdup(name))) {
848 free(cmd);
849 return NULL;
850 }
851 return cmd;
852 }
853
854 static void command_free(Command *cmd) {
855 if (!cmd)
856 return;
857
858 for (Command *c = cmd->cmd, *next; c; c = next) {
859 next = c->next;
860 command_free(c);
861 }
862
863 for (const char **args = cmd->argv; *args; args++)
864 free((void*)*args);
865 address_free(cmd->address);
866 text_regex_free(cmd->regex);
867 free(cmd);
868 }
869
870 static const CommandDef *command_lookup(Vis *vis, const char *name) {
871 return map_closest(vis->cmds, name);
872 }
873
874 static Command *command_parse(Vis *vis, const char **s, enum SamError *err) {
875 if (!**s) {
876 *err = SAM_ERR_COMMAND;
877 return NULL;
878 }
879 Command *cmd = command_new(NULL);
880 if (!cmd)
881 return NULL;
882
883 cmd->address = address_parse_compound(vis, s, err);
884 skip_spaces(s);
885
886 cmd->argv[0] = parse_cmdname(s);
887
888 if (!cmd->argv[0]) {
889 char name[2] = { **s ? **s : 'p', '\0' };
890 if (**s)
891 (*s)++;
892 if (!(cmd->argv[0] = strdup(name)))
893 goto fail;
894 }
895
896 const CommandDef *cmddef = command_lookup(vis, cmd->argv[0]);
897 if (!cmddef) {
898 *err = SAM_ERR_COMMAND;
899 goto fail;
900 }
901
902 cmd->cmddef = cmddef;
903
904 if (strcmp(cmd->argv[0], "{") == 0) {
905 Command *prev = NULL, *next;
906 int level = vis->nesting_level++;
907 do {
908 while (**s == ' ' || **s == '\t' || **s == '\n')
909 (*s)++;
910 next = command_parse(vis, s, err);
911 if (*err)
912 goto fail;
913 if (prev)
914 prev->next = next;
915 else
916 cmd->cmd = next;
917 } while ((prev = next));
918 if (level != vis->nesting_level) {
919 *err = SAM_ERR_UNMATCHED_BRACE;
920 goto fail;
921 }
922 } else if (strcmp(cmd->argv[0], "}") == 0) {
923 if (vis->nesting_level-- == 0) {
924 *err = SAM_ERR_UNMATCHED_BRACE;
925 goto fail;
926 }
927 command_free(cmd);
928 return NULL;
929 }
930
931 if (cmddef->flags & CMD_ADDRESS_NONE && cmd->address) {
932 *err = SAM_ERR_NO_ADDRESS;
933 goto fail;
934 }
935
936 if (cmddef->flags & CMD_FORCE && **s == '!') {
937 cmd->flags = '!';
938 (*s)++;
939 }
940
941 if ((cmddef->flags & CMD_COUNT) && (*err = parse_count(s, &cmd->count)))
942 goto fail;
943
944 if (cmddef->flags & CMD_REGEX) {
945 if ((cmddef->flags & CMD_REGEX_DEFAULT) && (!**s || **s == ' ')) {
946 skip_spaces(s);
947 } else {
948 const char *before = *s;
949 cmd->regex = parse_regex(vis, s);
950 if (!cmd->regex && (*s != before || !(cmddef->flags & CMD_COUNT))) {
951 *err = SAM_ERR_REGEX;
952 goto fail;
953 }
954 }
955 }
956
957 if (cmddef->flags & CMD_SHELL && !(cmd->argv[1] = parse_shellcmd(vis, s))) {
958 *err = SAM_ERR_SHELL;
959 goto fail;
960 }
961
962 if (cmddef->flags & CMD_TEXT && !(cmd->argv[1] = parse_text(s, &cmd->count))) {
963 *err = SAM_ERR_TEXT;
964 goto fail;
965 }
966
967 if (cmddef->flags & CMD_ARGV) {
968 parse_argv(s, &cmd->argv[1], MAX_ARGV-2);
969 cmd->argv[MAX_ARGV-1] = NULL;
970 }
971
972 if (cmddef->flags & CMD_CMD) {
973 skip_spaces(s);
974 if (cmddef->defcmd && (**s == '\n' || **s == '}' || **s == '\0')) {
975 if (**s == '\n')
976 (*s)++;
977 if (!(cmd->cmd = command_new(cmddef->defcmd)))
978 goto fail;
979 cmd->cmd->cmddef = command_lookup(vis, cmddef->defcmd);
980 } else {
981 if (!(cmd->cmd = command_parse(vis, s, err)))
982 goto fail;
983 if (strcmp(cmd->argv[0], "X") == 0 || strcmp(cmd->argv[0], "Y") == 0) {
984 Command *sel = command_new("select");
985 if (!sel)
986 goto fail;
987 sel->cmd = cmd->cmd;
988 sel->cmddef = &cmddef_select;
989 cmd->cmd = sel;
990 }
991 }
992 }
993
994 return cmd;
995 fail:
996 command_free(cmd);
997 return NULL;
998 }
999
1000 static Command *sam_parse(Vis *vis, const char *cmd, enum SamError *err) {
1001 vis->nesting_level = 0;
1002 const char **s = &cmd;
1003 Command *c = command_parse(vis, s, err);
1004 if (!c)
1005 return NULL;
1006 while (**s == ' ' || **s == '\t' || **s == '\n')
1007 (*s)++;
1008 if (**s) {
1009 *err = SAM_ERR_NEWLINE;
1010 command_free(c);
1011 return NULL;
1012 }
1013
1014 Command *sel = command_new("select");
1015 if (!sel) {
1016 command_free(c);
1017 return NULL;
1018 }
1019 sel->cmd = c;
1020 sel->cmddef = &cmddef_select;
1021 return sel;
1022 }
1023
1024 static Filerange address_line_evaluate(Address *addr, File *file, Filerange *range, int sign) {
1025 Text *txt = file->text;
1026 size_t offset = addr->number != EPOS ? addr->number : 1;
1027 size_t start = range->start, end = range->end, line;
1028 if (sign > 0) {
1029 char c;
1030 if (start < end && text_byte_get(txt, end-1, &c) && c == '\n')
1031 end--;
1032 line = text_lineno_by_pos(txt, end);
1033 line = text_pos_by_lineno(txt, line + offset);
1034 } else if (sign < 0) {
1035 line = text_lineno_by_pos(txt, start);
1036 line = offset < line ? text_pos_by_lineno(txt, line - offset) : 0;
1037 } else {
1038 if (addr->number == 0)
1039 return text_range_new(0, 0);
1040 line = text_pos_by_lineno(txt, addr->number);
1041 }
1042
1043 if (addr->type == 'g')
1044 return text_range_new(line, line);
1045 else
1046 return text_range_new(line, text_line_next(txt, line));
1047 }
1048
1049 static Filerange address_evaluate(Address *addr, File *file, Selection *sel, Filerange *range, int sign) {
1050 Filerange ret = text_range_empty();
1051
1052 do {
1053 switch (addr->type) {
1054 case '#':
1055 if (sign > 0)
1056 ret.start = ret.end = range->end + addr->number;
1057 else if (sign < 0)
1058 ret.start = ret.end = range->start - addr->number;
1059 else
1060 ret = text_range_new(addr->number, addr->number);
1061 break;
1062 case 'l':
1063 case 'g':
1064 ret = address_line_evaluate(addr, file, range, sign);
1065 break;
1066 case '\'':
1067 {
1068 size_t pos = EPOS;
1069 Array *marks = &file->marks[addr->number];
1070 size_t idx = sel ? view_selections_number(sel) : 0;
1071 SelectionRegion *sr = array_get(marks, idx);
1072 if (sr)
1073 pos = text_mark_get(file->text, sr->cursor);
1074 ret = text_range_new(pos, pos);
1075 break;
1076 }
1077 case '?':
1078 sign = sign == 0 ? -1 : -sign;
1079 /* fall through */
1080 case '/':
1081 if (sign >= 0)
1082 ret = text_object_search_forward(file->text, range->end, addr->regex);
1083 else
1084 ret = text_object_search_backward(file->text, range->start, addr->regex);
1085 break;
1086 case '$':
1087 {
1088 size_t size = text_size(file->text);
1089 ret = text_range_new(size, size);
1090 break;
1091 }
1092 case '.':
1093 ret = *range;
1094 break;
1095 case '+':
1096 case '-':
1097 sign = addr->type == '+' ? +1 : -1;
1098 if (!addr->right || addr->right->type == '+' || addr->right->type == '-')
1099 ret = address_line_evaluate(addr, file, range, sign);
1100 break;
1101 case ',':
1102 case ';':
1103 {
1104 Filerange left, right;
1105 if (addr->left)
1106 left = address_evaluate(addr->left, file, sel, range, 0);
1107 else
1108 left = text_range_new(0, 0);
1109
1110 if (addr->type == ';')
1111 range = &left;
1112
1113 if (addr->right) {
1114 right = address_evaluate(addr->right, file, sel, range, 0);
1115 } else {
1116 size_t size = text_size(file->text);
1117 right = text_range_new(size, size);
1118 }
1119 /* TODO: enforce strict ordering? */
1120 return text_range_union(&left, &right);
1121 }
1122 case '%':
1123 return text_range_new(0, text_size(file->text));
1124 }
1125 if (text_range_valid(&ret))
1126 range = &ret;
1127 } while ((addr = addr->right));
1128
1129 return ret;
1130 }
1131
1132 static bool count_evaluate(Command *cmd) {
1133 Count *count = &cmd->count;
1134 if (count->mod)
1135 return count->start ? cmd->iteration % count->start == 0 : true;
1136 return count->start <= cmd->iteration && cmd->iteration <= count->end;
1137 }
1138
1139 static bool sam_execute(Vis *vis, Win *win, Command *cmd, Selection *sel, Filerange *range) {
1140 bool ret = true;
1141 if (cmd->address && win)
1142 *range = address_evaluate(cmd->address, win->file, sel, range, 0);
1143
1144 cmd->iteration++;
1145 switch (cmd->argv[0][0]) {
1146 case '{':
1147 {
1148 for (Command *c = cmd->cmd; c && ret; c = c->next)
1149 ret &= sam_execute(vis, win, c, NULL, range);
1150 view_selections_dispose_force(sel);
1151 break;
1152 }
1153 default:
1154 ret = cmd->cmddef->func(vis, win, cmd, cmd->argv, sel, range);
1155 break;
1156 }
1157 return ret;
1158 }
1159
1160 static enum SamError validate(Command *cmd, bool loop, bool group) {
1161 if (cmd->cmddef->flags & CMD_DESTRUCTIVE) {
1162 if (loop)
1163 return SAM_ERR_LOOP_INVALID_CMD;
1164 if (group)
1165 return SAM_ERR_GROUP_INVALID_CMD;
1166 }
1167
1168 group |= (cmd->cmddef->flags & CMD_GROUP);
1169 loop |= (cmd->cmddef->flags & CMD_LOOP);
1170 for (Command *c = cmd->cmd; c; c = c->next) {
1171 enum SamError err = validate(c, loop, group);
1172 if (err != SAM_ERR_OK)
1173 return err;
1174 }
1175 return SAM_ERR_OK;
1176 }
1177
1178 static enum SamError command_validate(Command *cmd) {
1179 return validate(cmd, false, false);
1180 }
1181
1182 static bool count_negative(Command *cmd) {
1183 if (cmd->count.start < 0 || cmd->count.end < 0)
1184 return true;
1185 for (Command *c = cmd->cmd; c; c = c->next) {
1186 if (c->cmddef->func != cmd_extract && c->cmddef->func != cmd_select) {
1187 if (count_negative(c))
1188 return true;
1189 }
1190 }
1191 return false;
1192 }
1193
1194 static void count_init(Command *cmd, int max) {
1195 Count *count = &cmd->count;
1196 cmd->iteration = 0;
1197 if (count->start < 0)
1198 count->start += max;
1199 if (count->end < 0)
1200 count->end += max;
1201 for (Command *c = cmd->cmd; c; c = c->next) {
1202 if (c->cmddef->func != cmd_extract && c->cmddef->func != cmd_select)
1203 count_init(c, max);
1204 }
1205 }
1206
1207 enum SamError sam_cmd(Vis *vis, const char *s) {
1208 enum SamError err = SAM_ERR_OK;
1209 if (!s)
1210 return err;
1211
1212 Command *cmd = sam_parse(vis, s, &err);
1213 if (!cmd) {
1214 if (err == SAM_ERR_OK)
1215 err = SAM_ERR_MEMORY;
1216 return err;
1217 }
1218
1219 err = command_validate(cmd);
1220 if (err != SAM_ERR_OK) {
1221 command_free(cmd);
1222 return err;
1223 }
1224
1225 for (File *file = vis->files; file; file = file->next) {
1226 if (file->internal)
1227 continue;
1228 sam_transcript_init(&file->transcript);
1229 }
1230
1231 bool visual = vis->mode->visual;
1232 size_t primary_pos = vis->win ? view_cursor_get(&vis->win->view) : EPOS;
1233 Filerange range = text_range_empty();
1234 sam_execute(vis, vis->win, cmd, NULL, &range);
1235
1236 for (File *file = vis->files; file; file = file->next) {
1237 if (file->internal)
1238 continue;
1239 Transcript *t = &file->transcript;
1240 if (t->error != SAM_ERR_OK) {
1241 err = t->error;
1242 sam_transcript_free(t);
1243 continue;
1244 }
1245 vis_file_snapshot(vis, file);
1246 ptrdiff_t delta = 0;
1247 for (Change *c = t->changes; c; c = c->next) {
1248 c->range.start += delta;
1249 c->range.end += delta;
1250 if (c->type & TRANSCRIPT_DELETE) {
1251 text_delete_range(file->text, &c->range);
1252 delta -= text_range_size(&c->range);
1253 if (c->sel && c->type == TRANSCRIPT_DELETE) {
1254 if (visual)
1255 view_selections_dispose_force(c->sel);
1256 else
1257 view_cursors_to(c->sel, c->range.start);
1258 }
1259 }
1260 if (c->type & TRANSCRIPT_INSERT) {
1261 for (int i = 0; i < c->count; i++) {
1262 text_insert(file->text, c->range.start, c->data, c->len);
1263 delta += c->len;
1264 }
1265 Filerange r = text_range_new(c->range.start,
1266 c->range.start + c->len * c->count);
1267 if (c->sel) {
1268 if (visual) {
1269 view_selections_set(c->sel, &r);
1270 c->sel->anchored = true;
1271 } else {
1272 if (memchr(c->data, '\n', c->len))
1273 view_cursors_to(c->sel, r.start);
1274 else
1275 view_cursors_to(c->sel, r.end);
1276 }
1277 } else if (visual) {
1278 Selection *sel = view_selections_new(&c->win->view, r.start);
1279 if (sel) {
1280 view_selections_set(sel, &r);
1281 sel->anchored = true;
1282 }
1283 }
1284 }
1285 }
1286 sam_transcript_free(&file->transcript);
1287 vis_file_snapshot(vis, file);
1288 }
1289
1290 for (Win *win = vis->windows; win; win = win->next)
1291 view_selections_normalize(&win->view);
1292
1293 if (vis->win) {
1294 if (primary_pos != EPOS && view_selection_disposed(&vis->win->view))
1295 view_cursors_to(vis->win->view.selection, primary_pos);
1296 view_selections_primary_set(view_selections(&vis->win->view));
1297 vis_jumplist_save(vis);
1298 bool completed = true;
1299 for (Selection *s = view_selections(&vis->win->view); s; s = view_selections_next(s)) {
1300 if (s->anchored) {
1301 completed = false;
1302 break;
1303 }
1304 }
1305 vis_mode_switch(vis, completed ? VIS_MODE_NORMAL : VIS_MODE_VISUAL);
1306 }
1307 command_free(cmd);
1308 return err;
1309 }
1310
1311 /* process text input, substitute register content for backreferences etc. */
1312 Buffer text(Vis *vis, const char *text) {
1313 Buffer buf = {0};
1314 for (size_t len = strcspn(text, "\\&"); *text; len = strcspn(++text, "\\&")) {
1315 buffer_append(&buf, text, len);
1316 text += len;
1317 enum VisRegister regid = VIS_REG_INVALID;
1318 switch (text[0]) {
1319 case '&':
1320 regid = VIS_REG_AMPERSAND;
1321 break;
1322 case '\\':
1323 if ('1' <= text[1] && text[1] <= '9') {
1324 regid = VIS_REG_1 + text[1] - '1';
1325 text++;
1326 } else if (text[1] == '\\' || text[1] == '&') {
1327 text++;
1328 }
1329 break;
1330 case '\0':
1331 goto out;
1332 }
1333
1334 const char *data;
1335 size_t reglen = 0;
1336 if (regid != VIS_REG_INVALID) {
1337 data = register_get(vis, &vis->registers[regid], ®len);
1338 } else {
1339 data = text;
1340 reglen = 1;
1341 }
1342 buffer_append(&buf, data, reglen);
1343 }
1344 out:
1345 return buf;
1346 }
1347
1348 static bool cmd_insert(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1349 if (!win)
1350 return false;
1351 Buffer buf = text(vis, argv[1]);
1352 bool ret = sam_insert(win, sel, range->start, buf.data, buf.len, cmd->count.start);
1353 if (!ret)
1354 free(buf.data);
1355 return ret;
1356 }
1357
1358 static bool cmd_append(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1359 if (!win)
1360 return false;
1361 Buffer buf = text(vis, argv[1]);
1362 bool ret = sam_insert(win, sel, range->end, buf.data, buf.len, cmd->count.start);
1363 if (!ret)
1364 free(buf.data);
1365 return ret;
1366 }
1367
1368 static bool cmd_change(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1369 if (!win)
1370 return false;
1371 Buffer buf = text(vis, argv[1]);
1372 bool ret = sam_change(win, sel, range, buf.data, buf.len, cmd->count.start);
1373 if (!ret)
1374 free(buf.data);
1375 return ret;
1376 }
1377
1378 static bool cmd_delete(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1379 return win && sam_delete(win, sel, range);
1380 }
1381
1382 static bool cmd_guard(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1383 if (!win)
1384 return false;
1385 bool match = false;
1386 RegexMatch captures[1];
1387 size_t len = text_range_size(range);
1388 if (!cmd->regex)
1389 match = true;
1390 else if (!text_search_range_forward(win->file->text, range->start, len, cmd->regex, 1, captures, 0))
1391 match = captures[0].start < range->end;
1392 if ((count_evaluate(cmd) && match) ^ (argv[0][0] == 'v'))
1393 return sam_execute(vis, win, cmd->cmd, sel, range);
1394 view_selections_dispose_force(sel);
1395 return true;
1396 }
1397
1398 static int extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range, bool simulate) {
1399 bool ret = true;
1400 int count = 0;
1401 Text *txt = win->file->text;
1402
1403 if (cmd->regex) {
1404 size_t start = range->start, end = range->end;
1405 size_t last_start = argv[0][0] == 'x' ? EPOS : start;
1406 size_t nsub = 1 + text_regex_nsub(cmd->regex);
1407 if (nsub > MAX_REGEX_SUB)
1408 nsub = MAX_REGEX_SUB;
1409 RegexMatch match[MAX_REGEX_SUB];
1410 while (start <= end) {
1411 char c;
1412 int flags = start > range->start &&
1413 text_byte_get(txt, start - 1, &c) && c != '\n' ?
1414 REG_NOTBOL : 0;
1415 bool found = !text_search_range_forward(txt, start, end - start,
1416 cmd->regex, nsub, match,
1417 flags);
1418 Filerange r = text_range_empty();
1419 if (found) {
1420 if (argv[0][0] == 'x')
1421 r = text_range_new(match[0].start, match[0].end);
1422 else
1423 r = text_range_new(last_start, match[0].start);
1424 if (match[0].start == match[0].end) {
1425 if (last_start == match[0].start) {
1426 start++;
1427 continue;
1428 }
1429 /* in Plan 9's regexp library ^ matches the beginning
1430 * of a line, however in POSIX with REG_NEWLINE ^
1431 * matches the zero-length string immediately after a
1432 * newline. Try filtering out the last such match at EOF.
1433 */
1434 if (end == match[0].start && start > range->start &&
1435 text_byte_get(txt, end-1, &c) && c == '\n')
1436 break;
1437 start = match[0].end + 1;
1438 } else {
1439 start = match[0].end;
1440 }
1441 } else {
1442 if (argv[0][0] == 'y')
1443 r = text_range_new(start, end);
1444 start = end + 1;
1445 }
1446
1447 if (text_range_valid(&r)) {
1448 if (found) {
1449 for (size_t i = 0; i < nsub; i++) {
1450 Register *reg = &vis->registers[VIS_REG_AMPERSAND+i];
1451 register_put_range(vis, reg, txt, &match[i]);
1452 }
1453 last_start = match[0].end;
1454 } else {
1455 last_start = start;
1456 }
1457 if (simulate)
1458 count++;
1459 else
1460 ret &= sam_execute(vis, win, cmd->cmd, NULL, &r);
1461 }
1462 }
1463 } else {
1464 size_t start = range->start, end = range->end;
1465 while (start < end) {
1466 size_t next = text_line_next(txt, start);
1467 if (next > end)
1468 next = end;
1469 Filerange r = text_range_new(start, next);
1470 if (start == next || !text_range_valid(&r))
1471 break;
1472 if (simulate)
1473 count++;
1474 else
1475 ret &= sam_execute(vis, win, cmd->cmd, NULL, &r);
1476 start = next;
1477 }
1478 }
1479
1480 if (!simulate)
1481 view_selections_dispose_force(sel);
1482 return simulate ? count : ret;
1483 }
1484
1485 static bool cmd_extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1486 if (!win || !text_range_valid(range))
1487 return false;
1488 int matches = 0;
1489 if (count_negative(cmd->cmd))
1490 matches = extract(vis, win, cmd, argv, sel, range, true);
1491 count_init(cmd->cmd, matches+1);
1492 return extract(vis, win, cmd, argv, sel, range, false);
1493 }
1494
1495 static bool cmd_select(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1496 Filerange r = text_range_empty();
1497 if (!win)
1498 return sam_execute(vis, NULL, cmd->cmd, NULL, &r);
1499 bool ret = true;
1500 View *view = &win->view;
1501 Text *txt = win->file->text;
1502 bool multiple_cursors = view->selection_count > 1;
1503 Selection *primary = view_selections_primary_get(view);
1504
1505 if (vis->mode->visual)
1506 count_init(cmd->cmd, view->selection_count + 1);
1507
1508 for (Selection *s = view_selections(view), *next; s && ret; s = next) {
1509 next = view_selections_next(s);
1510 size_t pos = view_cursors_pos(s);
1511 if (vis->mode->visual) {
1512 r = view_selections_get(s);
1513 } else if (cmd->cmd->address) {
1514 /* convert a single line range to a goto line motion */
1515 if (!multiple_cursors && cmd->cmd->cmddef->func == cmd_print) {
1516 Address *addr = cmd->cmd->address;
1517 switch (addr->type) {
1518 case '+':
1519 case '-':
1520 addr = addr->right;
1521 /* fall through */
1522 case 'l':
1523 if (addr && addr->type == 'l' && !addr->right)
1524 addr->type = 'g';
1525 break;
1526 }
1527 }
1528 r = text_range_new(pos, pos);
1529 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_POS) {
1530 r = text_range_new(pos, pos);
1531 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_LINE) {
1532 r = text_object_line(txt, pos);
1533 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_AFTER) {
1534 size_t next_line = text_line_next(txt, pos);
1535 r = text_range_new(next_line, next_line);
1536 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_ALL) {
1537 r = text_range_new(0, text_size(txt));
1538 } else if (!multiple_cursors && (cmd->cmd->cmddef->flags & CMD_ADDRESS_ALL_1CURSOR)) {
1539 r = text_range_new(0, text_size(txt));
1540 } else {
1541 r = text_range_new(pos, text_char_next(txt, pos));
1542 }
1543 if (!text_range_valid(&r))
1544 r = text_range_new(0, 0);
1545 ret &= sam_execute(vis, win, cmd->cmd, s, &r);
1546 if (cmd->cmd->cmddef->flags & CMD_ONCE)
1547 break;
1548 }
1549
1550 if (vis->win && &vis->win->view == view && primary != view_selections_primary_get(view))
1551 view_selections_primary_set(view_selections(view));
1552 return ret;
1553 }
1554
1555 static bool cmd_print(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1556 if (!win || !text_range_valid(range))
1557 return false;
1558 if (!sel)
1559 sel = view_selections_new_force(&win->view, range->start);
1560 if (!sel)
1561 return false;
1562 if (range->start != range->end) {
1563 view_selections_set(sel, range);
1564 sel->anchored = true;
1565 } else {
1566 view_cursors_to(sel, range->start);
1567 view_selection_clear(sel);
1568 }
1569 return true;
1570 }
1571
1572 static bool cmd_files(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1573 bool ret = true;
1574 for (Win *wn, *w = vis->windows; w; w = wn) {
1575 /* w can get freed by sam_execute() so store w->next early */
1576 wn = w->next;
1577 if (w->file->internal)
1578 continue;
1579 bool match = !cmd->regex ||
1580 (w->file->name && text_regex_match(cmd->regex, w->file->name, 0) == 0);
1581 if (match ^ (argv[0][0] == 'Y')) {
1582 Filerange def = text_range_new(0, 0);
1583 ret &= sam_execute(vis, w, cmd->cmd, NULL, &def);
1584 }
1585 }
1586 return ret;
1587 }
1588
1589 static bool cmd_substitute(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1590 vis_info_show(vis, "Use :x/pattern/ c/replacement/ instead");
1591 return false;
1592 }
1593
1594 /* cmd_write stores win->file's contents end emits pre/post events.
1595 * If the range r covers the whole file, it is updated to account for
1596 * potential file's text mutation by a FILE_SAVE_PRE callback.
1597 */
1598 static bool cmd_write(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *r) {
1599 if (!win)
1600 return false;
1601
1602 File *file = win->file;
1603 if (sam_transcript_error(&file->transcript, SAM_ERR_WRITE_CONFLICT))
1604 return false;
1605
1606 Text *text = file->text;
1607 Filerange range_all = text_range_new(0, text_size(text));
1608 bool write_entire_file = text_range_equal(r, &range_all);
1609
1610 const char *filename = argv[1];
1611 if (!filename)
1612 filename = file->name;
1613 if (!filename) {
1614 if (file->fd == -1) {
1615 vis_info_show(vis, "Filename expected");
1616 return false;
1617 }
1618 if (!strchr(argv[0], 'q')) {
1619 vis_info_show(vis, "No filename given, use 'wq' to write to stdout");
1620 return false;
1621 }
1622
1623 if (!vis_event_emit(vis, VIS_EVENT_FILE_SAVE_PRE, file, (char*)NULL) && cmd->flags != '!') {
1624 vis_info_show(vis, "Rejected write to stdout by pre-save hook");
1625 return false;
1626 }
1627 /* a pre-save hook may have changed the text; need to re-take the range */
1628 if (write_entire_file)
1629 *r = text_range_new(0, text_size(text));
1630
1631 bool visual = vis->mode->visual;
1632
1633 for (Selection *s = view_selections(&win->view); s; s = view_selections_next(s)) {
1634 Filerange range = visual ? view_selections_get(s) : *r;
1635 ssize_t written = text_write_range(text, &range, file->fd);
1636 if (written == -1 || (size_t)written != text_range_size(&range)) {
1637 vis_info_show(vis, "Can not write to stdout");
1638 return false;
1639 }
1640 if (!visual)
1641 break;
1642 }
1643
1644 /* make sure the file is marked as saved i.e. not modified */
1645 text_save(text, NULL);
1646 vis_event_emit(vis, VIS_EVENT_FILE_SAVE_POST, file, (char*)NULL);
1647 return true;
1648 }
1649
1650 if (!argv[1] && cmd->flags != '!') {
1651 if (vis->mode->visual) {
1652 vis_info_show(vis, "WARNING: file will be reduced to active selection");
1653 return false;
1654 }
1655 if (!write_entire_file) {
1656 vis_info_show(vis, "WARNING: file will be reduced to provided range");
1657 return false;
1658 }
1659 }
1660
1661 for (const char **name = argv[1] ? &argv[1] : (const char*[]){ filename, NULL }; *name; name++) {
1662
1663 char *path = absolute_path(*name);
1664 if (!path)
1665 return false;
1666
1667 struct stat meta;
1668 bool existing_file = !stat(path, &meta);
1669 bool same_file = existing_file && file->name &&
1670 file->stat.st_dev == meta.st_dev && file->stat.st_ino == meta.st_ino;
1671
1672 if (cmd->flags != '!') {
1673 if (same_file && file->stat.st_mtime && file->stat.st_mtime < meta.st_mtime) {
1674 vis_info_show(vis, "WARNING: file has been changed since reading it");
1675 goto err;
1676 }
1677 if (existing_file && !same_file) {
1678 vis_info_show(vis, "WARNING: file exists");
1679 goto err;
1680 }
1681 }
1682
1683 if (!vis_event_emit(vis, VIS_EVENT_FILE_SAVE_PRE, file, path) && cmd->flags != '!') {
1684 vis_info_show(vis, "Rejected write to `%s' by pre-save hook", path);
1685 goto err;
1686 }
1687 /* a pre-save hook may have changed the text; need to re-take the range */
1688 if (write_entire_file)
1689 *r = text_range_new(0, text_size(text));
1690
1691 TextSave *ctx = text_save_begin(text, AT_FDCWD, path, file->save_method);
1692 if (!ctx) {
1693 const char *msg = errno ? strerror(errno) : "try changing `:set savemethod`";
1694 vis_info_show(vis, "Can't write `%s': %s", path, msg);
1695 goto err;
1696 }
1697
1698 bool failure = false;
1699 bool visual = vis->mode->visual;
1700
1701 for (Selection *s = view_selections(&win->view); s; s = view_selections_next(s)) {
1702 Filerange range = visual ? view_selections_get(s) : *r;
1703 ssize_t written = text_save_write_range(ctx, &range);
1704 failure = (written == -1 || (size_t)written != text_range_size(&range));
1705 if (failure) {
1706 text_save_cancel(ctx);
1707 break;
1708 }
1709
1710 if (!visual)
1711 break;
1712 }
1713
1714 if (failure || !text_save_commit(ctx)) {
1715 vis_info_show(vis, "Can't write `%s': %s", path, strerror(errno));
1716 goto err;
1717 }
1718
1719 if (!file->name) {
1720 file_name_set(file, path);
1721 same_file = true;
1722 }
1723 if (same_file || (!existing_file && strcmp(file->name, path) == 0))
1724 file->stat = text_stat(text);
1725 vis_event_emit(vis, VIS_EVENT_FILE_SAVE_POST, file, path);
1726 free(path);
1727 continue;
1728
1729 err:
1730 free(path);
1731 return false;
1732 }
1733 return true;
1734 }
1735
1736 static bool cmd_filter(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1737 if (!win)
1738 return false;
1739
1740 Buffer bufout = {0}, buferr = {0};
1741
1742 int status = vis_pipe(vis, win->file, range, &argv[1], &bufout, read_into_buffer, &buferr,
1743 read_into_buffer, false);
1744
1745 if (vis->interrupted) {
1746 vis_info_show(vis, "Command cancelled");
1747 } else if (status == 0) {
1748 char *data = bufout.data;
1749 bufout.data = 0;
1750 if (!sam_change(win, sel, range, data, bufout.len, 1))
1751 free(data);
1752 } else {
1753 vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
1754 }
1755
1756 buffer_release(&bufout);
1757 buffer_release(&buferr);
1758
1759 return !vis->interrupted && status == 0;
1760 }
1761
1762 static bool cmd_launch(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1763 Filerange invalid = text_range_new(sel ? view_cursors_pos(sel) : range->start, EPOS);
1764 return cmd_filter(vis, win, cmd, argv, sel, &invalid);
1765 }
1766
1767 static bool cmd_pipein(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1768 if (!win)
1769 return false;
1770 Filerange filter_range = text_range_new(range->end, range->end);
1771 bool ret = cmd_filter(vis, win, cmd, argv, sel, &filter_range);
1772 if (ret)
1773 ret = sam_delete(win, NULL, range);
1774 return ret;
1775 }
1776
1777 static bool cmd_pipeout(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1778 if (!win)
1779 return false;
1780 Buffer buferr = {0};
1781
1782 int status = vis_pipe(vis, win->file, range, (const char*[]){ argv[1], NULL }, NULL, NULL,
1783 &buferr, read_into_buffer, false);
1784
1785 if (vis->interrupted)
1786 vis_info_show(vis, "Command cancelled");
1787 else if (status != 0)
1788 vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
1789
1790 buffer_release(&buferr);
1791
1792 return !vis->interrupted && status == 0;
1793 }
1794
1795 static bool cmd_cd(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1796 const char *dir = argv[1];
1797 if (!dir)
1798 dir = getenv("HOME");
1799 return dir && chdir(dir) == 0;
1800 }
1801
1802 #include "vis-cmds.c"