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