make C-s and C-q available in key mappings
[vis/gkirilov.git] / sam.c
blobefdb120e33c1b0d2a8f7b3a6e4a309e46ea960ea
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-2020 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 <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"
36 #define MAX_ARGV 8
38 typedef struct Address Address;
39 typedef struct Command Command;
40 typedef struct CommandDef CommandDef;
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? */
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 */
65 typedef struct {
66 int start, end; /* interval [n,m] */
67 bool mod; /* % every n-th match, implies n == m */
68 } Count;
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 */
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 */
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*);
146 static const CommandDef cmds[] = {
147 // name help
148 // flags, default command, implementation
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
219 /* vi(m) related commands */
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
269 { NULL, VIS_HELP(NULL) CMD_NONE, NULL, NULL },
272 static const CommandDef cmddef_select = {
273 NULL, VIS_HELP(NULL) CMD_NONE, NULL, cmd_select
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;
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_BREAKAT,
305 OPTION_WRAP_COLUMN,
306 OPTION_SMARTCASE,
307 OPTION_LITERAL,
310 static const OptionDef options[] = {
311 [OPTION_SHELL] = {
312 { "shell" },
313 VIS_OPTION_TYPE_STRING,
314 VIS_HELP("Shell to use for external commands (default: $SHELL, /etc/passwd, /bin/sh)")
316 [OPTION_ESCDELAY] = {
317 { "escdelay" },
318 VIS_OPTION_TYPE_NUMBER,
319 VIS_HELP("Milliseconds to wait to distinguish <Escape> from terminal escape sequences")
321 [OPTION_AUTOINDENT] = {
322 { "autoindent", "ai" },
323 VIS_OPTION_TYPE_BOOL,
324 VIS_HELP("Copy leading white space from previous line")
326 [OPTION_EXPANDTAB] = {
327 { "expandtab", "et" },
328 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
329 VIS_HELP("Replace entered <Tab> with `tabwidth` spaces")
331 [OPTION_TABWIDTH] = {
332 { "tabwidth", "tw" },
333 VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
334 VIS_HELP("Number of spaces to display (and insert if `expandtab` is enabled) for a tab")
336 [OPTION_SHOW_SPACES] = {
337 { "showspaces", "show-spaces" },
338 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW|VIS_OPTION_DEPRECATED,
339 VIS_HELP("Display replacement symbol instead of a space")
340 NULL,
341 "show-spaces"
343 [OPTION_SHOW_TABS] = {
344 { "showtabs", "show-tabs" },
345 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW|VIS_OPTION_DEPRECATED,
346 VIS_HELP("Display replacement symbol for tabs")
347 NULL,
348 "show-tabs"
350 [OPTION_SHOW_NEWLINES] = {
351 { "shownewlines", "show-newlines" },
352 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW|VIS_OPTION_DEPRECATED,
353 VIS_HELP("Display replacement symbol for newlines")
354 NULL,
355 "show-newlines"
357 [OPTION_SHOW_EOF] = {
358 { "showeof", "show-eof" },
359 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW|VIS_OPTION_DEPRECATED,
360 VIS_HELP("Display replacement symbol for lines after the end of the file")
361 NULL,
362 "show-eof"
364 [OPTION_STATUSBAR] = {
365 { "statusbar", "sb" },
366 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
367 VIS_HELP("Display status bar")
369 [OPTION_NUMBER] = {
370 { "numbers", "nu" },
371 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
372 VIS_HELP("Display absolute line numbers")
374 [OPTION_NUMBER_RELATIVE] = {
375 { "relativenumbers", "rnu" },
376 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
377 VIS_HELP("Display relative line numbers")
379 [OPTION_CURSOR_LINE] = {
380 { "cursorline", "cul" },
381 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
382 VIS_HELP("Highlight current cursor line")
384 [OPTION_COLOR_COLUMN] = {
385 { "colorcolumn", "cc" },
386 VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
387 VIS_HELP("Highlight a fixed column")
389 [OPTION_SAVE_METHOD] = {
390 { "savemethod" },
391 VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW,
392 VIS_HELP("Save method to use for current file 'auto', 'atomic' or 'inplace'")
394 [OPTION_LOAD_METHOD] = {
395 { "loadmethod" },
396 VIS_OPTION_TYPE_STRING,
397 VIS_HELP("How to load existing files 'auto', 'read' or 'mmap'")
399 [OPTION_CHANGE_256COLORS] = {
400 { "change256colors", "change-256colors" },
401 VIS_OPTION_TYPE_BOOL|VIS_OPTION_DEPRECATED,
402 VIS_HELP("Change 256 color palette to support 24bit colors")
403 NULL,
404 "change-256colors"
406 [OPTION_LAYOUT] = {
407 { "layout" },
408 VIS_OPTION_TYPE_STRING,
409 VIS_HELP("Vertical or horizontal window layout")
411 [OPTION_SMARTCASE] = {
412 { "smartcase", "scs" },
413 VIS_OPTION_TYPE_BOOL,
414 VIS_HELP("Case-insensitive search, unless the pattern contains upper case characters")
416 [OPTION_LITERAL] = {
417 { "literal" },
418 VIS_OPTION_TYPE_BOOL,
419 VIS_HELP("Literal string search")
421 [OPTION_BREAKAT] = {
422 { "breakat", "brk" },
423 VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW,
424 VIS_HELP("Characters which might cause a word wrap")
426 [OPTION_WRAP_COLUMN] = {
427 { "wrapcolumn", "wc" },
428 VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
429 VIS_HELP("Wrap lines at minimum of window width and wrapcolumn")
433 bool sam_init(Vis *vis) {
434 if (!(vis->cmds = map_new()))
435 return false;
436 bool ret = true;
437 for (const CommandDef *cmd = cmds; cmd && cmd->name; cmd++)
438 ret &= map_put(vis->cmds, cmd->name, cmd);
440 if (!(vis->options = map_new()))
441 return false;
442 for (int i = 0; i < LENGTH(options); i++) {
443 for (const char *const *name = options[i].names; *name; name++)
444 ret &= map_put(vis->options, *name, &options[i]);
447 return ret;
450 const char *sam_error(enum SamError err) {
451 static const char *error_msg[] = {
452 [SAM_ERR_OK] = "Success",
453 [SAM_ERR_MEMORY] = "Out of memory",
454 [SAM_ERR_ADDRESS] = "Bad address",
455 [SAM_ERR_NO_ADDRESS] = "Command takes no address",
456 [SAM_ERR_UNMATCHED_BRACE] = "Unmatched `}'",
457 [SAM_ERR_REGEX] = "Bad regular expression",
458 [SAM_ERR_TEXT] = "Bad text",
459 [SAM_ERR_SHELL] = "Shell command expected",
460 [SAM_ERR_COMMAND] = "Unknown command",
461 [SAM_ERR_EXECUTE] = "Error executing command",
462 [SAM_ERR_NEWLINE] = "Newline expected",
463 [SAM_ERR_MARK] = "Invalid mark",
464 [SAM_ERR_CONFLICT] = "Conflicting changes",
465 [SAM_ERR_WRITE_CONFLICT] = "Can not write while changing",
466 [SAM_ERR_LOOP_INVALID_CMD] = "Destructive command in looping construct",
467 [SAM_ERR_GROUP_INVALID_CMD] = "Destructive command in group",
468 [SAM_ERR_COUNT] = "Invalid count",
471 size_t idx = err;
472 return idx < LENGTH(error_msg) ? error_msg[idx] : NULL;
475 static void change_free(Change *c) {
476 if (!c)
477 return;
478 free((char*)c->data);
479 free(c);
482 static Change *change_new(Transcript *t, enum ChangeType type, Filerange *range, Win *win, Selection *sel) {
483 if (!text_range_valid(range))
484 return NULL;
485 Change **prev, *next;
486 if (t->latest && t->latest->range.end <= range->start) {
487 prev = &t->latest->next;
488 next = t->latest->next;
489 } else {
490 prev = &t->changes;
491 next = t->changes;
493 while (next && next->range.end <= range->start) {
494 prev = &next->next;
495 next = next->next;
497 if (next && next->range.start < range->end) {
498 t->error = SAM_ERR_CONFLICT;
499 return NULL;
501 Change *new = calloc(1, sizeof *new);
502 if (new) {
503 new->type = type;
504 new->range = *range;
505 new->sel = sel;
506 new->win = win;
507 new->next = next;
508 *prev = new;
509 t->latest = new;
511 return new;
514 static void sam_transcript_init(Transcript *t) {
515 memset(t, 0, sizeof *t);
518 static bool sam_transcript_error(Transcript *t, enum SamError error) {
519 if (t->changes)
520 t->error = error;
521 return t->error;
524 static void sam_transcript_free(Transcript *t) {
525 for (Change *c = t->changes, *next; c; c = next) {
526 next = c->next;
527 change_free(c);
531 static bool sam_insert(Win *win, Selection *sel, size_t pos, const char *data, size_t len, int count) {
532 Filerange range = text_range_new(pos, pos);
533 Change *c = change_new(&win->file->transcript, TRANSCRIPT_INSERT, &range, win, sel);
534 if (c) {
535 c->data = data;
536 c->len = len;
537 c->count = count;
539 return c;
542 static bool sam_delete(Win *win, Selection *sel, Filerange *range) {
543 return change_new(&win->file->transcript, TRANSCRIPT_DELETE, range, win, sel);
546 static bool sam_change(Win *win, Selection *sel, Filerange *range, const char *data, size_t len, int count) {
547 Change *c = change_new(&win->file->transcript, TRANSCRIPT_CHANGE, range, win, sel);
548 if (c) {
549 c->data = data;
550 c->len = len;
551 c->count = count;
553 return c;
556 static Address *address_new(void) {
557 Address *addr = calloc(1, sizeof *addr);
558 if (addr)
559 addr->number = EPOS;
560 return addr;
563 static void address_free(Address *addr) {
564 if (!addr)
565 return;
566 address_free(addr->left);
567 address_free(addr->right);
568 free(addr);
571 static void skip_spaces(const char **s) {
572 while (**s == ' ' || **s == '\t')
573 (*s)++;
576 static char *parse_until(const char **s, const char *until, const char *escchars, int type){
577 Buffer buf;
578 buffer_init(&buf);
579 size_t len = strlen(until);
580 bool escaped = false;
582 for (; **s && (!memchr(until, **s, len) || escaped); (*s)++) {
583 if (type != CMD_SHELL && !escaped && **s == '\\') {
584 escaped = true;
585 continue;
588 char c = **s;
590 if (escaped) {
591 escaped = false;
592 if (c == '\n')
593 continue;
594 if (c == 'n') {
595 c = '\n';
596 } else if (c == 't') {
597 c = '\t';
598 } else if (type != CMD_REGEX && type != CMD_TEXT && c == '\\') {
599 // ignore one of the back slashes
600 } else {
601 bool delim = memchr(until, c, len);
602 bool esc = escchars && memchr(escchars, c, strlen(escchars));
603 if (!delim && !esc)
604 buffer_append(&buf, "\\", 1);
608 if (!buffer_append(&buf, &c, 1)) {
609 buffer_release(&buf);
610 return NULL;
614 buffer_terminate(&buf);
616 return buffer_move(&buf);
619 static char *parse_delimited(const char **s, int type) {
620 char delim[2] = { **s, '\0' };
621 if (!delim[0] || isspace((unsigned char)delim[0]))
622 return NULL;
623 (*s)++;
624 char *chunk = parse_until(s, delim, NULL, type);
625 if (**s == delim[0])
626 (*s)++;
627 return chunk;
630 static int parse_number(const char **s) {
631 char *end = NULL;
632 int number = strtoull(*s, &end, 10);
633 if (end == *s)
634 return 0;
635 *s = end;
636 return number;
639 static char *parse_text(const char **s, Count *count) {
640 skip_spaces(s);
641 const char *before = *s;
642 count->start = parse_number(s);
643 if (*s == before)
644 count->start = 1;
645 if (**s != '\n') {
646 before = *s;
647 char *text = parse_delimited(s, CMD_TEXT);
648 return (!text && *s != before) ? strdup("") : text;
651 Buffer buf;
652 buffer_init(&buf);
653 const char *start = *s + 1;
654 bool dot = false;
656 for ((*s)++; **s && (!dot || **s != '\n'); (*s)++)
657 dot = (**s == '.');
659 if (!dot || !buffer_put(&buf, start, *s - start - 1) ||
660 !buffer_append(&buf, "\0", 1)) {
661 buffer_release(&buf);
662 return NULL;
665 return buffer_move(&buf);
668 static char *parse_shellcmd(Vis *vis, const char **s) {
669 skip_spaces(s);
670 char *cmd = parse_until(s, "\n", NULL, false);
671 if (!cmd) {
672 const char *last_cmd = register_get(vis, &vis->registers[VIS_REG_SHELL], NULL);
673 return last_cmd ? strdup(last_cmd) : NULL;
675 register_put0(vis, &vis->registers[VIS_REG_SHELL], cmd);
676 return cmd;
679 static void parse_argv(const char **s, const char *argv[], size_t maxarg) {
680 for (size_t i = 0; i < maxarg; i++) {
681 skip_spaces(s);
682 if (**s == '"' || **s == '\'')
683 argv[i] = parse_delimited(s, CMD_ARGV);
684 else
685 argv[i] = parse_until(s, " \t\n", "\'\"", CMD_ARGV);
689 static bool valid_cmdname(const char *s) {
690 unsigned char c = (unsigned char)*s;
691 return c && !isspace(c) && !isdigit(c) && (!ispunct(c) || c == '_' || (c == '-' && valid_cmdname(s+1)));
694 static char *parse_cmdname(const char **s) {
695 Buffer buf;
696 buffer_init(&buf);
698 skip_spaces(s);
699 while (valid_cmdname(*s))
700 buffer_append(&buf, (*s)++, 1);
702 buffer_terminate(&buf);
704 return buffer_move(&buf);
707 static Regex *parse_regex(Vis *vis, const char **s) {
708 const char *before = *s;
709 char *pattern = parse_delimited(s, CMD_REGEX);
710 if (!pattern && *s == before)
711 return NULL;
712 Regex *regex = vis_regex(vis, pattern, 0);
713 free(pattern);
714 return regex;
717 static enum SamError parse_count(const char **s, Count *count) {
718 count->mod = **s == '%';
720 if (count->mod) {
721 (*s)++;
722 int n = parse_number(s);
723 if (!n)
724 return SAM_ERR_COUNT;
725 count->start = n;
726 count->end = n;
727 return SAM_ERR_OK;
730 const char *before = *s;
731 if (!(count->start = parse_number(s)) && *s != before)
732 return SAM_ERR_COUNT;
733 if (**s != ',') {
734 count->end = count->start ? count->start : INT_MAX;
735 return SAM_ERR_OK;
736 } else {
737 (*s)++;
739 before = *s;
740 if (!(count->end = parse_number(s)) && *s != before)
741 return SAM_ERR_COUNT;
742 if (!count->end)
743 count->end = INT_MAX;
744 return SAM_ERR_OK;
747 static Address *address_parse_simple(Vis *vis, const char **s, enum SamError *err) {
749 skip_spaces(s);
751 Address addr = {
752 .type = **s,
753 .regex = NULL,
754 .number = EPOS,
755 .left = NULL,
756 .right = NULL,
759 switch (addr.type) {
760 case '#': /* character #n */
761 (*s)++;
762 addr.number = parse_number(s);
763 break;
764 case '0': case '1': case '2': case '3': case '4': /* line n */
765 case '5': case '6': case '7': case '8': case '9':
766 addr.type = 'l';
767 addr.number = parse_number(s);
768 break;
769 case '\'':
770 (*s)++;
771 if ((addr.number = vis_mark_from(vis, **s)) == VIS_MARK_INVALID) {
772 *err = SAM_ERR_MARK;
773 return NULL;
775 (*s)++;
776 break;
777 case '/': /* regexp forwards */
778 case '?': /* regexp backwards */
779 addr.regex = parse_regex(vis, s);
780 if (!addr.regex) {
781 *err = SAM_ERR_REGEX;
782 return NULL;
784 break;
785 case '$': /* end of file */
786 case '.':
787 case '+':
788 case '-':
789 case '%':
790 (*s)++;
791 break;
792 default:
793 return NULL;
796 if ((addr.right = address_parse_simple(vis, s, err))) {
797 switch (addr.right->type) {
798 case '.':
799 case '$':
800 return NULL;
801 case '#':
802 case 'l':
803 case '/':
804 case '?':
805 if (addr.type != '+' && addr.type != '-') {
806 Address *plus = address_new();
807 if (!plus) {
808 address_free(addr.right);
809 return NULL;
811 plus->type = '+';
812 plus->right = addr.right;
813 addr.right = plus;
815 break;
819 Address *ret = address_new();
820 if (!ret) {
821 address_free(addr.right);
822 return NULL;
824 *ret = addr;
825 return ret;
828 static Address *address_parse_compound(Vis *vis, const char **s, enum SamError *err) {
829 Address addr = { 0 }, *left = address_parse_simple(vis, s, err), *right = NULL;
830 skip_spaces(s);
831 addr.type = **s;
832 switch (addr.type) {
833 case ',': /* a1,a2 */
834 case ';': /* a1;a2 */
835 (*s)++;
836 right = address_parse_compound(vis, s, err);
837 if (right && (right->type == ',' || right->type == ';') && !right->left) {
838 *err = SAM_ERR_ADDRESS;
839 goto fail;
841 break;
842 default:
843 return left;
846 addr.left = left;
847 addr.right = right;
849 Address *ret = address_new();
850 if (ret) {
851 *ret = addr;
852 return ret;
855 fail:
856 address_free(left);
857 address_free(right);
858 return NULL;
861 static Command *command_new(const char *name) {
862 Command *cmd = calloc(1, sizeof(Command));
863 if (!cmd)
864 return NULL;
865 if (name && !(cmd->argv[0] = strdup(name))) {
866 free(cmd);
867 return NULL;
869 return cmd;
872 static void command_free(Command *cmd) {
873 if (!cmd)
874 return;
876 for (Command *c = cmd->cmd, *next; c; c = next) {
877 next = c->next;
878 command_free(c);
881 for (const char **args = cmd->argv; *args; args++)
882 free((void*)*args);
883 address_free(cmd->address);
884 free(cmd);
887 static const CommandDef *command_lookup(Vis *vis, const char *name) {
888 return map_closest(vis->cmds, name);
891 static Command *command_parse(Vis *vis, const char **s, enum SamError *err) {
892 if (!**s) {
893 *err = SAM_ERR_COMMAND;
894 return NULL;
896 Command *cmd = command_new(NULL);
897 if (!cmd)
898 return NULL;
900 cmd->address = address_parse_compound(vis, s, err);
901 skip_spaces(s);
903 cmd->argv[0] = parse_cmdname(s);
905 if (!cmd->argv[0]) {
906 char name[2] = { **s ? **s : 'p', '\0' };
907 if (**s)
908 (*s)++;
909 if (!(cmd->argv[0] = strdup(name)))
910 goto fail;
913 const CommandDef *cmddef = command_lookup(vis, cmd->argv[0]);
914 if (!cmddef) {
915 *err = SAM_ERR_COMMAND;
916 goto fail;
919 cmd->cmddef = cmddef;
921 if (strcmp(cmd->argv[0], "{") == 0) {
922 Command *prev = NULL, *next;
923 int level = vis->nesting_level++;
924 do {
925 while (**s == ' ' || **s == '\t' || **s == '\n')
926 (*s)++;
927 next = command_parse(vis, s, err);
928 if (*err)
929 goto fail;
930 if (prev)
931 prev->next = next;
932 else
933 cmd->cmd = next;
934 } while ((prev = next));
935 if (level != vis->nesting_level) {
936 *err = SAM_ERR_UNMATCHED_BRACE;
937 goto fail;
939 } else if (strcmp(cmd->argv[0], "}") == 0) {
940 if (vis->nesting_level-- == 0) {
941 *err = SAM_ERR_UNMATCHED_BRACE;
942 goto fail;
944 command_free(cmd);
945 return NULL;
948 if (cmddef->flags & CMD_ADDRESS_NONE && cmd->address) {
949 *err = SAM_ERR_NO_ADDRESS;
950 goto fail;
953 if (cmddef->flags & CMD_FORCE && **s == '!') {
954 cmd->flags = '!';
955 (*s)++;
958 if ((cmddef->flags & CMD_COUNT) && (*err = parse_count(s, &cmd->count)))
959 goto fail;
961 if (cmddef->flags & CMD_REGEX) {
962 if ((cmddef->flags & CMD_REGEX_DEFAULT) && (!**s || **s == ' ')) {
963 skip_spaces(s);
964 } else {
965 const char *before = *s;
966 cmd->regex = parse_regex(vis, s);
967 if (!cmd->regex && (*s != before || !(cmddef->flags & CMD_COUNT))) {
968 *err = SAM_ERR_REGEX;
969 goto fail;
974 if (cmddef->flags & CMD_SHELL && !(cmd->argv[1] = parse_shellcmd(vis, s))) {
975 *err = SAM_ERR_SHELL;
976 goto fail;
979 if (cmddef->flags & CMD_TEXT && !(cmd->argv[1] = parse_text(s, &cmd->count))) {
980 *err = SAM_ERR_TEXT;
981 goto fail;
984 if (cmddef->flags & CMD_ARGV) {
985 parse_argv(s, &cmd->argv[1], MAX_ARGV-2);
986 cmd->argv[MAX_ARGV-1] = NULL;
989 if (cmddef->flags & CMD_CMD) {
990 skip_spaces(s);
991 if (cmddef->defcmd && (**s == '\n' || **s == '}' || **s == '\0')) {
992 if (**s == '\n')
993 (*s)++;
994 if (!(cmd->cmd = command_new(cmddef->defcmd)))
995 goto fail;
996 cmd->cmd->cmddef = command_lookup(vis, cmddef->defcmd);
997 } else {
998 if (!(cmd->cmd = command_parse(vis, s, err)))
999 goto fail;
1000 if (strcmp(cmd->argv[0], "X") == 0 || strcmp(cmd->argv[0], "Y") == 0) {
1001 Command *sel = command_new("select");
1002 if (!sel)
1003 goto fail;
1004 sel->cmd = cmd->cmd;
1005 sel->cmddef = &cmddef_select;
1006 cmd->cmd = sel;
1011 return cmd;
1012 fail:
1013 command_free(cmd);
1014 return NULL;
1017 static Command *sam_parse(Vis *vis, const char *cmd, enum SamError *err) {
1018 vis->nesting_level = 0;
1019 const char **s = &cmd;
1020 Command *c = command_parse(vis, s, err);
1021 if (!c)
1022 return NULL;
1023 while (**s == ' ' || **s == '\t' || **s == '\n')
1024 (*s)++;
1025 if (**s) {
1026 *err = SAM_ERR_NEWLINE;
1027 command_free(c);
1028 return NULL;
1031 Command *sel = command_new("select");
1032 if (!sel) {
1033 command_free(c);
1034 return NULL;
1036 sel->cmd = c;
1037 sel->cmddef = &cmddef_select;
1038 return sel;
1041 static Filerange address_line_evaluate(Address *addr, File *file, Filerange *range, int sign) {
1042 Text *txt = file->text;
1043 size_t offset = addr->number != EPOS ? addr->number : 1;
1044 size_t start = range->start, end = range->end, line;
1045 if (sign > 0) {
1046 char c;
1047 if (start < end && text_byte_get(txt, end-1, &c) && c == '\n')
1048 end--;
1049 line = text_lineno_by_pos(txt, end);
1050 line = text_pos_by_lineno(txt, line + offset);
1051 } else if (sign < 0) {
1052 line = text_lineno_by_pos(txt, start);
1053 line = offset < line ? text_pos_by_lineno(txt, line - offset) : 0;
1054 } else {
1055 if (addr->number == 0)
1056 return text_range_new(0, 0);
1057 line = text_pos_by_lineno(txt, addr->number);
1060 if (addr->type == 'g')
1061 return text_range_new(line, line);
1062 else
1063 return text_range_new(line, text_line_next(txt, line));
1066 static Filerange address_evaluate(Address *addr, File *file, Selection *sel, Filerange *range, int sign) {
1067 Filerange ret = text_range_empty();
1069 do {
1070 switch (addr->type) {
1071 case '#':
1072 if (sign > 0)
1073 ret.start = ret.end = range->end + addr->number;
1074 else if (sign < 0)
1075 ret.start = ret.end = range->start - addr->number;
1076 else
1077 ret = text_range_new(addr->number, addr->number);
1078 break;
1079 case 'l':
1080 case 'g':
1081 ret = address_line_evaluate(addr, file, range, sign);
1082 break;
1083 case '\'':
1085 size_t pos = EPOS;
1086 Array *marks = &file->marks[addr->number];
1087 size_t idx = sel ? view_selections_number(sel) : 0;
1088 SelectionRegion *sr = array_get(marks, idx);
1089 if (sr)
1090 pos = text_mark_get(file->text, sr->cursor);
1091 ret = text_range_new(pos, pos);
1092 break;
1094 case '?':
1095 sign = sign == 0 ? -1 : -sign;
1096 /* fall through */
1097 case '/':
1098 if (sign >= 0)
1099 ret = text_object_search_forward(file->text, range->end, addr->regex);
1100 else
1101 ret = text_object_search_backward(file->text, range->start, addr->regex);
1102 break;
1103 case '$':
1105 size_t size = text_size(file->text);
1106 ret = text_range_new(size, size);
1107 break;
1109 case '.':
1110 ret = *range;
1111 break;
1112 case '+':
1113 case '-':
1114 sign = addr->type == '+' ? +1 : -1;
1115 if (!addr->right || addr->right->type == '+' || addr->right->type == '-')
1116 ret = address_line_evaluate(addr, file, range, sign);
1117 break;
1118 case ',':
1119 case ';':
1121 Filerange left, right;
1122 if (addr->left)
1123 left = address_evaluate(addr->left, file, sel, range, 0);
1124 else
1125 left = text_range_new(0, 0);
1127 if (addr->type == ';')
1128 range = &left;
1130 if (addr->right) {
1131 right = address_evaluate(addr->right, file, sel, range, 0);
1132 } else {
1133 size_t size = text_size(file->text);
1134 right = text_range_new(size, size);
1136 /* TODO: enforce strict ordering? */
1137 return text_range_union(&left, &right);
1139 case '%':
1140 return text_range_new(0, text_size(file->text));
1142 if (text_range_valid(&ret))
1143 range = &ret;
1144 } while ((addr = addr->right));
1146 return ret;
1149 static bool count_evaluate(Command *cmd) {
1150 Count *count = &cmd->count;
1151 if (count->mod)
1152 return count->start ? cmd->iteration % count->start == 0 : true;
1153 return count->start <= cmd->iteration && cmd->iteration <= count->end;
1156 static bool sam_execute(Vis *vis, Win *win, Command *cmd, Selection *sel, Filerange *range) {
1157 bool ret = true;
1158 if (cmd->address && win)
1159 *range = address_evaluate(cmd->address, win->file, sel, range, 0);
1161 cmd->iteration++;
1162 switch (cmd->argv[0][0]) {
1163 case '{':
1165 for (Command *c = cmd->cmd; c && ret; c = c->next)
1166 ret &= sam_execute(vis, win, c, NULL, range);
1167 view_selections_dispose_force(sel);
1168 break;
1170 default:
1171 ret = cmd->cmddef->func(vis, win, cmd, cmd->argv, sel, range);
1172 break;
1174 return ret;
1177 static enum SamError validate(Command *cmd, bool loop, bool group) {
1178 if (cmd->cmddef->flags & CMD_DESTRUCTIVE) {
1179 if (loop)
1180 return SAM_ERR_LOOP_INVALID_CMD;
1181 if (group)
1182 return SAM_ERR_GROUP_INVALID_CMD;
1185 group |= (cmd->cmddef->flags & CMD_GROUP);
1186 loop |= (cmd->cmddef->flags & CMD_LOOP);
1187 for (Command *c = cmd->cmd; c; c = c->next) {
1188 enum SamError err = validate(c, loop, group);
1189 if (err != SAM_ERR_OK)
1190 return err;
1192 return SAM_ERR_OK;
1195 static enum SamError command_validate(Command *cmd) {
1196 return validate(cmd, false, false);
1199 static bool count_negative(Command *cmd) {
1200 if (cmd->count.start < 0 || cmd->count.end < 0)
1201 return true;
1202 for (Command *c = cmd->cmd; c; c = c->next) {
1203 if (c->cmddef->func != cmd_extract && c->cmddef->func != cmd_select) {
1204 if (count_negative(c))
1205 return true;
1208 return false;
1211 static void count_init(Command *cmd, int max) {
1212 Count *count = &cmd->count;
1213 cmd->iteration = 0;
1214 if (count->start < 0)
1215 count->start += max;
1216 if (count->end < 0)
1217 count->end += max;
1218 for (Command *c = cmd->cmd; c; c = c->next) {
1219 if (c->cmddef->func != cmd_extract && c->cmddef->func != cmd_select)
1220 count_init(c, max);
1224 enum SamError sam_cmd(Vis *vis, const char *s) {
1225 enum SamError err = SAM_ERR_OK;
1226 if (!s)
1227 return err;
1229 Command *cmd = sam_parse(vis, s, &err);
1230 if (!cmd) {
1231 if (err == SAM_ERR_OK)
1232 err = SAM_ERR_MEMORY;
1233 return err;
1236 err = command_validate(cmd);
1237 if (err != SAM_ERR_OK) {
1238 command_free(cmd);
1239 return err;
1242 for (File *file = vis->files; file; file = file->next) {
1243 if (file->internal)
1244 continue;
1245 sam_transcript_init(&file->transcript);
1248 bool visual = vis->mode->visual;
1249 size_t primary_pos = vis->win ? view_cursor_get(vis->win->view) : EPOS;
1250 Filerange range = text_range_empty();
1251 sam_execute(vis, vis->win, cmd, NULL, &range);
1253 for (File *file = vis->files; file; file = file->next) {
1254 if (file->internal)
1255 continue;
1256 Transcript *t = &file->transcript;
1257 if (t->error != SAM_ERR_OK) {
1258 err = t->error;
1259 sam_transcript_free(t);
1260 continue;
1262 vis_file_snapshot(vis, file);
1263 ptrdiff_t delta = 0;
1264 for (Change *c = t->changes; c; c = c->next) {
1265 c->range.start += delta;
1266 c->range.end += delta;
1267 if (c->type & TRANSCRIPT_DELETE) {
1268 text_delete_range(file->text, &c->range);
1269 delta -= text_range_size(&c->range);
1270 if (c->sel && c->type == TRANSCRIPT_DELETE) {
1271 if (visual)
1272 view_selections_dispose_force(c->sel);
1273 else
1274 view_cursors_to(c->sel, c->range.start);
1277 if (c->type & TRANSCRIPT_INSERT) {
1278 for (int i = 0; i < c->count; i++) {
1279 text_insert(file->text, c->range.start, c->data, c->len);
1280 delta += c->len;
1282 Filerange r = text_range_new(c->range.start,
1283 c->range.start + c->len * c->count);
1284 if (c->sel) {
1285 if (visual) {
1286 view_selections_set(c->sel, &r);
1287 view_selections_anchor(c->sel, true);
1288 } else {
1289 if (memchr(c->data, '\n', c->len))
1290 view_cursors_to(c->sel, r.start);
1291 else
1292 view_cursors_to(c->sel, r.end);
1294 } else if (visual) {
1295 Selection *sel = view_selections_new(c->win->view, r.start);
1296 if (sel) {
1297 view_selections_set(sel, &r);
1298 view_selections_anchor(sel, true);
1303 sam_transcript_free(&file->transcript);
1304 vis_file_snapshot(vis, file);
1307 for (Win *win = vis->windows; win; win = win->next)
1308 view_selections_normalize(win->view);
1310 if (vis->win) {
1311 if (primary_pos != EPOS && view_selection_disposed(vis->win->view))
1312 view_cursor_to(vis->win->view, primary_pos);
1313 view_selections_primary_set(view_selections(vis->win->view));
1314 vis_jumplist_save(vis);
1315 bool completed = true;
1316 for (Selection *s = view_selections(vis->win->view); s; s = view_selections_next(s)) {
1317 if (view_selections_anchored(s)) {
1318 completed = false;
1319 break;
1322 vis_mode_switch(vis, completed ? VIS_MODE_NORMAL : VIS_MODE_VISUAL);
1324 command_free(cmd);
1325 return err;
1328 /* process text input, substitute register content for backreferences etc. */
1329 Buffer text(Vis *vis, const char *text) {
1330 Buffer buf;
1331 buffer_init(&buf);
1332 for (size_t len = strcspn(text, "\\&"); *text; len = strcspn(++text, "\\&")) {
1333 buffer_append(&buf, text, len);
1334 text += len;
1335 enum VisRegister regid = VIS_REG_INVALID;
1336 switch (text[0]) {
1337 case '&':
1338 regid = VIS_REG_AMPERSAND;
1339 break;
1340 case '\\':
1341 if ('1' <= text[1] && text[1] <= '9') {
1342 regid = VIS_REG_1 + text[1] - '1';
1343 text++;
1344 } else if (text[1] == '\\' || text[1] == '&') {
1345 text++;
1347 break;
1348 case '\0':
1349 goto out;
1352 const char *data;
1353 size_t reglen = 0;
1354 if (regid != VIS_REG_INVALID) {
1355 data = register_get(vis, &vis->registers[regid], &reglen);
1356 } else {
1357 data = text;
1358 reglen = 1;
1360 buffer_append(&buf, data, reglen);
1362 out:
1363 return buf;
1366 static bool cmd_insert(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1367 if (!win)
1368 return false;
1369 Buffer buf = text(vis, argv[1]);
1370 size_t len = buffer_length(&buf);
1371 char *data = buffer_move(&buf);
1372 bool ret = sam_insert(win, sel, range->start, data, len, cmd->count.start);
1373 if (!ret)
1374 free(data);
1375 return ret;
1378 static bool cmd_append(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1379 if (!win)
1380 return false;
1381 Buffer buf = text(vis, argv[1]);
1382 size_t len = buffer_length(&buf);
1383 char *data = buffer_move(&buf);
1384 bool ret = sam_insert(win, sel, range->end, data, len, cmd->count.start);
1385 if (!ret)
1386 free(data);
1387 return ret;
1390 static bool cmd_change(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1391 if (!win)
1392 return false;
1393 Buffer buf = text(vis, argv[1]);
1394 size_t len = buffer_length(&buf);
1395 char *data = buffer_move(&buf);
1396 bool ret = sam_change(win, sel, range, data, len, cmd->count.start);
1397 if (!ret)
1398 free(data);
1399 return ret;
1402 static bool cmd_delete(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1403 return win && sam_delete(win, sel, range);
1406 static bool cmd_guard(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1407 if (!win)
1408 return false;
1409 bool match = false;
1410 RegexMatch captures[1];
1411 size_t len = text_range_size(range);
1412 if (!cmd->regex)
1413 match = true;
1414 else if (!text_search_range_forward(win->file->text, range->start, len, cmd->regex, 1, captures, 0))
1415 match = captures[0].start < range->end;
1416 if ((count_evaluate(cmd) && match) ^ (argv[0][0] == 'v'))
1417 return sam_execute(vis, win, cmd->cmd, sel, range);
1418 view_selections_dispose_force(sel);
1419 return true;
1422 static int extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range, bool simulate) {
1423 bool ret = true;
1424 int count = 0;
1425 Text *txt = win->file->text;
1427 if (cmd->regex) {
1428 size_t start = range->start, end = range->end;
1429 size_t last_start = argv[0][0] == 'x' ? EPOS : start;
1430 size_t nsub = 1 + text_regex_nsub(cmd->regex);
1431 if (nsub > MAX_REGEX_SUB)
1432 nsub = MAX_REGEX_SUB;
1433 RegexMatch match[MAX_REGEX_SUB];
1434 while (start <= end) {
1435 char c;
1436 int flags = start > range->start &&
1437 text_byte_get(txt, start - 1, &c) && c != '\n' ?
1438 REG_NOTBOL : 0;
1439 bool found = !text_search_range_forward(txt, start, end - start,
1440 cmd->regex, nsub, match,
1441 flags);
1442 Filerange r = text_range_empty();
1443 if (found) {
1444 if (argv[0][0] == 'x')
1445 r = text_range_new(match[0].start, match[0].end);
1446 else
1447 r = text_range_new(last_start, match[0].start);
1448 if (match[0].start == match[0].end) {
1449 if (last_start == match[0].start) {
1450 start++;
1451 continue;
1453 /* in Plan 9's regexp library ^ matches the beginning
1454 * of a line, however in POSIX with REG_NEWLINE ^
1455 * matches the zero-length string immediately after a
1456 * newline. Try filtering out the last such match at EOF.
1458 if (end == match[0].start && start > range->start &&
1459 text_byte_get(txt, end-1, &c) && c == '\n')
1460 break;
1461 start = match[0].end + 1;
1462 } else {
1463 start = match[0].end;
1465 } else {
1466 if (argv[0][0] == 'y')
1467 r = text_range_new(start, end);
1468 start = end + 1;
1471 if (text_range_valid(&r)) {
1472 if (found) {
1473 for (size_t i = 0; i < nsub; i++) {
1474 Register *reg = &vis->registers[VIS_REG_AMPERSAND+i];
1475 register_put_range(vis, reg, txt, &match[i]);
1477 last_start = match[0].end;
1478 } else {
1479 last_start = start;
1481 if (simulate)
1482 count++;
1483 else
1484 ret &= sam_execute(vis, win, cmd->cmd, NULL, &r);
1487 } else {
1488 size_t start = range->start, end = range->end;
1489 while (start < end) {
1490 size_t next = text_line_next(txt, start);
1491 if (next > end)
1492 next = end;
1493 Filerange r = text_range_new(start, next);
1494 if (start == next || !text_range_valid(&r))
1495 break;
1496 if (simulate)
1497 count++;
1498 else
1499 ret &= sam_execute(vis, win, cmd->cmd, NULL, &r);
1500 start = next;
1504 if (!simulate)
1505 view_selections_dispose_force(sel);
1506 return simulate ? count : ret;
1509 static bool cmd_extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1510 if (!win || !text_range_valid(range))
1511 return false;
1512 int matches = 0;
1513 if (count_negative(cmd->cmd))
1514 matches = extract(vis, win, cmd, argv, sel, range, true);
1515 count_init(cmd->cmd, matches+1);
1516 return extract(vis, win, cmd, argv, sel, range, false);
1519 static bool cmd_select(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1520 Filerange r = text_range_empty();
1521 if (!win)
1522 return sam_execute(vis, NULL, cmd->cmd, NULL, &r);
1523 bool ret = true;
1524 View *view = win->view;
1525 Text *txt = win->file->text;
1526 bool multiple_cursors = view_selections_count(view) > 1;
1527 Selection *primary = view_selections_primary_get(view);
1529 if (vis->mode->visual)
1530 count_init(cmd->cmd, view_selections_count(view)+1);
1532 for (Selection *s = view_selections(view), *next; s && ret; s = next) {
1533 next = view_selections_next(s);
1534 size_t pos = view_cursors_pos(s);
1535 if (vis->mode->visual) {
1536 r = view_selections_get(s);
1537 } else if (cmd->cmd->address) {
1538 /* convert a single line range to a goto line motion */
1539 if (!multiple_cursors && cmd->cmd->cmddef->func == cmd_print) {
1540 Address *addr = cmd->cmd->address;
1541 switch (addr->type) {
1542 case '+':
1543 case '-':
1544 addr = addr->right;
1545 /* fall through */
1546 case 'l':
1547 if (addr && addr->type == 'l' && !addr->right)
1548 addr->type = 'g';
1549 break;
1552 r = text_range_new(pos, pos);
1553 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_POS) {
1554 r = text_range_new(pos, pos);
1555 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_LINE) {
1556 r = text_object_line(txt, pos);
1557 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_AFTER) {
1558 size_t next_line = text_line_next(txt, pos);
1559 r = text_range_new(next_line, next_line);
1560 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_ALL) {
1561 r = text_range_new(0, text_size(txt));
1562 } else if (!multiple_cursors && (cmd->cmd->cmddef->flags & CMD_ADDRESS_ALL_1CURSOR)) {
1563 r = text_range_new(0, text_size(txt));
1564 } else {
1565 r = text_range_new(pos, text_char_next(txt, pos));
1567 if (!text_range_valid(&r))
1568 r = text_range_new(0, 0);
1569 ret &= sam_execute(vis, win, cmd->cmd, s, &r);
1570 if (cmd->cmd->cmddef->flags & CMD_ONCE)
1571 break;
1574 if (vis->win && vis->win->view == view && primary != view_selections_primary_get(view))
1575 view_selections_primary_set(view_selections(view));
1576 return ret;
1579 static bool cmd_print(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1580 if (!win || !text_range_valid(range))
1581 return false;
1582 View *view = win->view;
1583 if (!sel)
1584 sel = view_selections_new_force(view, range->start);
1585 if (!sel)
1586 return false;
1587 if (range->start != range->end) {
1588 view_selections_set(sel, range);
1589 view_selections_anchor(sel, true);
1590 } else {
1591 view_cursors_to(sel, range->start);
1592 view_selection_clear(sel);
1594 return true;
1597 static bool cmd_files(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1598 bool ret = true;
1599 for (Win *wn, *w = vis->windows; w; w = wn) {
1600 /* w can get freed by sam_execute() so store w->next early */
1601 wn = w->next;
1602 if (w->file->internal)
1603 continue;
1604 bool match = !cmd->regex ||
1605 (w->file->name && text_regex_match(cmd->regex, w->file->name, 0) == 0);
1606 if (match ^ (argv[0][0] == 'Y')) {
1607 Filerange def = text_range_new(0, 0);
1608 ret &= sam_execute(vis, w, cmd->cmd, NULL, &def);
1611 return ret;
1614 static bool cmd_substitute(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1615 vis_info_show(vis, "Use :x/pattern/ c/replacement/ instead");
1616 return false;
1619 /* cmd_write stores win->file's contents end emits pre/post events.
1620 * If the range r covers the whole file, it is updated to account for
1621 * potential file's text mutation by a FILE_SAVE_PRE callback.
1623 static bool cmd_write(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *r) {
1624 if (!win)
1625 return false;
1627 File *file = win->file;
1628 if (sam_transcript_error(&file->transcript, SAM_ERR_WRITE_CONFLICT))
1629 return false;
1631 Text *text = file->text;
1632 Filerange range_all = text_range_new(0, text_size(text));
1633 bool write_entire_file = text_range_equal(r, &range_all);
1635 const char *filename = argv[1];
1636 if (!filename)
1637 filename = file->name;
1638 if (!filename) {
1639 if (file->fd == -1) {
1640 vis_info_show(vis, "Filename expected");
1641 return false;
1643 if (!strchr(argv[0], 'q')) {
1644 vis_info_show(vis, "No filename given, use 'wq' to write to stdout");
1645 return false;
1648 if (!vis_event_emit(vis, VIS_EVENT_FILE_SAVE_PRE, file, (char*)NULL) && cmd->flags != '!') {
1649 vis_info_show(vis, "Rejected write to stdout by pre-save hook");
1650 return false;
1652 /* a pre-save hook may have changed the text; need to re-take the range */
1653 if (write_entire_file)
1654 *r = text_range_new(0, text_size(text));
1656 bool visual = vis->mode->visual;
1658 for (Selection *s = view_selections(win->view); s; s = view_selections_next(s)) {
1659 Filerange range = visual ? view_selections_get(s) : *r;
1660 ssize_t written = text_write_range(text, &range, file->fd);
1661 if (written == -1 || (size_t)written != text_range_size(&range)) {
1662 vis_info_show(vis, "Can not write to stdout");
1663 return false;
1665 if (!visual)
1666 break;
1669 /* make sure the file is marked as saved i.e. not modified */
1670 text_save(text, NULL);
1671 vis_event_emit(vis, VIS_EVENT_FILE_SAVE_POST, file, (char*)NULL);
1672 return true;
1675 if (!argv[1] && cmd->flags != '!') {
1676 if (vis->mode->visual) {
1677 vis_info_show(vis, "WARNING: file will be reduced to active selection");
1678 return false;
1680 if (!write_entire_file) {
1681 vis_info_show(vis, "WARNING: file will be reduced to provided range");
1682 return false;
1686 for (const char **name = argv[1] ? &argv[1] : (const char*[]){ filename, NULL }; *name; name++) {
1688 char *path = absolute_path(*name);
1689 if (!path)
1690 return false;
1692 struct stat meta;
1693 bool existing_file = !stat(path, &meta);
1694 bool same_file = existing_file && file->name &&
1695 file->stat.st_dev == meta.st_dev && file->stat.st_ino == meta.st_ino;
1697 if (cmd->flags != '!') {
1698 if (same_file && file->stat.st_mtime && file->stat.st_mtime < meta.st_mtime) {
1699 vis_info_show(vis, "WARNING: file has been changed since reading it");
1700 goto err;
1702 if (existing_file && !same_file) {
1703 vis_info_show(vis, "WARNING: file exists");
1704 goto err;
1708 if (!vis_event_emit(vis, VIS_EVENT_FILE_SAVE_PRE, file, path) && cmd->flags != '!') {
1709 vis_info_show(vis, "Rejected write to `%s' by pre-save hook", path);
1710 goto err;
1712 /* a pre-save hook may have changed the text; need to re-take the range */
1713 if (write_entire_file)
1714 *r = text_range_new(0, text_size(text));
1716 TextSave *ctx = text_save_begin(text, AT_FDCWD, path, file->save_method);
1717 if (!ctx) {
1718 const char *msg = errno ? strerror(errno) : "try changing `:set savemethod`";
1719 vis_info_show(vis, "Can't write `%s': %s", path, msg);
1720 goto err;
1723 bool failure = false;
1724 bool visual = vis->mode->visual;
1726 for (Selection *s = view_selections(win->view); s; s = view_selections_next(s)) {
1727 Filerange range = visual ? view_selections_get(s) : *r;
1728 ssize_t written = text_save_write_range(ctx, &range);
1729 failure = (written == -1 || (size_t)written != text_range_size(&range));
1730 if (failure) {
1731 text_save_cancel(ctx);
1732 break;
1735 if (!visual)
1736 break;
1739 if (failure || !text_save_commit(ctx)) {
1740 vis_info_show(vis, "Can't write `%s': %s", path, strerror(errno));
1741 goto err;
1744 if (!file->name) {
1745 file_name_set(file, path);
1746 same_file = true;
1748 if (same_file || (!existing_file && strcmp(file->name, path) == 0))
1749 file->stat = text_stat(text);
1750 vis_event_emit(vis, VIS_EVENT_FILE_SAVE_POST, file, path);
1751 free(path);
1752 continue;
1754 err:
1755 free(path);
1756 return false;
1758 return true;
1761 static ssize_t read_buffer(void *context, char *data, size_t len) {
1762 buffer_append(context, data, len);
1763 return len;
1766 static bool cmd_filter(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1767 if (!win)
1768 return false;
1770 Buffer bufout, buferr;
1771 buffer_init(&bufout);
1772 buffer_init(&buferr);
1774 int status = vis_pipe(vis, win->file, range, &argv[1], &bufout, read_buffer, &buferr, read_buffer, false);
1776 if (vis->interrupted) {
1777 vis_info_show(vis, "Command cancelled");
1778 } else if (status == 0) {
1779 size_t len = buffer_length(&bufout);
1780 char *data = buffer_move(&bufout);
1781 if (!sam_change(win, sel, range, data, len, 1))
1782 free(data);
1783 } else {
1784 vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
1787 buffer_release(&bufout);
1788 buffer_release(&buferr);
1790 return !vis->interrupted && status == 0;
1793 static bool cmd_launch(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1794 Filerange invalid = text_range_new(sel ? view_cursors_pos(sel) : range->start, EPOS);
1795 return cmd_filter(vis, win, cmd, argv, sel, &invalid);
1798 static bool cmd_pipein(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1799 if (!win)
1800 return false;
1801 Filerange filter_range = text_range_new(range->end, range->end);
1802 bool ret = cmd_filter(vis, win, cmd, argv, sel, &filter_range);
1803 if (ret)
1804 ret = sam_delete(win, NULL, range);
1805 return ret;
1808 static bool cmd_pipeout(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1809 if (!win)
1810 return false;
1811 Buffer buferr;
1812 buffer_init(&buferr);
1814 int status = vis_pipe(vis, win->file, range, (const char*[]){ argv[1], NULL }, NULL, NULL, &buferr, read_buffer, false);
1816 if (vis->interrupted)
1817 vis_info_show(vis, "Command cancelled");
1818 else if (status != 0)
1819 vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
1821 buffer_release(&buferr);
1823 return !vis->interrupted && status == 0;
1826 static bool cmd_cd(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1827 const char *dir = argv[1];
1828 if (!dir)
1829 dir = getenv("HOME");
1830 return dir && chdir(dir) == 0;
1833 #include "vis-cmds.c"