Revert "vis: process aliased key sequences individually"
[vis.git] / sam.c
blob7bd4c5e90b6f176b402375c5ffd171928db6eaee
1 /*
2 * Heavily inspired (and partially based upon) the X11 version of
3 * Rob Pike's sam text editor originally written for Plan 9.
5 * Copyright © 2016-2017 Marc André Tanner <mat at brain-dump.org>
6 * Copyright © 1998 by Lucent Technologies
8 * Permission to use, copy, modify, and distribute this software for any
9 * purpose without fee is hereby granted, provided that this entire notice
10 * is included in all copies of any software which is or includes a copy
11 * or modification of this software and in all copies of the supporting
12 * documentation for such software.
14 * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
15 * WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR LUCENT TECHNOLOGIES MAKE ANY
16 * REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY
17 * OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.
19 #include <string.h>
20 #include <strings.h>
21 #include <stdio.h>
22 #include <ctype.h>
23 #include <errno.h>
24 #include <unistd.h>
25 #include <limits.h>
26 #include "sam.h"
27 #include "vis-core.h"
28 #include "buffer.h"
29 #include "text.h"
30 #include "text-motions.h"
31 #include "text-objects.h"
32 #include "text-regex.h"
33 #include "util.h"
35 #define MAX_ARGV 8
37 typedef struct Address Address;
38 typedef struct Command Command;
39 typedef struct CommandDef CommandDef;
41 struct Change {
42 enum ChangeType {
43 TRANSCRIPT_INSERT = 1 << 0,
44 TRANSCRIPT_DELETE = 1 << 1,
45 TRANSCRIPT_CHANGE = TRANSCRIPT_INSERT|TRANSCRIPT_DELETE,
46 } type;
47 Win *win; /* window in which changed file is being displayed */
48 Selection *cursor; /* cursor associated with this change, might be NULL */
49 Filerange range; /* inserts are denoted by zero sized range (same start/end) */
50 const char *data; /* will be free(3)-ed after transcript has been processed */
51 size_t len; /* size in bytes of the chunk pointed to by data */
52 Change *next; /* modification position increase monotonically */
55 struct Address {
56 char type; /* # (char) l (line) g (goto line) / ? . $ + - , ; % ' */
57 Regex *regex; /* NULL denotes default for x, y, X, and Y commands */
58 size_t number; /* line or character number */
59 Address *left; /* left hand side of a compound address , ; */
60 Address *right; /* either right hand side of a compound address or next address */
63 typedef struct {
64 int start, end; /* interval [n,m] */
65 bool mod; /* % every n-th match, implies n == m */
66 } Count;
68 struct Command {
69 const char *argv[MAX_ARGV];/* [0]=cmd-name, [1..MAX_ARGV-2]=arguments, last element always NULL */
70 Address *address; /* range of text for command */
71 Regex *regex; /* regex to match, used by x, y, g, v, X, Y */
72 const CommandDef *cmddef; /* which command is this? */
73 Count count; /* command count, defaults to [0,+inf] */
74 int iteration; /* current command loop iteration */
75 char flags; /* command specific flags */
76 Command *cmd; /* target of x, y, g, v, X, Y, { */
77 Command *next; /* next command in {} group */
80 struct CommandDef {
81 const char *name; /* command name */
82 VIS_HELP_DECL(const char *help;) /* short, one-line help text */
83 enum {
84 CMD_NONE = 0, /* standalone command without any arguments */
85 CMD_CMD = 1 << 0, /* does the command take a sub/target command? */
86 CMD_REGEX = 1 << 1, /* regex after command? */
87 CMD_REGEX_DEFAULT = 1 << 2, /* is the regex optional i.e. can we use a default? */
88 CMD_COUNT = 1 << 3, /* does the command support a count as in s2/../? */
89 CMD_TEXT = 1 << 4, /* does the command need a text to insert? */
90 CMD_ADDRESS_NONE = 1 << 5, /* is it an error to specify an address for the command? */
91 CMD_ADDRESS_POS = 1 << 6, /* no address implies an empty range at current cursor position */
92 CMD_ADDRESS_LINE = 1 << 7, /* if no address is given, use the current line */
93 CMD_ADDRESS_AFTER = 1 << 8, /* if no address is given, begin at the start of the next line */
94 CMD_ADDRESS_ALL = 1 << 9, /* if no address is given, apply to whole file (independent of #cursors) */
95 CMD_ADDRESS_ALL_1CURSOR = 1 << 10, /* if no address is given and only 1 cursor exists, apply to whole file */
96 CMD_SHELL = 1 << 11, /* command needs a shell command as argument */
97 CMD_FORCE = 1 << 12, /* can the command be forced with ! */
98 CMD_ARGV = 1 << 13, /* whether shell like argument splitting is desired */
99 CMD_ONCE = 1 << 14, /* command should only be executed once, not for every selection */
100 CMD_LOOP = 1 << 15, /* a looping construct like `x`, `y` */
101 CMD_GROUP = 1 << 16, /* a command group { ... } */
102 CMD_DESTRUCTIVE = 1 << 17, /* command potentially destroys window */
103 } flags;
104 const char *defcmd; /* name of a default target command */
105 bool (*func)(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*); /* command implementation */
108 /* sam commands */
109 static bool cmd_insert(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
110 static bool cmd_append(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
111 static bool cmd_change(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
112 static bool cmd_delete(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
113 static bool cmd_guard(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
114 static bool cmd_extract(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
115 static bool cmd_select(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
116 static bool cmd_print(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
117 static bool cmd_files(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
118 static bool cmd_pipein(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
119 static bool cmd_pipeout(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
120 static bool cmd_filter(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
121 static bool cmd_launch(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
122 static bool cmd_substitute(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
123 static bool cmd_write(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
124 static bool cmd_read(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
125 static bool cmd_edit(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
126 static bool cmd_quit(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
127 static bool cmd_cd(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
128 /* vi(m) commands */
129 static bool cmd_set(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
130 static bool cmd_open(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
131 static bool cmd_qall(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
132 static bool cmd_split(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
133 static bool cmd_vsplit(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
134 static bool cmd_new(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
135 static bool cmd_vnew(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
136 static bool cmd_wq(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
137 static bool cmd_earlier_later(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
138 static bool cmd_help(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
139 static bool cmd_map(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
140 static bool cmd_unmap(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
141 static bool cmd_langmap(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
142 static bool cmd_user(Vis*, Win*, Command*, const char *argv[], Selection*, Filerange*);
144 static const CommandDef cmds[] = {
145 // name help
146 // flags, default command, implemenation
148 "a", VIS_HELP("Append text after range")
149 CMD_TEXT, NULL, cmd_append
150 }, {
151 "c", VIS_HELP("Change text in range")
152 CMD_TEXT, NULL, cmd_change
153 }, {
154 "d", VIS_HELP("Delete text in range")
155 CMD_NONE, NULL, cmd_delete
156 }, {
157 "g", VIS_HELP("If range contains regexp, run command")
158 CMD_COUNT|CMD_REGEX|CMD_CMD, "p", cmd_guard
159 }, {
160 "i", VIS_HELP("Insert text before range")
161 CMD_TEXT, NULL, cmd_insert
162 }, {
163 "p", VIS_HELP("Create selection covering range")
164 CMD_NONE, NULL, cmd_print
165 }, {
166 "s", VIS_HELP("Substitute: use x/pattern/ c/replacement/ instead")
167 CMD_SHELL|CMD_ADDRESS_LINE, NULL, cmd_substitute
168 }, {
169 "v", VIS_HELP("If range does not contain regexp, run command")
170 CMD_COUNT|CMD_REGEX|CMD_CMD, "p", cmd_guard
171 }, {
172 "x", VIS_HELP("Set range and run command on each match")
173 CMD_CMD|CMD_REGEX|CMD_REGEX_DEFAULT|CMD_ADDRESS_ALL_1CURSOR|CMD_LOOP, "p", cmd_extract
174 }, {
175 "y", VIS_HELP("As `x` but select unmatched text")
176 CMD_CMD|CMD_REGEX|CMD_ADDRESS_ALL_1CURSOR|CMD_LOOP, "p", cmd_extract
177 }, {
178 "X", VIS_HELP("Run command on files whose name matches")
179 CMD_CMD|CMD_REGEX|CMD_REGEX_DEFAULT|CMD_ADDRESS_NONE|CMD_ONCE, NULL, cmd_files
180 }, {
181 "Y", VIS_HELP("As `X` but select unmatched files")
182 CMD_CMD|CMD_REGEX|CMD_ADDRESS_NONE|CMD_ONCE, NULL, cmd_files
183 }, {
184 ">", VIS_HELP("Send range to stdin of command")
185 CMD_SHELL|CMD_ADDRESS_LINE, NULL, cmd_pipeout
186 }, {
187 "<", VIS_HELP("Replace range by stdout of command")
188 CMD_SHELL|CMD_ADDRESS_POS, NULL, cmd_pipein
189 }, {
190 "|", VIS_HELP("Pipe range through command")
191 CMD_SHELL|CMD_ADDRESS_POS, NULL, cmd_filter
192 }, {
193 "!", VIS_HELP("Run the command")
194 CMD_SHELL|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_launch
195 }, {
196 "w", VIS_HELP("Write range to named file")
197 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_ALL, NULL, cmd_write
198 }, {
199 "r", VIS_HELP("Replace range by contents of file")
200 CMD_ARGV|CMD_ADDRESS_AFTER, NULL, cmd_read
201 }, {
202 "{", VIS_HELP("Start of command group")
203 CMD_GROUP, NULL, NULL
204 }, {
205 "}", VIS_HELP("End of command group" )
206 CMD_NONE, NULL, NULL
207 }, {
208 "e", VIS_HELP("Edit file")
209 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE|CMD_DESTRUCTIVE, NULL, cmd_edit
210 }, {
211 "q", VIS_HELP("Quit the current window")
212 CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE|CMD_DESTRUCTIVE, NULL, cmd_quit
213 }, {
214 "cd", VIS_HELP("Change directory")
215 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_cd
217 /* vi(m) related commands */
219 "help", VIS_HELP("Show this help")
220 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_help
221 }, {
222 "map", VIS_HELP("Map key binding `:map <mode> <lhs> <rhs>`")
223 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_map
224 }, {
225 "map-window", VIS_HELP("As `map` but window local")
226 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_map
227 }, {
228 "unmap", VIS_HELP("Unmap key binding `:unmap <mode> <lhs>`")
229 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_unmap
230 }, {
231 "unmap-window", VIS_HELP("As `unmap` but window local")
232 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_unmap
233 }, {
234 "langmap", VIS_HELP("Map keyboard layout `:langmap <locale-keys> <latin-keys>`")
235 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_langmap
236 }, {
237 "new", VIS_HELP("Create new window")
238 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_new
239 }, {
240 "open", VIS_HELP("Open file")
241 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_open
242 }, {
243 "qall", VIS_HELP("Exit vis")
244 CMD_FORCE|CMD_ONCE|CMD_ADDRESS_NONE|CMD_DESTRUCTIVE, NULL, cmd_qall
245 }, {
246 "set", VIS_HELP("Set option")
247 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_set
248 }, {
249 "split", VIS_HELP("Horizontally split window")
250 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_split
251 }, {
252 "vnew", VIS_HELP("As `:new` but split vertically")
253 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_vnew
254 }, {
255 "vsplit", VIS_HELP("Vertically split window")
256 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_vsplit
257 }, {
258 "wq", VIS_HELP("Write file and quit")
259 CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_ALL|CMD_DESTRUCTIVE, NULL, cmd_wq
260 }, {
261 "earlier", VIS_HELP("Go to older text state")
262 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_earlier_later
263 }, {
264 "later", VIS_HELP("Go to newer text state")
265 CMD_ARGV|CMD_ONCE|CMD_ADDRESS_NONE, NULL, cmd_earlier_later
267 { NULL, VIS_HELP(NULL) CMD_NONE, NULL, NULL },
270 static const CommandDef cmddef_select = {
271 NULL, VIS_HELP(NULL) CMD_NONE, NULL, cmd_select
274 /* :set command options */
275 typedef struct {
276 const char *names[3]; /* name and optional alias */
277 enum VisOption flags; /* option type, etc. */
278 VIS_HELP_DECL(const char *help;) /* short, one line help text */
279 VisOptionFunction *func; /* option handler, NULL for bulitins */
280 void *context; /* context passed to option handler function */
281 } OptionDef;
283 enum {
284 OPTION_SHELL,
285 OPTION_ESCDELAY,
286 OPTION_AUTOINDENT,
287 OPTION_EXPANDTAB,
288 OPTION_TABWIDTH,
289 OPTION_SHOW_SPACES,
290 OPTION_SHOW_TABS,
291 OPTION_SHOW_NEWLINES,
292 OPTION_SHOW_EOF,
293 OPTION_NUMBER,
294 OPTION_NUMBER_RELATIVE,
295 OPTION_CURSOR_LINE,
296 OPTION_COLOR_COLUMN,
297 OPTION_SAVE_METHOD,
298 OPTION_CHANGE_256COLORS,
301 static const OptionDef options[] = {
302 [OPTION_SHELL] = {
303 { "shell" },
304 VIS_OPTION_TYPE_STRING,
305 VIS_HELP("Shell to use for external commands (default: $SHELL, /etc/passwd, /bin/sh)")
307 [OPTION_ESCDELAY] = {
308 { "escdelay" },
309 VIS_OPTION_TYPE_NUMBER,
310 VIS_HELP("Milliseconds to wait to distinguish <Escape> from terminal escape sequences")
312 [OPTION_AUTOINDENT] = {
313 { "autoindent", "ai" },
314 VIS_OPTION_TYPE_BOOL,
315 VIS_HELP("Copy leading white space from previous line")
317 [OPTION_EXPANDTAB] = {
318 { "expandtab", "et" },
319 VIS_OPTION_TYPE_BOOL,
320 VIS_HELP("Replace entered <Tab> with `tabwidth` spaces")
322 [OPTION_TABWIDTH] = {
323 { "tabwidth", "tw" },
324 VIS_OPTION_TYPE_NUMBER,
325 VIS_HELP("Number of spaces to display (and insert if `expandtab` is enabled) for a tab")
327 [OPTION_SHOW_SPACES] = {
328 { "show-spaces" },
329 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
330 VIS_HELP("Display replacement symbol instead of a space")
332 [OPTION_SHOW_TABS] = {
333 { "show-tabs" },
334 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
335 VIS_HELP("Display replacement symbol for tabs")
337 [OPTION_SHOW_NEWLINES] = {
338 { "show-newlines" },
339 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
340 VIS_HELP("Display replacement symbol for newlines")
342 [OPTION_SHOW_EOF] = {
343 { "show-eof" },
344 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
345 VIS_HELP("Display replacement symbol for lines after the end of the file")
347 [OPTION_NUMBER] = {
348 { "numbers", "nu" },
349 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
350 VIS_HELP("Display absolute line numbers")
352 [OPTION_NUMBER_RELATIVE] = {
353 { "relativenumbers", "rnu" },
354 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
355 VIS_HELP("Display relative line numbers")
357 [OPTION_CURSOR_LINE] = {
358 { "cursorline", "cul" },
359 VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
360 VIS_HELP("Highlight current cursor line")
362 [OPTION_COLOR_COLUMN] = {
363 { "colorcolumn", "cc" },
364 VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
365 VIS_HELP("Highlight a fixed column")
367 [OPTION_SAVE_METHOD] = {
368 { "savemethod" },
369 VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW,
370 VIS_HELP("Save method to use for current file 'auto', 'atomic' or 'inplace'")
372 [OPTION_CHANGE_256COLORS] = {
373 { "change-256colors" },
374 VIS_OPTION_TYPE_BOOL,
375 VIS_HELP("Change 256 color palette to support 24bit colors")
379 bool sam_init(Vis *vis) {
380 if (!(vis->cmds = map_new()))
381 return false;
382 bool ret = true;
383 for (const CommandDef *cmd = cmds; cmd && cmd->name; cmd++)
384 ret &= map_put(vis->cmds, cmd->name, cmd);
386 if (!(vis->options = map_new()))
387 return false;
388 for (int i = 0; i < LENGTH(options); i++) {
389 for (const char *const *name = options[i].names; *name; name++)
390 ret &= map_put(vis->options, *name, &options[i]);
393 return ret;
396 const char *sam_error(enum SamError err) {
397 static const char *error_msg[] = {
398 [SAM_ERR_OK] = "Success",
399 [SAM_ERR_MEMORY] = "Out of memory",
400 [SAM_ERR_ADDRESS] = "Bad address",
401 [SAM_ERR_NO_ADDRESS] = "Command takes no address",
402 [SAM_ERR_UNMATCHED_BRACE] = "Unmatched `}'",
403 [SAM_ERR_REGEX] = "Bad regular expression",
404 [SAM_ERR_TEXT] = "Bad text",
405 [SAM_ERR_SHELL] = "Shell command expected",
406 [SAM_ERR_COMMAND] = "Unknown command",
407 [SAM_ERR_EXECUTE] = "Error executing command",
408 [SAM_ERR_NEWLINE] = "Newline expected",
409 [SAM_ERR_MARK] = "Invalid mark",
410 [SAM_ERR_CONFLICT] = "Conflicting changes",
411 [SAM_ERR_WRITE_CONFLICT] = "Can not write while changing",
412 [SAM_ERR_LOOP_INVALID_CMD] = "Destructive command in looping construct",
413 [SAM_ERR_GROUP_INVALID_CMD] = "Destructive command in group",
414 [SAM_ERR_COUNT] = "Invalid count",
417 size_t idx = err;
418 return idx < LENGTH(error_msg) ? error_msg[idx] : NULL;
421 static void change_free(Change *c) {
422 if (!c)
423 return;
424 free((char*)c->data);
425 free(c);
428 static Change *change_new(Transcript *t, enum ChangeType type, Filerange *range, Win *win, Selection *sel) {
429 if (!text_range_valid(range))
430 return NULL;
431 Change **prev, *next;
432 if (t->latest && t->latest->range.end <= range->start) {
433 prev = &t->latest->next;
434 next = t->latest->next;
435 } else {
436 prev = &t->changes;
437 next = t->changes;
439 while (next && next->range.end <= range->start) {
440 prev = &next->next;
441 next = next->next;
443 if (next && next->range.start < range->end) {
444 t->error = SAM_ERR_CONFLICT;
445 return NULL;
447 Change *new = calloc(1, sizeof *new);
448 if (new) {
449 new->type = type;
450 new->range = *range;
451 new->cursor = sel;
452 new->win = win;
453 new->next = next;
454 *prev = new;
455 t->latest = new;
457 return new;
460 static void sam_transcript_init(Transcript *t) {
461 memset(t, 0, sizeof *t);
464 static bool sam_transcript_error(Transcript *t, enum SamError error) {
465 if (t->changes)
466 t->error = error;
467 return t->error;
470 static void sam_transcript_free(Transcript *t) {
471 for (Change *c = t->changes, *next; c; c = next) {
472 next = c->next;
473 change_free(c);
477 static bool sam_insert(Win *win, Selection *sel, size_t pos, const char *data, size_t len) {
478 Filerange range = text_range_new(pos, pos);
479 Change *c = change_new(&win->file->transcript, TRANSCRIPT_INSERT, &range, win, sel);
480 if (c) {
481 c->data = data;
482 c->len = len;
484 return c;
487 static bool sam_delete(Win *win, Selection *sel, Filerange *range) {
488 return change_new(&win->file->transcript, TRANSCRIPT_DELETE, range, win, sel);
491 static bool sam_change(Win *win, Selection *sel, Filerange *range, const char *data, size_t len) {
492 Change *c = change_new(&win->file->transcript, TRANSCRIPT_CHANGE, range, win, sel);
493 if (c) {
494 c->data = data;
495 c->len = len;
497 return c;
500 static Address *address_new(void) {
501 Address *addr = calloc(1, sizeof *addr);
502 if (addr)
503 addr->number = EPOS;
504 return addr;
507 static void address_free(Address *addr) {
508 if (!addr)
509 return;
510 text_regex_free(addr->regex);
511 address_free(addr->left);
512 address_free(addr->right);
513 free(addr);
516 static void skip_spaces(const char **s) {
517 while (**s == ' ' || **s == '\t')
518 (*s)++;
521 static char *parse_until(const char **s, const char *until, const char *escchars, int type){
522 Buffer buf;
523 buffer_init(&buf);
524 size_t len = strlen(until);
525 bool escaped = false;
527 for (; **s && (!memchr(until, **s, len) || escaped); (*s)++) {
528 if (type != CMD_SHELL && !escaped && **s == '\\') {
529 escaped = true;
530 continue;
533 char c = **s;
535 if (escaped) {
536 escaped = false;
537 if (c == '\n')
538 continue;
539 if (c == 'n') {
540 c = '\n';
541 } else if (c == 't') {
542 c = '\t';
543 } else if (type != CMD_REGEX && type != CMD_TEXT && c == '\\') {
544 // ignore one of the back slashes
545 } else {
546 bool delim = memchr(until, c, len);
547 bool esc = escchars && memchr(escchars, c, strlen(escchars));
548 if (!delim && !esc)
549 buffer_append(&buf, "\\", 1);
553 if (!buffer_append(&buf, &c, 1)) {
554 buffer_release(&buf);
555 return NULL;
559 buffer_terminate(&buf);
561 return buffer_move(&buf);
564 static char *parse_delimited(const char **s, int type) {
565 char delim[2] = { **s, '\0' };
566 if (!delim[0] || isspace((unsigned char)delim[0]))
567 return NULL;
568 (*s)++;
569 char *chunk = parse_until(s, delim, NULL, type);
570 if (**s == delim[0])
571 (*s)++;
572 return chunk;
575 static char *parse_text(const char **s) {
576 skip_spaces(s);
577 if (**s != '\n') {
578 const char *before = *s;
579 char *text = parse_delimited(s, CMD_TEXT);
580 return (!text && *s != before) ? strdup("") : text;
583 Buffer buf;
584 buffer_init(&buf);
585 const char *start = *s + 1;
586 bool dot = false;
588 for ((*s)++; **s && (!dot || **s != '\n'); (*s)++)
589 dot = (**s == '.');
591 if (!dot || !buffer_put(&buf, start, *s - start - 1) ||
592 !buffer_append(&buf, "\0", 1)) {
593 buffer_release(&buf);
594 return NULL;
597 return buffer_move(&buf);
600 static char *parse_shellcmd(Vis *vis, const char **s) {
601 skip_spaces(s);
602 char *cmd = parse_until(s, "\n", NULL, false);
603 if (!cmd) {
604 const char *last_cmd = register_get(vis, &vis->registers[VIS_REG_SHELL], NULL);
605 return last_cmd ? strdup(last_cmd) : NULL;
607 register_put0(vis, &vis->registers[VIS_REG_SHELL], cmd);
608 return cmd;
611 static void parse_argv(const char **s, const char *argv[], size_t maxarg) {
612 for (size_t i = 0; i < maxarg; i++) {
613 skip_spaces(s);
614 if (**s == '"' || **s == '\'')
615 argv[i] = parse_delimited(s, CMD_ARGV);
616 else
617 argv[i] = parse_until(s, " \t\n", "\'\"", CMD_ARGV);
621 static bool valid_cmdname(const char *s) {
622 unsigned char c = (unsigned char)*s;
623 return c && !isspace(c) && !isdigit(c) && (!ispunct(c) || (c == '-' && valid_cmdname(s+1)));
626 static char *parse_cmdname(const char **s) {
627 Buffer buf;
628 buffer_init(&buf);
630 skip_spaces(s);
631 while (valid_cmdname(*s))
632 buffer_append(&buf, (*s)++, 1);
634 buffer_terminate(&buf);
636 return buffer_move(&buf);
639 static Regex *parse_regex(Vis *vis, const char **s) {
640 const char *before = *s;
641 char *pattern = parse_delimited(s, CMD_REGEX);
642 if (!pattern && *s == before)
643 return NULL;
644 Regex *regex = vis_regex(vis, pattern);
645 free(pattern);
646 return regex;
649 static int parse_number(const char **s) {
650 char *end = NULL;
651 int number = strtoull(*s, &end, 10);
652 if (end == *s)
653 return 0;
654 *s = end;
655 return number;
658 static enum SamError parse_count(const char **s, Count *count) {
659 count->mod = **s == '%';
661 if (count->mod) {
662 (*s)++;
663 int n = parse_number(s);
664 if (!n)
665 return SAM_ERR_COUNT;
666 count->start = n;
667 count->end = n;
668 return SAM_ERR_OK;
671 const char *before = *s;
672 if (!(count->start = parse_number(s)) && *s != before)
673 return SAM_ERR_COUNT;
674 if (**s != ',') {
675 count->end = count->start ? count->start : INT_MAX;
676 return SAM_ERR_OK;
677 } else {
678 (*s)++;
680 before = *s;
681 if (!(count->end = parse_number(s)) && *s != before)
682 return SAM_ERR_COUNT;
683 if (!count->end)
684 count->end = INT_MAX;
685 return SAM_ERR_OK;
688 static Address *address_parse_simple(Vis *vis, const char **s, enum SamError *err) {
690 skip_spaces(s);
692 Address addr = {
693 .type = **s,
694 .regex = NULL,
695 .number = EPOS,
696 .left = NULL,
697 .right = NULL,
700 switch (addr.type) {
701 case '#': /* character #n */
702 (*s)++;
703 addr.number = parse_number(s);
704 break;
705 case '0': case '1': case '2': case '3': case '4': /* line n */
706 case '5': case '6': case '7': case '8': case '9':
707 addr.type = 'l';
708 addr.number = parse_number(s);
709 break;
710 case '\'':
711 (*s)++;
712 if ((addr.number = vis_mark_from(vis, **s)) == VIS_MARK_INVALID) {
713 *err = SAM_ERR_MARK;
714 return NULL;
716 (*s)++;
717 break;
718 case '/': /* regexp forwards */
719 case '?': /* regexp backwards */
720 addr.regex = parse_regex(vis, s);
721 if (!addr.regex) {
722 *err = SAM_ERR_REGEX;
723 return NULL;
725 break;
726 case '$': /* end of file */
727 case '.':
728 case '+':
729 case '-':
730 case '%':
731 (*s)++;
732 break;
733 default:
734 return NULL;
737 if ((addr.right = address_parse_simple(vis, s, err))) {
738 switch (addr.right->type) {
739 case '.':
740 case '$':
741 return NULL;
742 case '#':
743 case 'l':
744 case '/':
745 case '?':
746 if (addr.type != '+' && addr.type != '-') {
747 Address *plus = address_new();
748 if (!plus) {
749 address_free(addr.right);
750 return NULL;
752 plus->type = '+';
753 plus->right = addr.right;
754 addr.right = plus;
756 break;
760 Address *ret = address_new();
761 if (!ret) {
762 address_free(addr.right);
763 return NULL;
765 *ret = addr;
766 return ret;
769 static Address *address_parse_compound(Vis *vis, const char **s, enum SamError *err) {
770 Address addr = { 0 }, *left = address_parse_simple(vis, s, err), *right = NULL;
771 skip_spaces(s);
772 addr.type = **s;
773 switch (addr.type) {
774 case ',': /* a1,a2 */
775 case ';': /* a1;a2 */
776 (*s)++;
777 right = address_parse_compound(vis, s, err);
778 if (right && (right->type == ',' || right->type == ';') && !right->left) {
779 *err = SAM_ERR_ADDRESS;
780 goto fail;
782 break;
783 default:
784 return left;
787 addr.left = left;
788 addr.right = right;
790 Address *ret = address_new();
791 if (ret) {
792 *ret = addr;
793 return ret;
796 fail:
797 address_free(left);
798 address_free(right);
799 return NULL;
802 static Command *command_new(const char *name) {
803 Command *cmd = calloc(1, sizeof(Command));
804 if (!cmd)
805 return NULL;
806 if (name && !(cmd->argv[0] = strdup(name))) {
807 free(cmd);
808 return NULL;
810 return cmd;
813 static void command_free(Command *cmd) {
814 if (!cmd)
815 return;
817 for (Command *c = cmd->cmd, *next; c; c = next) {
818 next = c->next;
819 command_free(c);
822 for (const char **args = cmd->argv; *args; args++)
823 free((void*)*args);
824 address_free(cmd->address);
825 text_regex_free(cmd->regex);
826 free(cmd);
829 static const CommandDef *command_lookup(Vis *vis, const char *name) {
830 return map_closest(vis->cmds, name);
833 static Command *command_parse(Vis *vis, const char **s, enum SamError *err) {
834 if (!**s) {
835 *err = SAM_ERR_COMMAND;
836 return NULL;
838 Command *cmd = command_new(NULL);
839 if (!cmd)
840 return NULL;
842 cmd->address = address_parse_compound(vis, s, err);
843 skip_spaces(s);
845 cmd->argv[0] = parse_cmdname(s);
847 if (!cmd->argv[0]) {
848 char name[2] = { **s ? **s : 'p', '\0' };
849 if (**s)
850 (*s)++;
851 if (!(cmd->argv[0] = strdup(name)))
852 goto fail;
855 const CommandDef *cmddef = command_lookup(vis, cmd->argv[0]);
856 if (!cmddef) {
857 *err = SAM_ERR_COMMAND;
858 goto fail;
861 cmd->cmddef = cmddef;
863 if (strcmp(cmd->argv[0], "{") == 0) {
864 Command *prev = NULL, *next;
865 int level = vis->nesting_level++;
866 do {
867 while (**s == ' ' || **s == '\t' || **s == '\n')
868 (*s)++;
869 next = command_parse(vis, s, err);
870 if (*err)
871 goto fail;
872 if (prev)
873 prev->next = next;
874 else
875 cmd->cmd = next;
876 } while ((prev = next));
877 if (level != vis->nesting_level) {
878 *err = SAM_ERR_UNMATCHED_BRACE;
879 goto fail;
881 } else if (strcmp(cmd->argv[0], "}") == 0) {
882 if (vis->nesting_level-- == 0) {
883 *err = SAM_ERR_UNMATCHED_BRACE;
884 goto fail;
886 command_free(cmd);
887 return NULL;
890 if (cmddef->flags & CMD_ADDRESS_NONE && cmd->address) {
891 *err = SAM_ERR_NO_ADDRESS;
892 goto fail;
895 if (cmddef->flags & CMD_FORCE && **s == '!') {
896 cmd->flags = '!';
897 (*s)++;
900 if ((cmddef->flags & CMD_COUNT) && (*err = parse_count(s, &cmd->count)))
901 goto fail;
903 if (cmddef->flags & CMD_REGEX) {
904 if ((cmddef->flags & CMD_REGEX_DEFAULT) && (!**s || **s == ' ')) {
905 skip_spaces(s);
906 } else {
907 const char *before = *s;
908 cmd->regex = parse_regex(vis, s);
909 if (!cmd->regex && (*s != before || !(cmddef->flags & CMD_COUNT))) {
910 *err = SAM_ERR_REGEX;
911 goto fail;
916 if (cmddef->flags & CMD_SHELL && !(cmd->argv[1] = parse_shellcmd(vis, s))) {
917 *err = SAM_ERR_SHELL;
918 goto fail;
921 if (cmddef->flags & CMD_TEXT && !(cmd->argv[1] = parse_text(s))) {
922 *err = SAM_ERR_TEXT;
923 goto fail;
926 if (cmddef->flags & CMD_ARGV) {
927 parse_argv(s, &cmd->argv[1], MAX_ARGV-2);
928 cmd->argv[MAX_ARGV-1] = NULL;
931 if (cmddef->flags & CMD_CMD) {
932 skip_spaces(s);
933 if (cmddef->defcmd && (**s == '\n' || **s == '}' || **s == '\0')) {
934 if (**s == '\n')
935 (*s)++;
936 if (!(cmd->cmd = command_new(cmddef->defcmd)))
937 goto fail;
938 cmd->cmd->cmddef = command_lookup(vis, cmddef->defcmd);
939 } else {
940 if (!(cmd->cmd = command_parse(vis, s, err)))
941 goto fail;
942 if (strcmp(cmd->argv[0], "X") == 0 || strcmp(cmd->argv[0], "Y") == 0) {
943 Command *sel = command_new("select");
944 if (!sel)
945 goto fail;
946 sel->cmd = cmd->cmd;
947 sel->cmddef = &cmddef_select;
948 cmd->cmd = sel;
953 return cmd;
954 fail:
955 command_free(cmd);
956 return NULL;
959 static Command *sam_parse(Vis *vis, const char *cmd, enum SamError *err) {
960 vis->nesting_level = 0;
961 const char **s = &cmd;
962 Command *c = command_parse(vis, s, err);
963 if (!c)
964 return NULL;
965 while (**s == ' ' || **s == '\t' || **s == '\n')
966 (*s)++;
967 if (**s) {
968 *err = SAM_ERR_NEWLINE;
969 command_free(c);
970 return NULL;
973 Command *sel = command_new("select");
974 if (!sel) {
975 command_free(c);
976 return NULL;
978 sel->cmd = c;
979 sel->cmddef = &cmddef_select;
980 return sel;
983 static Filerange address_line_evaluate(Address *addr, File *file, Filerange *range, int sign) {
984 Text *txt = file->text;
985 size_t offset = addr->number != EPOS ? addr->number : 1;
986 size_t start = range->start, end = range->end, line;
987 if (sign > 0) {
988 char c;
989 if (start < end && end > 0 && text_byte_get(txt, end-1, &c) && c == '\n')
990 end--;
991 line = text_lineno_by_pos(txt, end);
992 line = text_pos_by_lineno(txt, line + offset);
993 } else if (sign < 0) {
994 line = text_lineno_by_pos(txt, start);
995 line = offset < line ? text_pos_by_lineno(txt, line - offset) : 0;
996 } else {
997 if (addr->number == 0)
998 return text_range_new(0, 0);
999 line = text_pos_by_lineno(txt, addr->number);
1002 if (addr->type == 'g')
1003 return text_range_new(line, line);
1004 else
1005 return text_range_new(line, text_line_next(txt, line));
1008 static Filerange address_evaluate(Address *addr, File *file, Selection *sel, Filerange *range, int sign) {
1009 Filerange ret = text_range_empty();
1011 do {
1012 switch (addr->type) {
1013 case '#':
1014 if (sign > 0)
1015 ret.start = ret.end = range->end + addr->number;
1016 else if (sign < 0)
1017 ret.start = ret.end = range->start - addr->number;
1018 else
1019 ret = text_range_new(addr->number, addr->number);
1020 break;
1021 case 'l':
1022 case 'g':
1023 ret = address_line_evaluate(addr, file, range, sign);
1024 break;
1025 case '\'':
1027 size_t pos = EPOS;
1028 Array *marks = &file->marks[addr->number];
1029 size_t idx = sel ? view_selections_number(sel) : 0;
1030 SelectionRegion *sr = array_get(marks, idx);
1031 if (sr)
1032 pos = text_mark_get(file->text, sr->cursor);
1033 ret = text_range_new(pos, pos);
1034 break;
1036 case '?':
1037 sign = sign == 0 ? -1 : -sign;
1038 /* fall through */
1039 case '/':
1040 if (sign >= 0)
1041 ret = text_object_search_forward(file->text, range->end, addr->regex);
1042 else
1043 ret = text_object_search_backward(file->text, range->start, addr->regex);
1044 break;
1045 case '$':
1047 size_t size = text_size(file->text);
1048 ret = text_range_new(size, size);
1049 break;
1051 case '.':
1052 ret = *range;
1053 break;
1054 case '+':
1055 case '-':
1056 sign = addr->type == '+' ? +1 : -1;
1057 if (!addr->right || addr->right->type == '+' || addr->right->type == '-')
1058 ret = address_line_evaluate(addr, file, range, sign);
1059 break;
1060 case ',':
1061 case ';':
1063 Filerange left, right;
1064 if (addr->left)
1065 left = address_evaluate(addr->left, file, sel, range, 0);
1066 else
1067 left = text_range_new(0, 0);
1069 if (addr->type == ';')
1070 range = &left;
1072 if (addr->right) {
1073 right = address_evaluate(addr->right, file, sel, range, 0);
1074 } else {
1075 size_t size = text_size(file->text);
1076 right = text_range_new(size, size);
1078 /* TODO: enforce strict ordering? */
1079 return text_range_union(&left, &right);
1081 case '%':
1082 return text_range_new(0, text_size(file->text));
1084 if (text_range_valid(&ret))
1085 range = &ret;
1086 } while ((addr = addr->right));
1088 return ret;
1091 static bool count_evaluate(Command *cmd) {
1092 Count *count = &cmd->count;
1093 if (count->mod)
1094 return count->start ? cmd->iteration % count->start == 0 : true;
1095 return count->start <= cmd->iteration && cmd->iteration <= count->end;
1098 static bool sam_execute(Vis *vis, Win *win, Command *cmd, Selection *sel, Filerange *range) {
1099 bool ret = true;
1100 if (cmd->address && win)
1101 *range = address_evaluate(cmd->address, win->file, sel, range, 0);
1103 cmd->iteration++;
1104 switch (cmd->argv[0][0]) {
1105 case '{':
1107 for (Command *c = cmd->cmd; c && ret; c = c->next)
1108 ret &= sam_execute(vis, win, c, NULL, range);
1109 view_selections_dispose_force(sel);
1110 break;
1112 default:
1113 ret = cmd->cmddef->func(vis, win, cmd, cmd->argv, sel, range);
1114 break;
1116 return ret;
1119 static enum SamError validate(Command *cmd, bool loop, bool group) {
1120 if (cmd->cmddef->flags & CMD_DESTRUCTIVE) {
1121 if (loop)
1122 return SAM_ERR_LOOP_INVALID_CMD;
1123 if (group)
1124 return SAM_ERR_GROUP_INVALID_CMD;
1127 group |= (cmd->cmddef->flags & CMD_GROUP);
1128 loop |= (cmd->cmddef->flags & CMD_LOOP);
1129 for (Command *c = cmd->cmd; c; c = c->next) {
1130 enum SamError err = validate(c, loop, group);
1131 if (err != SAM_ERR_OK)
1132 return err;
1134 return SAM_ERR_OK;
1137 static enum SamError command_validate(Command *cmd) {
1138 return validate(cmd, false, false);
1141 static bool count_negative(Command *cmd) {
1142 if (cmd->count.start < 0 || cmd->count.end < 0)
1143 return true;
1144 for (Command *c = cmd->cmd; c; c = c->next) {
1145 if (c->cmddef->func != cmd_extract && c->cmddef->func != cmd_select) {
1146 if (count_negative(c))
1147 return true;
1150 return false;
1153 static void count_init(Command *cmd, int max) {
1154 Count *count = &cmd->count;
1155 cmd->iteration = 0;
1156 if (count->start < 0)
1157 count->start += max;
1158 if (count->end < 0)
1159 count->end += max;
1160 for (Command *c = cmd->cmd; c; c = c->next) {
1161 if (c->cmddef->func != cmd_extract && c->cmddef->func != cmd_select)
1162 count_init(c, max);
1166 enum SamError sam_cmd(Vis *vis, const char *s) {
1167 enum SamError err = SAM_ERR_OK;
1168 if (!s)
1169 return err;
1171 Command *cmd = sam_parse(vis, s, &err);
1172 if (!cmd) {
1173 if (err == SAM_ERR_OK)
1174 err = SAM_ERR_MEMORY;
1175 return err;
1178 err = command_validate(cmd);
1179 if (err != SAM_ERR_OK) {
1180 command_free(cmd);
1181 return err;
1184 for (File *file = vis->files; file; file = file->next) {
1185 if (file->internal)
1186 continue;
1187 sam_transcript_init(&file->transcript);
1190 bool visual = vis->mode->visual;
1191 size_t primary_pos = vis->win ? view_cursor_get(vis->win->view) : EPOS;
1192 Filerange range = text_range_empty();
1193 sam_execute(vis, vis->win, cmd, NULL, &range);
1195 for (File *file = vis->files; file; file = file->next) {
1196 if (file->internal)
1197 continue;
1198 Transcript *t = &file->transcript;
1199 if (t->error != SAM_ERR_OK) {
1200 err = t->error;
1201 sam_transcript_free(t);
1202 continue;
1204 vis_file_snapshot(vis, file);
1205 ptrdiff_t delta = 0;
1206 for (Change *c = t->changes; c; c = c->next) {
1207 c->range.start += delta;
1208 c->range.end += delta;
1209 if (c->type & TRANSCRIPT_DELETE) {
1210 text_delete_range(file->text, &c->range);
1211 delta -= text_range_size(&c->range);
1212 if (c->cursor && c->type == TRANSCRIPT_DELETE) {
1213 if (visual)
1214 view_selections_dispose_force(c->cursor);
1215 else
1216 view_cursors_to(c->cursor, c->range.start);
1219 if (c->type & TRANSCRIPT_INSERT) {
1220 text_insert(file->text, c->range.start, c->data, c->len);
1221 delta += c->len;
1222 Filerange sel = text_range_new(c->range.start,
1223 c->range.start+c->len);
1224 if (c->cursor) {
1225 if (visual) {
1226 view_selections_set(c->cursor, &sel);
1227 view_selections_anchor(c->cursor);
1228 } else {
1229 if (memchr(c->data, '\n', c->len))
1230 view_cursors_to(c->cursor, sel.start);
1231 else
1232 view_cursors_to(c->cursor, sel.end);
1234 } else if (visual) {
1235 Selection *cursor = view_selections_new(c->win->view, sel.start);
1236 if (cursor) {
1237 view_selections_set(cursor, &sel);
1238 view_selections_anchor(cursor);
1243 sam_transcript_free(&file->transcript);
1244 vis_file_snapshot(vis, file);
1247 if (vis->win) {
1248 if (primary_pos != EPOS && view_selection_disposed(vis->win->view))
1249 view_cursor_to(vis->win->view, primary_pos);
1250 view_selections_primary_set(view_selections(vis->win->view));
1251 vis_jumplist_save(vis);
1252 bool completed = true;
1253 for (Selection *s = view_selections(vis->win->view); s; s = view_selections_next(s)) {
1254 if (view_selections_anchored(s)) {
1255 completed = false;
1256 break;
1259 vis_mode_switch(vis, completed ? VIS_MODE_NORMAL : VIS_MODE_VISUAL);
1261 command_free(cmd);
1262 return err;
1265 /* process text input, substitute register content for backreferences etc. */
1266 Buffer text(Vis *vis, const char *text) {
1267 Buffer buf;
1268 buffer_init(&buf);
1269 for (size_t len = strcspn(text, "\\&"); *text; len = strcspn(++text, "\\&")) {
1270 buffer_append(&buf, text, len);
1271 text += len;
1272 enum VisRegister regid = VIS_REG_INVALID;
1273 switch (text[0]) {
1274 case '&':
1275 regid = VIS_REG_AMPERSAND;
1276 break;
1277 case '\\':
1278 if ('1' <= text[1] && text[1] <= '9') {
1279 regid = VIS_REG_1 + text[1] - '1';
1280 text++;
1281 } else if (text[1] == '\\' || text[1] == '&') {
1282 text++;
1284 break;
1285 case '\0':
1286 goto out;
1289 const char *data;
1290 size_t reglen = 0;
1291 if (regid != VIS_REG_INVALID) {
1292 data = register_get(vis, &vis->registers[regid], &reglen);
1293 } else {
1294 data = text;
1295 reglen = 1;
1297 buffer_append(&buf, data, reglen);
1299 out:
1300 return buf;
1303 static bool cmd_insert(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1304 if (!win)
1305 return false;
1306 Buffer buf = text(vis, argv[1]);
1307 size_t len = buffer_length(&buf);
1308 char *data = buffer_move(&buf);
1309 bool ret = sam_insert(win, sel, range->start, data, len);
1310 if (!ret)
1311 free(data);
1312 return ret;
1315 static bool cmd_append(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1316 if (!win)
1317 return false;
1318 Buffer buf = text(vis, argv[1]);
1319 size_t len = buffer_length(&buf);
1320 char *data = buffer_move(&buf);
1321 bool ret = sam_insert(win, sel, range->end, data, len);
1322 if (!ret)
1323 free(data);
1324 return ret;
1327 static bool cmd_change(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1328 if (!win)
1329 return false;
1330 Buffer buf = text(vis, argv[1]);
1331 size_t len = buffer_length(&buf);
1332 char *data = buffer_move(&buf);
1333 bool ret = sam_change(win, sel, range, data, len);
1334 if (!ret)
1335 free(data);
1336 return ret;
1339 static bool cmd_delete(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1340 return win && sam_delete(win, sel, range);
1343 static bool cmd_guard(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1344 if (!win)
1345 return false;
1346 bool match = !cmd->regex || !text_search_range_forward(win->file->text, range->start,
1347 text_range_size(range), cmd->regex, 0, NULL, 0);
1348 if ((count_evaluate(cmd) && match) ^ (argv[0][0] == 'v'))
1349 return sam_execute(vis, win, cmd->cmd, sel, range);
1350 view_selections_dispose_force(sel);
1351 return true;
1354 static int extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range, bool simulate) {
1355 bool ret = true;
1356 int count = 0;
1357 Text *txt = win->file->text;
1359 if (cmd->regex) {
1360 bool trailing_match = false;
1361 size_t start = range->start, end = range->end, last_start = EPOS;
1362 size_t nsub = 1 + text_regex_nsub(cmd->regex);
1363 if (nsub > 10)
1364 nsub = 10;
1365 RegexMatch match[nsub];
1366 while (start < end || trailing_match) {
1367 trailing_match = false;
1368 bool found = text_search_range_forward(txt, start,
1369 end - start, cmd->regex, nsub, match,
1370 start > range->start ? REG_NOTBOL : 0) == 0;
1371 Filerange r = text_range_empty();
1372 if (found) {
1373 if (argv[0][0] == 'x')
1374 r = text_range_new(match[0].start, match[0].end);
1375 else
1376 r = text_range_new(start, match[0].start);
1377 if (match[0].start == match[0].end) {
1378 if (last_start == match[0].start) {
1379 start++;
1380 continue;
1382 /* in Plan 9's regexp library ^ matches the beginning
1383 * of a line, however in POSIX with REG_NEWLINE ^
1384 * matches the zero-length string immediately after a
1385 * newline. Try filtering out the last such match at EOF.
1387 if (end == match[0].start && start > range->start)
1388 break;
1390 start = match[0].end;
1391 if (start == end)
1392 trailing_match = true;
1393 } else {
1394 if (argv[0][0] == 'y')
1395 r = text_range_new(start, end);
1396 start = end;
1399 if (text_range_valid(&r)) {
1400 if (found) {
1401 for (size_t i = 0; i < nsub; i++) {
1402 Register *reg = &vis->registers[VIS_REG_AMPERSAND+i];
1403 register_put_range(vis, reg, txt, &match[i]);
1406 if (simulate)
1407 count++;
1408 else
1409 ret &= sam_execute(vis, win, cmd->cmd, NULL, &r);
1410 last_start = start;
1413 } else {
1414 size_t start = range->start, end = range->end;
1415 while (start < end) {
1416 size_t next = text_line_next(txt, start);
1417 if (next > end)
1418 next = end;
1419 Filerange r = text_range_new(start, next);
1420 if (start == next || !text_range_valid(&r))
1421 break;
1422 if (simulate)
1423 count++;
1424 else
1425 ret &= sam_execute(vis, win, cmd->cmd, NULL, &r);
1426 start = next;
1430 if (!simulate)
1431 view_selections_dispose_force(sel);
1432 return simulate ? count : ret;
1435 static bool cmd_extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1436 if (!win)
1437 return false;
1438 int matches = 0;
1439 if (count_negative(cmd->cmd))
1440 matches = extract(vis, win, cmd, argv, sel, range, true);
1441 count_init(cmd->cmd, matches+1);
1442 return extract(vis, win, cmd, argv, sel, range, false);
1445 static bool cmd_select(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1446 Filerange r = text_range_empty();
1447 if (!win)
1448 return sam_execute(vis, NULL, cmd->cmd, NULL, &r);
1449 bool ret = true;
1450 View *view = win->view;
1451 Text *txt = win->file->text;
1452 bool multiple_cursors = view_selections_count(view) > 1;
1453 Selection *primary = view_selections_primary_get(view);
1455 if (vis->mode->visual)
1456 count_init(cmd->cmd, view_selections_count(view)+1);
1458 for (Selection *s = view_selections(view), *next; s && ret; s = next) {
1459 next = view_selections_next(s);
1460 size_t pos = view_cursors_pos(s);
1461 if (vis->mode->visual) {
1462 r = view_selections_get(s);
1463 } else if (cmd->cmd->address) {
1464 /* convert a single line range to a goto line motion */
1465 if (!multiple_cursors && cmd->cmd->cmddef->func == cmd_print) {
1466 Address *addr = cmd->cmd->address;
1467 switch (addr->type) {
1468 case '+':
1469 case '-':
1470 addr = addr->right;
1471 /* fall through */
1472 case 'l':
1473 if (addr && addr->type == 'l' && !addr->right)
1474 addr->type = 'g';
1475 break;
1478 r = text_range_new(pos, pos);
1479 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_POS) {
1480 r = text_range_new(pos, pos);
1481 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_LINE) {
1482 r = text_object_line(txt, pos);
1483 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_AFTER) {
1484 size_t next_line = text_line_next(txt, pos);
1485 r = text_range_new(next_line, next_line);
1486 } else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_ALL) {
1487 r = text_range_new(0, text_size(txt));
1488 } else if (!multiple_cursors && (cmd->cmd->cmddef->flags & CMD_ADDRESS_ALL_1CURSOR)) {
1489 r = text_range_new(0, text_size(txt));
1490 } else {
1491 r = text_range_new(pos, text_char_next(txt, pos));
1493 if (!text_range_valid(&r))
1494 r = text_range_new(0, 0);
1495 ret &= sam_execute(vis, win, cmd->cmd, s, &r);
1496 if (cmd->cmd->cmddef->flags & CMD_ONCE)
1497 break;
1500 if (vis->win && vis->win->view == view && primary != view_selections_primary_get(view))
1501 view_selections_primary_set(view_selections(view));
1502 return ret;
1505 static bool cmd_print(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1506 if (!win || !text_range_valid(range))
1507 return false;
1508 View *view = win->view;
1509 if (!sel)
1510 sel = view_selections_new_force(view, range->start);
1511 if (!sel)
1512 return false;
1513 if (range->start != range->end) {
1514 view_selections_set(sel, range);
1515 view_selections_anchor(sel);
1516 } else {
1517 view_cursors_to(sel, range->start);
1518 view_selection_clear(sel);
1520 return true;
1523 static bool cmd_files(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1524 bool ret = true;
1525 for (Win *win = vis->windows; win; win = win->next) {
1526 if (win->file->internal)
1527 continue;
1528 bool match = !cmd->regex || (win->file->name &&
1529 text_regex_match(cmd->regex, win->file->name, 0));
1530 if (match ^ (argv[0][0] == 'Y')) {
1531 Filerange def = text_range_new(0, 0);
1532 ret &= sam_execute(vis, win, cmd->cmd, NULL, &def);
1535 return ret;
1538 static bool cmd_substitute(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1539 vis_info_show(vis, "Use :x/pattern/ c/replacement/ instead");
1540 return false;
1543 static bool cmd_write(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *r) {
1544 if (!win)
1545 return false;
1547 File *file = win->file;
1548 if (sam_transcript_error(&file->transcript, SAM_ERR_WRITE_CONFLICT))
1549 return false;
1551 Text *text = file->text;
1552 const char *filename = argv[1];
1553 if (!filename)
1554 filename = file->name;
1555 if (!filename) {
1556 if (file->fd == -1) {
1557 vis_info_show(vis, "Filename expected");
1558 return false;
1560 if (!strchr(argv[0], 'q')) {
1561 vis_info_show(vis, "No filename given, use 'wq' to write to stdout");
1562 return false;
1565 if (!vis_event_emit(vis, VIS_EVENT_FILE_SAVE_PRE, file, (char*)NULL) && cmd->flags != '!') {
1566 vis_info_show(vis, "Rejected write to stdout by pre-save hook");
1567 return false;
1570 bool visual = vis->mode->visual;
1572 for (Selection *s = view_selections(win->view); s; s = view_selections_next(s)) {
1573 Filerange range = visual ? view_selections_get(s) : *r;
1574 ssize_t written = text_write_range(text, &range, file->fd);
1575 if (written == -1 || (size_t)written != text_range_size(&range)) {
1576 vis_info_show(vis, "Can not write to stdout");
1577 return false;
1579 if (!visual)
1580 break;
1583 /* make sure the file is marked as saved i.e. not modified */
1584 text_save(text, NULL);
1585 vis_event_emit(vis, VIS_EVENT_FILE_SAVE_POST, file, (char*)NULL);
1586 return true;
1589 if (!argv[1] && cmd->flags != '!') {
1590 if (vis->mode->visual) {
1591 vis_info_show(vis, "WARNING: file will be reduced to active selection");
1592 return false;
1594 Filerange all = text_range_new(0, text_size(text));
1595 if (!text_range_equal(r, &all)) {
1596 vis_info_show(vis, "WARNING: file will be reduced to provided range");
1597 return false;
1601 for (const char **name = argv[1] ? &argv[1] : (const char*[]){ filename, NULL }; *name; name++) {
1602 struct stat meta;
1603 if (cmd->flags != '!' && file->stat.st_mtime && stat(*name, &meta) == 0 &&
1604 file->stat.st_mtime < meta.st_mtime) {
1605 vis_info_show(vis, "WARNING: file has been changed since reading it");
1606 return false;
1609 if (!vis_event_emit(vis, VIS_EVENT_FILE_SAVE_PRE, file, *name) && cmd->flags != '!') {
1610 vis_info_show(vis, "Rejected write to `%s' by pre-save hook", *name);
1611 return false;
1614 TextSave *ctx = text_save_begin(text, *name, file->save_method);
1615 if (!ctx) {
1616 const char *msg = errno ? strerror(errno) : "try changing `:set savemethod`";
1617 vis_info_show(vis, "Can't write `%s': %s", *name, msg);
1618 return false;
1621 bool failure = false;
1622 bool visual = vis->mode->visual;
1624 for (Selection *s = view_selections(win->view); s; s = view_selections_next(s)) {
1625 Filerange range = visual ? view_selections_get(s) : *r;
1626 ssize_t written = text_save_write_range(ctx, &range);
1627 failure = (written == -1 || (size_t)written != text_range_size(&range));
1628 if (failure) {
1629 text_save_cancel(ctx);
1630 break;
1633 if (!visual)
1634 break;
1637 if (failure || !text_save_commit(ctx)) {
1638 vis_info_show(vis, "Can't write `%s': %s", *name, strerror(errno));
1639 return false;
1642 if (!file->name)
1643 file_name_set(file, *name);
1644 if (strcmp(file->name, *name) == 0)
1645 file->stat = text_stat(text);
1646 vis_event_emit(vis, VIS_EVENT_FILE_SAVE_POST, file, *name);
1648 return true;
1651 static ssize_t read_buffer(void *context, char *data, size_t len) {
1652 buffer_append(context, data, len);
1653 return len;
1656 static bool cmd_filter(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1657 if (!win)
1658 return false;
1660 Buffer bufout, buferr;
1661 buffer_init(&bufout);
1662 buffer_init(&buferr);
1664 int status = vis_pipe(vis, win->file, range, &argv[1], &bufout, read_buffer, &buferr, read_buffer);
1666 if (vis->interrupted) {
1667 vis_info_show(vis, "Command cancelled");
1668 } else if (status == 0) {
1669 size_t len = buffer_length(&bufout);
1670 char *data = buffer_move(&bufout);
1671 if (!sam_change(win, sel, range, data, len))
1672 free(data);
1673 } else {
1674 vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
1677 buffer_release(&bufout);
1678 buffer_release(&buferr);
1680 return !vis->interrupted && status == 0;
1683 static bool cmd_launch(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1684 Filerange invalid = text_range_new(sel ? view_cursors_pos(sel) : range->start, EPOS);
1685 return cmd_filter(vis, win, cmd, argv, sel, &invalid);
1688 static bool cmd_pipein(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1689 if (!win)
1690 return false;
1691 Filerange filter_range = text_range_new(range->end, range->end);
1692 bool ret = cmd_filter(vis, win, cmd, argv, sel, &filter_range);
1693 if (ret)
1694 ret = sam_delete(win, NULL, range);
1695 return ret;
1698 static bool cmd_pipeout(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1699 if (!win)
1700 return false;
1701 Buffer buferr;
1702 buffer_init(&buferr);
1704 int status = vis_pipe(vis, win->file, range, (const char*[]){ argv[1], NULL }, NULL, NULL, &buferr, read_buffer);
1706 if (status == 0 && sel)
1707 view_cursors_to(sel, range->start);
1709 if (vis->interrupted)
1710 vis_info_show(vis, "Command cancelled");
1711 else if (status != 0)
1712 vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
1714 buffer_release(&buferr);
1716 return !vis->interrupted && status == 0;
1719 static bool cmd_cd(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
1720 const char *dir = argv[1];
1721 if (!dir)
1722 dir = getenv("HOME");
1723 return dir && chdir(dir) == 0;
1726 #include "vis-cmds.c"