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