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.
31 #include "text-motions.h"
32 #include "text-objects.h"
33 #include "text-regex.h"
38 typedef struct Address Address
;
39 typedef struct Command Command
;
40 typedef struct CommandDef CommandDef
;
44 TRANSCRIPT_INSERT
= 1 << 0,
45 TRANSCRIPT_DELETE
= 1 << 1,
46 TRANSCRIPT_CHANGE
= TRANSCRIPT_INSERT
|TRANSCRIPT_DELETE
,
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? */
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 */
66 int start
, end
; /* interval [n,m] */
67 bool mod
; /* % every n-th match, implies n == m */
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 */
83 const char *name
; /* command name */
84 VIS_HELP_DECL(const char *help
;) /* short, one-line help text */
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 */
106 const char *defcmd
; /* name of a default target command */
107 bool (*func
)(Vis
*, Win
*, Command
*, const char *argv
[], Selection
*, Filerange
*); /* command implementation */
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
*);
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
[] = {
148 // flags, default command, implementation
150 "a", VIS_HELP("Append text after range")
151 CMD_TEXT
, NULL
, cmd_append
153 "c", VIS_HELP("Change text in range")
154 CMD_TEXT
, NULL
, cmd_change
156 "d", VIS_HELP("Delete text in range")
157 CMD_NONE
, NULL
, cmd_delete
159 "g", VIS_HELP("If range contains regexp, run command")
160 CMD_COUNT
|CMD_REGEX
|CMD_CMD
, "p", cmd_guard
162 "i", VIS_HELP("Insert text before range")
163 CMD_TEXT
, NULL
, cmd_insert
165 "p", VIS_HELP("Create selection covering range")
166 CMD_NONE
, NULL
, cmd_print
168 "s", VIS_HELP("Substitute: use x/pattern/ c/replacement/ instead")
169 CMD_SHELL
|CMD_ADDRESS_LINE
, NULL
, cmd_substitute
171 "v", VIS_HELP("If range does not contain regexp, run command")
172 CMD_COUNT
|CMD_REGEX
|CMD_CMD
, "p", cmd_guard
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
177 "y", VIS_HELP("As `x` but select unmatched text")
178 CMD_CMD
|CMD_REGEX
|CMD_ADDRESS_ALL_1CURSOR
|CMD_LOOP
, "p", cmd_extract
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
183 "Y", VIS_HELP("As `X` but select unmatched files")
184 CMD_CMD
|CMD_REGEX
|CMD_ADDRESS_NONE
|CMD_ONCE
, NULL
, cmd_files
186 ">", VIS_HELP("Send range to stdin of command")
187 CMD_SHELL
|CMD_ADDRESS_LINE
, NULL
, cmd_pipeout
189 "<", VIS_HELP("Replace range by stdout of command")
190 CMD_SHELL
|CMD_ADDRESS_POS
, NULL
, cmd_pipein
192 "|", VIS_HELP("Pipe range through command")
193 CMD_SHELL
, NULL
, cmd_filter
195 "!", VIS_HELP("Run the command")
196 CMD_SHELL
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_launch
198 "w", VIS_HELP("Write range to named file")
199 CMD_ARGV
|CMD_FORCE
|CMD_ONCE
|CMD_ADDRESS_ALL
, NULL
, cmd_write
201 "r", VIS_HELP("Replace range by contents of file")
202 CMD_ARGV
|CMD_ADDRESS_AFTER
, NULL
, cmd_read
204 "{", VIS_HELP("Start of command group")
205 CMD_GROUP
, NULL
, NULL
207 "}", VIS_HELP("End of command group" )
210 "e", VIS_HELP("Edit file")
211 CMD_ARGV
|CMD_FORCE
|CMD_ONCE
|CMD_ADDRESS_NONE
|CMD_DESTRUCTIVE
, NULL
, cmd_edit
213 "q", VIS_HELP("Quit the current window")
214 CMD_ARGV
|CMD_FORCE
|CMD_ONCE
|CMD_ADDRESS_NONE
|CMD_DESTRUCTIVE
, NULL
, cmd_quit
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
224 "map", VIS_HELP("Map key binding `:map <mode> <lhs> <rhs>`")
225 CMD_ARGV
|CMD_FORCE
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_map
227 "map-window", VIS_HELP("As `map` but window local")
228 CMD_ARGV
|CMD_FORCE
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_map
230 "unmap", VIS_HELP("Unmap key binding `:unmap <mode> <lhs>`")
231 CMD_ARGV
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_unmap
233 "unmap-window", VIS_HELP("As `unmap` but window local")
234 CMD_ARGV
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_unmap
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
239 "new", VIS_HELP("Create new window")
240 CMD_ARGV
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_new
242 "open", VIS_HELP("Open file")
243 CMD_ARGV
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_open
245 "qall", VIS_HELP("Exit vis")
246 CMD_ARGV
|CMD_FORCE
|CMD_ONCE
|CMD_ADDRESS_NONE
|CMD_DESTRUCTIVE
, NULL
, cmd_qall
248 "set", VIS_HELP("Set option")
249 CMD_ARGV
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_set
251 "split", VIS_HELP("Horizontally split window")
252 CMD_ARGV
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_split
254 "vnew", VIS_HELP("As `:new` but split vertically")
255 CMD_ARGV
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_vnew
257 "vsplit", VIS_HELP("Vertically split window")
258 CMD_ARGV
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_vsplit
260 "wq", VIS_HELP("Write file and quit")
261 CMD_ARGV
|CMD_FORCE
|CMD_ONCE
|CMD_ADDRESS_ALL
|CMD_DESTRUCTIVE
, NULL
, cmd_wq
263 "earlier", VIS_HELP("Go to older text state")
264 CMD_ARGV
|CMD_ONCE
|CMD_ADDRESS_NONE
, NULL
, cmd_earlier_later
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 */
278 const char *names
[3]; /* name and optional alias */
279 enum VisOption flags
; /* option type, etc. */
280 VIS_HELP_DECL(const char *help
;) /* short, one line help text */
281 VisOptionFunction
*func
; /* option handler, NULL for builtins */
282 void *context
; /* context passed to option handler function */
293 OPTION_SHOW_NEWLINES
,
297 OPTION_NUMBER_RELATIVE
,
302 OPTION_CHANGE_256COLORS
,
310 static const OptionDef options
[] = {
313 VIS_OPTION_TYPE_STRING
,
314 VIS_HELP("Shell to use for external commands (default: $SHELL, /etc/passwd, /bin/sh)")
316 [OPTION_ESCDELAY
] = {
318 VIS_OPTION_TYPE_NUMBER
,
319 VIS_HELP("Milliseconds to wait to distinguish <Escape> from terminal escape sequences")
321 [OPTION_AUTOINDENT
] = {
322 { "autoindent", "ai" },
323 VIS_OPTION_TYPE_BOOL
,
324 VIS_HELP("Copy leading white space from previous line")
326 [OPTION_EXPANDTAB
] = {
327 { "expandtab", "et" },
328 VIS_OPTION_TYPE_BOOL
|VIS_OPTION_NEED_WINDOW
,
329 VIS_HELP("Replace entered <Tab> with `tabwidth` spaces")
331 [OPTION_TABWIDTH
] = {
332 { "tabwidth", "tw" },
333 VIS_OPTION_TYPE_NUMBER
|VIS_OPTION_NEED_WINDOW
,
334 VIS_HELP("Number of spaces to display (and insert if `expandtab` is enabled) for a tab")
336 [OPTION_SHOW_SPACES
] = {
337 { "showspaces", "show-spaces" },
338 VIS_OPTION_TYPE_BOOL
|VIS_OPTION_NEED_WINDOW
|VIS_OPTION_DEPRECATED
,
339 VIS_HELP("Display replacement symbol instead of a space")
343 [OPTION_SHOW_TABS
] = {
344 { "showtabs", "show-tabs" },
345 VIS_OPTION_TYPE_BOOL
|VIS_OPTION_NEED_WINDOW
|VIS_OPTION_DEPRECATED
,
346 VIS_HELP("Display replacement symbol for tabs")
350 [OPTION_SHOW_NEWLINES
] = {
351 { "shownewlines", "show-newlines" },
352 VIS_OPTION_TYPE_BOOL
|VIS_OPTION_NEED_WINDOW
|VIS_OPTION_DEPRECATED
,
353 VIS_HELP("Display replacement symbol for newlines")
357 [OPTION_SHOW_EOF
] = {
358 { "showeof", "show-eof" },
359 VIS_OPTION_TYPE_BOOL
|VIS_OPTION_NEED_WINDOW
|VIS_OPTION_DEPRECATED
,
360 VIS_HELP("Display replacement symbol for lines after the end of the file")
364 [OPTION_STATUSBAR
] = {
365 { "statusbar", "sb" },
366 VIS_OPTION_TYPE_BOOL
|VIS_OPTION_NEED_WINDOW
,
367 VIS_HELP("Display status bar")
371 VIS_OPTION_TYPE_BOOL
|VIS_OPTION_NEED_WINDOW
,
372 VIS_HELP("Display absolute line numbers")
374 [OPTION_NUMBER_RELATIVE
] = {
375 { "relativenumbers", "rnu" },
376 VIS_OPTION_TYPE_BOOL
|VIS_OPTION_NEED_WINDOW
,
377 VIS_HELP("Display relative line numbers")
379 [OPTION_CURSOR_LINE
] = {
380 { "cursorline", "cul" },
381 VIS_OPTION_TYPE_BOOL
|VIS_OPTION_NEED_WINDOW
,
382 VIS_HELP("Highlight current cursor line")
384 [OPTION_COLOR_COLUMN
] = {
385 { "colorcolumn", "cc" },
386 VIS_OPTION_TYPE_NUMBER
|VIS_OPTION_NEED_WINDOW
,
387 VIS_HELP("Highlight a fixed column")
389 [OPTION_SAVE_METHOD
] = {
391 VIS_OPTION_TYPE_STRING
|VIS_OPTION_NEED_WINDOW
,
392 VIS_HELP("Save method to use for current file 'auto', 'atomic' or 'inplace'")
394 [OPTION_LOAD_METHOD
] = {
396 VIS_OPTION_TYPE_STRING
,
397 VIS_HELP("How to load existing files 'auto', 'read' or 'mmap'")
399 [OPTION_CHANGE_256COLORS
] = {
400 { "change256colors", "change-256colors" },
401 VIS_OPTION_TYPE_BOOL
|VIS_OPTION_DEPRECATED
,
402 VIS_HELP("Change 256 color palette to support 24bit colors")
408 VIS_OPTION_TYPE_STRING
,
409 VIS_HELP("Vertical or horizontal window layout")
411 [OPTION_SMARTCASE
] = {
412 { "smartcase", "scs" },
413 VIS_OPTION_TYPE_BOOL
,
414 VIS_HELP("Case-insensitive search, unless the pattern contains upper case characters")
418 VIS_OPTION_TYPE_BOOL
,
419 VIS_HELP("Literal string search")
422 { "breakat", "brk" },
423 VIS_OPTION_TYPE_STRING
|VIS_OPTION_NEED_WINDOW
,
424 VIS_HELP("Characters which might cause a word wrap")
426 [OPTION_WRAP_COLUMN
] = {
427 { "wrapcolumn", "wc" },
428 VIS_OPTION_TYPE_NUMBER
|VIS_OPTION_NEED_WINDOW
,
429 VIS_HELP("Wrap lines at minimum of window width and wrapcolumn")
433 bool sam_init(Vis
*vis
) {
434 if (!(vis
->cmds
= map_new()))
437 for (const CommandDef
*cmd
= cmds
; cmd
&& cmd
->name
; cmd
++)
438 ret
&= map_put(vis
->cmds
, cmd
->name
, cmd
);
440 if (!(vis
->options
= map_new()))
442 for (int i
= 0; i
< LENGTH(options
); i
++) {
443 for (const char *const *name
= options
[i
].names
; *name
; name
++)
444 ret
&= map_put(vis
->options
, *name
, &options
[i
]);
450 const char *sam_error(enum SamError err
) {
451 static const char *error_msg
[] = {
452 [SAM_ERR_OK
] = "Success",
453 [SAM_ERR_MEMORY
] = "Out of memory",
454 [SAM_ERR_ADDRESS
] = "Bad address",
455 [SAM_ERR_NO_ADDRESS
] = "Command takes no address",
456 [SAM_ERR_UNMATCHED_BRACE
] = "Unmatched `}'",
457 [SAM_ERR_REGEX
] = "Bad regular expression",
458 [SAM_ERR_TEXT
] = "Bad text",
459 [SAM_ERR_SHELL
] = "Shell command expected",
460 [SAM_ERR_COMMAND
] = "Unknown command",
461 [SAM_ERR_EXECUTE
] = "Error executing command",
462 [SAM_ERR_NEWLINE
] = "Newline expected",
463 [SAM_ERR_MARK
] = "Invalid mark",
464 [SAM_ERR_CONFLICT
] = "Conflicting changes",
465 [SAM_ERR_WRITE_CONFLICT
] = "Can not write while changing",
466 [SAM_ERR_LOOP_INVALID_CMD
] = "Destructive command in looping construct",
467 [SAM_ERR_GROUP_INVALID_CMD
] = "Destructive command in group",
468 [SAM_ERR_COUNT
] = "Invalid count",
472 return idx
< LENGTH(error_msg
) ? error_msg
[idx
] : NULL
;
475 static void change_free(Change
*c
) {
478 free((char*)c
->data
);
482 static Change
*change_new(Transcript
*t
, enum ChangeType type
, Filerange
*range
, Win
*win
, Selection
*sel
) {
483 if (!text_range_valid(range
))
485 Change
**prev
, *next
;
486 if (t
->latest
&& t
->latest
->range
.end
<= range
->start
) {
487 prev
= &t
->latest
->next
;
488 next
= t
->latest
->next
;
493 while (next
&& next
->range
.end
<= range
->start
) {
497 if (next
&& next
->range
.start
< range
->end
) {
498 t
->error
= SAM_ERR_CONFLICT
;
501 Change
*new = calloc(1, sizeof *new);
514 static void sam_transcript_init(Transcript
*t
) {
515 memset(t
, 0, sizeof *t
);
518 static bool sam_transcript_error(Transcript
*t
, enum SamError error
) {
524 static void sam_transcript_free(Transcript
*t
) {
525 for (Change
*c
= t
->changes
, *next
; c
; c
= next
) {
531 static bool sam_insert(Win
*win
, Selection
*sel
, size_t pos
, const char *data
, size_t len
, int count
) {
532 Filerange range
= text_range_new(pos
, pos
);
533 Change
*c
= change_new(&win
->file
->transcript
, TRANSCRIPT_INSERT
, &range
, win
, sel
);
542 static bool sam_delete(Win
*win
, Selection
*sel
, Filerange
*range
) {
543 return change_new(&win
->file
->transcript
, TRANSCRIPT_DELETE
, range
, win
, sel
);
546 static bool sam_change(Win
*win
, Selection
*sel
, Filerange
*range
, const char *data
, size_t len
, int count
) {
547 Change
*c
= change_new(&win
->file
->transcript
, TRANSCRIPT_CHANGE
, range
, win
, sel
);
556 static Address
*address_new(void) {
557 Address
*addr
= calloc(1, sizeof *addr
);
563 static void address_free(Address
*addr
) {
566 address_free(addr
->left
);
567 address_free(addr
->right
);
571 static void skip_spaces(const char **s
) {
572 while (**s
== ' ' || **s
== '\t')
576 static char *parse_until(const char **s
, const char *until
, const char *escchars
, int type
){
579 size_t len
= strlen(until
);
580 bool escaped
= false;
582 for (; **s
&& (!memchr(until
, **s
, len
) || escaped
); (*s
)++) {
583 if (type
!= CMD_SHELL
&& !escaped
&& **s
== '\\') {
596 } else if (c
== 't') {
598 } else if (type
!= CMD_REGEX
&& type
!= CMD_TEXT
&& c
== '\\') {
599 // ignore one of the back slashes
601 bool delim
= memchr(until
, c
, len
);
602 bool esc
= escchars
&& memchr(escchars
, c
, strlen(escchars
));
604 buffer_append(&buf
, "\\", 1);
608 if (!buffer_append(&buf
, &c
, 1)) {
609 buffer_release(&buf
);
614 buffer_terminate(&buf
);
616 return buffer_move(&buf
);
619 static char *parse_delimited(const char **s
, int type
) {
620 char delim
[2] = { **s
, '\0' };
621 if (!delim
[0] || isspace((unsigned char)delim
[0]))
624 char *chunk
= parse_until(s
, delim
, NULL
, type
);
630 static int parse_number(const char **s
) {
632 int number
= strtoull(*s
, &end
, 10);
639 static char *parse_text(const char **s
, Count
*count
) {
641 const char *before
= *s
;
642 count
->start
= parse_number(s
);
647 char *text
= parse_delimited(s
, CMD_TEXT
);
648 return (!text
&& *s
!= before
) ? strdup("") : text
;
653 const char *start
= *s
+ 1;
656 for ((*s
)++; **s
&& (!dot
|| **s
!= '\n'); (*s
)++)
659 if (!dot
|| !buffer_put(&buf
, start
, *s
- start
- 1) ||
660 !buffer_append(&buf
, "\0", 1)) {
661 buffer_release(&buf
);
665 return buffer_move(&buf
);
668 static char *parse_shellcmd(Vis
*vis
, const char **s
) {
670 char *cmd
= parse_until(s
, "\n", NULL
, false);
672 const char *last_cmd
= register_get(vis
, &vis
->registers
[VIS_REG_SHELL
], NULL
);
673 return last_cmd
? strdup(last_cmd
) : NULL
;
675 register_put0(vis
, &vis
->registers
[VIS_REG_SHELL
], cmd
);
679 static void parse_argv(const char **s
, const char *argv
[], size_t maxarg
) {
680 for (size_t i
= 0; i
< maxarg
; i
++) {
682 if (**s
== '"' || **s
== '\'')
683 argv
[i
] = parse_delimited(s
, CMD_ARGV
);
685 argv
[i
] = parse_until(s
, " \t\n", "\'\"", CMD_ARGV
);
689 static bool valid_cmdname(const char *s
) {
690 unsigned char c
= (unsigned char)*s
;
691 return c
&& !isspace(c
) && !isdigit(c
) && (!ispunct(c
) || c
== '_' || (c
== '-' && valid_cmdname(s
+1)));
694 static char *parse_cmdname(const char **s
) {
699 while (valid_cmdname(*s
))
700 buffer_append(&buf
, (*s
)++, 1);
702 buffer_terminate(&buf
);
704 return buffer_move(&buf
);
707 static Regex
*parse_regex(Vis
*vis
, const char **s
) {
708 const char *before
= *s
;
709 char *pattern
= parse_delimited(s
, CMD_REGEX
);
710 if (!pattern
&& *s
== before
)
712 Regex
*regex
= vis_regex(vis
, pattern
, 0);
717 static enum SamError
parse_count(const char **s
, Count
*count
) {
718 count
->mod
= **s
== '%';
722 int n
= parse_number(s
);
724 return SAM_ERR_COUNT
;
730 const char *before
= *s
;
731 if (!(count
->start
= parse_number(s
)) && *s
!= before
)
732 return SAM_ERR_COUNT
;
734 count
->end
= count
->start
? count
->start
: INT_MAX
;
740 if (!(count
->end
= parse_number(s
)) && *s
!= before
)
741 return SAM_ERR_COUNT
;
743 count
->end
= INT_MAX
;
747 static Address
*address_parse_simple(Vis
*vis
, const char **s
, enum SamError
*err
) {
760 case '#': /* character #n */
762 addr
.number
= parse_number(s
);
764 case '0': case '1': case '2': case '3': case '4': /* line n */
765 case '5': case '6': case '7': case '8': case '9':
767 addr
.number
= parse_number(s
);
771 if ((addr
.number
= vis_mark_from(vis
, **s
)) == VIS_MARK_INVALID
) {
777 case '/': /* regexp forwards */
778 case '?': /* regexp backwards */
779 addr
.regex
= parse_regex(vis
, s
);
781 *err
= SAM_ERR_REGEX
;
785 case '$': /* end of file */
796 if ((addr
.right
= address_parse_simple(vis
, s
, err
))) {
797 switch (addr
.right
->type
) {
805 if (addr
.type
!= '+' && addr
.type
!= '-') {
806 Address
*plus
= address_new();
808 address_free(addr
.right
);
812 plus
->right
= addr
.right
;
819 Address
*ret
= address_new();
821 address_free(addr
.right
);
828 static Address
*address_parse_compound(Vis
*vis
, const char **s
, enum SamError
*err
) {
829 Address addr
= { 0 }, *left
= address_parse_simple(vis
, s
, err
), *right
= NULL
;
833 case ',': /* a1,a2 */
834 case ';': /* a1;a2 */
836 right
= address_parse_compound(vis
, s
, err
);
837 if (right
&& (right
->type
== ',' || right
->type
== ';') && !right
->left
) {
838 *err
= SAM_ERR_ADDRESS
;
849 Address
*ret
= address_new();
861 static Command
*command_new(const char *name
) {
862 Command
*cmd
= calloc(1, sizeof(Command
));
865 if (name
&& !(cmd
->argv
[0] = strdup(name
))) {
872 static void command_free(Command
*cmd
) {
876 for (Command
*c
= cmd
->cmd
, *next
; c
; c
= next
) {
881 for (const char **args
= cmd
->argv
; *args
; args
++)
883 address_free(cmd
->address
);
887 static const CommandDef
*command_lookup(Vis
*vis
, const char *name
) {
888 return map_closest(vis
->cmds
, name
);
891 static Command
*command_parse(Vis
*vis
, const char **s
, enum SamError
*err
) {
893 *err
= SAM_ERR_COMMAND
;
896 Command
*cmd
= command_new(NULL
);
900 cmd
->address
= address_parse_compound(vis
, s
, err
);
903 cmd
->argv
[0] = parse_cmdname(s
);
906 char name
[2] = { **s
? **s
: 'p', '\0' };
909 if (!(cmd
->argv
[0] = strdup(name
)))
913 const CommandDef
*cmddef
= command_lookup(vis
, cmd
->argv
[0]);
915 *err
= SAM_ERR_COMMAND
;
919 cmd
->cmddef
= cmddef
;
921 if (strcmp(cmd
->argv
[0], "{") == 0) {
922 Command
*prev
= NULL
, *next
;
923 int level
= vis
->nesting_level
++;
925 while (**s
== ' ' || **s
== '\t' || **s
== '\n')
927 next
= command_parse(vis
, s
, err
);
934 } while ((prev
= next
));
935 if (level
!= vis
->nesting_level
) {
936 *err
= SAM_ERR_UNMATCHED_BRACE
;
939 } else if (strcmp(cmd
->argv
[0], "}") == 0) {
940 if (vis
->nesting_level
-- == 0) {
941 *err
= SAM_ERR_UNMATCHED_BRACE
;
948 if (cmddef
->flags
& CMD_ADDRESS_NONE
&& cmd
->address
) {
949 *err
= SAM_ERR_NO_ADDRESS
;
953 if (cmddef
->flags
& CMD_FORCE
&& **s
== '!') {
958 if ((cmddef
->flags
& CMD_COUNT
) && (*err
= parse_count(s
, &cmd
->count
)))
961 if (cmddef
->flags
& CMD_REGEX
) {
962 if ((cmddef
->flags
& CMD_REGEX_DEFAULT
) && (!**s
|| **s
== ' ')) {
965 const char *before
= *s
;
966 cmd
->regex
= parse_regex(vis
, s
);
967 if (!cmd
->regex
&& (*s
!= before
|| !(cmddef
->flags
& CMD_COUNT
))) {
968 *err
= SAM_ERR_REGEX
;
974 if (cmddef
->flags
& CMD_SHELL
&& !(cmd
->argv
[1] = parse_shellcmd(vis
, s
))) {
975 *err
= SAM_ERR_SHELL
;
979 if (cmddef
->flags
& CMD_TEXT
&& !(cmd
->argv
[1] = parse_text(s
, &cmd
->count
))) {
984 if (cmddef
->flags
& CMD_ARGV
) {
985 parse_argv(s
, &cmd
->argv
[1], MAX_ARGV
-2);
986 cmd
->argv
[MAX_ARGV
-1] = NULL
;
989 if (cmddef
->flags
& CMD_CMD
) {
991 if (cmddef
->defcmd
&& (**s
== '\n' || **s
== '}' || **s
== '\0')) {
994 if (!(cmd
->cmd
= command_new(cmddef
->defcmd
)))
996 cmd
->cmd
->cmddef
= command_lookup(vis
, cmddef
->defcmd
);
998 if (!(cmd
->cmd
= command_parse(vis
, s
, err
)))
1000 if (strcmp(cmd
->argv
[0], "X") == 0 || strcmp(cmd
->argv
[0], "Y") == 0) {
1001 Command
*sel
= command_new("select");
1004 sel
->cmd
= cmd
->cmd
;
1005 sel
->cmddef
= &cmddef_select
;
1017 static Command
*sam_parse(Vis
*vis
, const char *cmd
, enum SamError
*err
) {
1018 vis
->nesting_level
= 0;
1019 const char **s
= &cmd
;
1020 Command
*c
= command_parse(vis
, s
, err
);
1023 while (**s
== ' ' || **s
== '\t' || **s
== '\n')
1026 *err
= SAM_ERR_NEWLINE
;
1031 Command
*sel
= command_new("select");
1037 sel
->cmddef
= &cmddef_select
;
1041 static Filerange
address_line_evaluate(Address
*addr
, File
*file
, Filerange
*range
, int sign
) {
1042 Text
*txt
= file
->text
;
1043 size_t offset
= addr
->number
!= EPOS
? addr
->number
: 1;
1044 size_t start
= range
->start
, end
= range
->end
, line
;
1047 if (start
< end
&& text_byte_get(txt
, end
-1, &c
) && c
== '\n')
1049 line
= text_lineno_by_pos(txt
, end
);
1050 line
= text_pos_by_lineno(txt
, line
+ offset
);
1051 } else if (sign
< 0) {
1052 line
= text_lineno_by_pos(txt
, start
);
1053 line
= offset
< line
? text_pos_by_lineno(txt
, line
- offset
) : 0;
1055 if (addr
->number
== 0)
1056 return text_range_new(0, 0);
1057 line
= text_pos_by_lineno(txt
, addr
->number
);
1060 if (addr
->type
== 'g')
1061 return text_range_new(line
, line
);
1063 return text_range_new(line
, text_line_next(txt
, line
));
1066 static Filerange
address_evaluate(Address
*addr
, File
*file
, Selection
*sel
, Filerange
*range
, int sign
) {
1067 Filerange ret
= text_range_empty();
1070 switch (addr
->type
) {
1073 ret
.start
= ret
.end
= range
->end
+ addr
->number
;
1075 ret
.start
= ret
.end
= range
->start
- addr
->number
;
1077 ret
= text_range_new(addr
->number
, addr
->number
);
1081 ret
= address_line_evaluate(addr
, file
, range
, sign
);
1086 Array
*marks
= &file
->marks
[addr
->number
];
1087 size_t idx
= sel
? view_selections_number(sel
) : 0;
1088 SelectionRegion
*sr
= array_get(marks
, idx
);
1090 pos
= text_mark_get(file
->text
, sr
->cursor
);
1091 ret
= text_range_new(pos
, pos
);
1095 sign
= sign
== 0 ? -1 : -sign
;
1099 ret
= text_object_search_forward(file
->text
, range
->end
, addr
->regex
);
1101 ret
= text_object_search_backward(file
->text
, range
->start
, addr
->regex
);
1105 size_t size
= text_size(file
->text
);
1106 ret
= text_range_new(size
, size
);
1114 sign
= addr
->type
== '+' ? +1 : -1;
1115 if (!addr
->right
|| addr
->right
->type
== '+' || addr
->right
->type
== '-')
1116 ret
= address_line_evaluate(addr
, file
, range
, sign
);
1121 Filerange left
, right
;
1123 left
= address_evaluate(addr
->left
, file
, sel
, range
, 0);
1125 left
= text_range_new(0, 0);
1127 if (addr
->type
== ';')
1131 right
= address_evaluate(addr
->right
, file
, sel
, range
, 0);
1133 size_t size
= text_size(file
->text
);
1134 right
= text_range_new(size
, size
);
1136 /* TODO: enforce strict ordering? */
1137 return text_range_union(&left
, &right
);
1140 return text_range_new(0, text_size(file
->text
));
1142 if (text_range_valid(&ret
))
1144 } while ((addr
= addr
->right
));
1149 static bool count_evaluate(Command
*cmd
) {
1150 Count
*count
= &cmd
->count
;
1152 return count
->start
? cmd
->iteration
% count
->start
== 0 : true;
1153 return count
->start
<= cmd
->iteration
&& cmd
->iteration
<= count
->end
;
1156 static bool sam_execute(Vis
*vis
, Win
*win
, Command
*cmd
, Selection
*sel
, Filerange
*range
) {
1158 if (cmd
->address
&& win
)
1159 *range
= address_evaluate(cmd
->address
, win
->file
, sel
, range
, 0);
1162 switch (cmd
->argv
[0][0]) {
1165 for (Command
*c
= cmd
->cmd
; c
&& ret
; c
= c
->next
)
1166 ret
&= sam_execute(vis
, win
, c
, NULL
, range
);
1167 view_selections_dispose_force(sel
);
1171 ret
= cmd
->cmddef
->func(vis
, win
, cmd
, cmd
->argv
, sel
, range
);
1177 static enum SamError
validate(Command
*cmd
, bool loop
, bool group
) {
1178 if (cmd
->cmddef
->flags
& CMD_DESTRUCTIVE
) {
1180 return SAM_ERR_LOOP_INVALID_CMD
;
1182 return SAM_ERR_GROUP_INVALID_CMD
;
1185 group
|= (cmd
->cmddef
->flags
& CMD_GROUP
);
1186 loop
|= (cmd
->cmddef
->flags
& CMD_LOOP
);
1187 for (Command
*c
= cmd
->cmd
; c
; c
= c
->next
) {
1188 enum SamError err
= validate(c
, loop
, group
);
1189 if (err
!= SAM_ERR_OK
)
1195 static enum SamError
command_validate(Command
*cmd
) {
1196 return validate(cmd
, false, false);
1199 static bool count_negative(Command
*cmd
) {
1200 if (cmd
->count
.start
< 0 || cmd
->count
.end
< 0)
1202 for (Command
*c
= cmd
->cmd
; c
; c
= c
->next
) {
1203 if (c
->cmddef
->func
!= cmd_extract
&& c
->cmddef
->func
!= cmd_select
) {
1204 if (count_negative(c
))
1211 static void count_init(Command
*cmd
, int max
) {
1212 Count
*count
= &cmd
->count
;
1214 if (count
->start
< 0)
1215 count
->start
+= max
;
1218 for (Command
*c
= cmd
->cmd
; c
; c
= c
->next
) {
1219 if (c
->cmddef
->func
!= cmd_extract
&& c
->cmddef
->func
!= cmd_select
)
1224 enum SamError
sam_cmd(Vis
*vis
, const char *s
) {
1225 enum SamError err
= SAM_ERR_OK
;
1229 Command
*cmd
= sam_parse(vis
, s
, &err
);
1231 if (err
== SAM_ERR_OK
)
1232 err
= SAM_ERR_MEMORY
;
1236 err
= command_validate(cmd
);
1237 if (err
!= SAM_ERR_OK
) {
1242 for (File
*file
= vis
->files
; file
; file
= file
->next
) {
1245 sam_transcript_init(&file
->transcript
);
1248 bool visual
= vis
->mode
->visual
;
1249 size_t primary_pos
= vis
->win
? view_cursor_get(vis
->win
->view
) : EPOS
;
1250 Filerange range
= text_range_empty();
1251 sam_execute(vis
, vis
->win
, cmd
, NULL
, &range
);
1253 for (File
*file
= vis
->files
; file
; file
= file
->next
) {
1256 Transcript
*t
= &file
->transcript
;
1257 if (t
->error
!= SAM_ERR_OK
) {
1259 sam_transcript_free(t
);
1262 vis_file_snapshot(vis
, file
);
1263 ptrdiff_t delta
= 0;
1264 for (Change
*c
= t
->changes
; c
; c
= c
->next
) {
1265 c
->range
.start
+= delta
;
1266 c
->range
.end
+= delta
;
1267 if (c
->type
& TRANSCRIPT_DELETE
) {
1268 text_delete_range(file
->text
, &c
->range
);
1269 delta
-= text_range_size(&c
->range
);
1270 if (c
->sel
&& c
->type
== TRANSCRIPT_DELETE
) {
1272 view_selections_dispose_force(c
->sel
);
1274 view_cursors_to(c
->sel
, c
->range
.start
);
1277 if (c
->type
& TRANSCRIPT_INSERT
) {
1278 for (int i
= 0; i
< c
->count
; i
++) {
1279 text_insert(file
->text
, c
->range
.start
, c
->data
, c
->len
);
1282 Filerange r
= text_range_new(c
->range
.start
,
1283 c
->range
.start
+ c
->len
* c
->count
);
1286 view_selections_set(c
->sel
, &r
);
1287 view_selections_anchor(c
->sel
, true);
1289 if (memchr(c
->data
, '\n', c
->len
))
1290 view_cursors_to(c
->sel
, r
.start
);
1292 view_cursors_to(c
->sel
, r
.end
);
1294 } else if (visual
) {
1295 Selection
*sel
= view_selections_new(c
->win
->view
, r
.start
);
1297 view_selections_set(sel
, &r
);
1298 view_selections_anchor(sel
, true);
1303 sam_transcript_free(&file
->transcript
);
1304 vis_file_snapshot(vis
, file
);
1307 for (Win
*win
= vis
->windows
; win
; win
= win
->next
)
1308 view_selections_normalize(win
->view
);
1311 if (primary_pos
!= EPOS
&& view_selection_disposed(vis
->win
->view
))
1312 view_cursor_to(vis
->win
->view
, primary_pos
);
1313 view_selections_primary_set(view_selections(vis
->win
->view
));
1314 vis_jumplist_save(vis
);
1315 bool completed
= true;
1316 for (Selection
*s
= view_selections(vis
->win
->view
); s
; s
= view_selections_next(s
)) {
1317 if (view_selections_anchored(s
)) {
1322 vis_mode_switch(vis
, completed
? VIS_MODE_NORMAL
: VIS_MODE_VISUAL
);
1328 /* process text input, substitute register content for backreferences etc. */
1329 Buffer
text(Vis
*vis
, const char *text
) {
1332 for (size_t len
= strcspn(text
, "\\&"); *text
; len
= strcspn(++text
, "\\&")) {
1333 buffer_append(&buf
, text
, len
);
1335 enum VisRegister regid
= VIS_REG_INVALID
;
1338 regid
= VIS_REG_AMPERSAND
;
1341 if ('1' <= text
[1] && text
[1] <= '9') {
1342 regid
= VIS_REG_1
+ text
[1] - '1';
1344 } else if (text
[1] == '\\' || text
[1] == '&') {
1354 if (regid
!= VIS_REG_INVALID
) {
1355 data
= register_get(vis
, &vis
->registers
[regid
], ®len
);
1360 buffer_append(&buf
, data
, reglen
);
1366 static bool cmd_insert(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1369 Buffer buf
= text(vis
, argv
[1]);
1370 size_t len
= buffer_length(&buf
);
1371 char *data
= buffer_move(&buf
);
1372 bool ret
= sam_insert(win
, sel
, range
->start
, data
, len
, cmd
->count
.start
);
1378 static bool cmd_append(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1381 Buffer buf
= text(vis
, argv
[1]);
1382 size_t len
= buffer_length(&buf
);
1383 char *data
= buffer_move(&buf
);
1384 bool ret
= sam_insert(win
, sel
, range
->end
, data
, len
, cmd
->count
.start
);
1390 static bool cmd_change(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1393 Buffer buf
= text(vis
, argv
[1]);
1394 size_t len
= buffer_length(&buf
);
1395 char *data
= buffer_move(&buf
);
1396 bool ret
= sam_change(win
, sel
, range
, data
, len
, cmd
->count
.start
);
1402 static bool cmd_delete(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1403 return win
&& sam_delete(win
, sel
, range
);
1406 static bool cmd_guard(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1410 RegexMatch captures
[1];
1411 size_t len
= text_range_size(range
);
1414 else if (!text_search_range_forward(win
->file
->text
, range
->start
, len
, cmd
->regex
, 1, captures
, 0))
1415 match
= captures
[0].start
< range
->end
;
1416 if ((count_evaluate(cmd
) && match
) ^ (argv
[0][0] == 'v'))
1417 return sam_execute(vis
, win
, cmd
->cmd
, sel
, range
);
1418 view_selections_dispose_force(sel
);
1422 static int extract(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
, bool simulate
) {
1425 Text
*txt
= win
->file
->text
;
1428 size_t start
= range
->start
, end
= range
->end
;
1429 size_t last_start
= argv
[0][0] == 'x' ? EPOS
: start
;
1430 size_t nsub
= 1 + text_regex_nsub(cmd
->regex
);
1431 if (nsub
> MAX_REGEX_SUB
)
1432 nsub
= MAX_REGEX_SUB
;
1433 RegexMatch match
[MAX_REGEX_SUB
];
1434 while (start
<= end
) {
1436 int flags
= start
> range
->start
&&
1437 text_byte_get(txt
, start
- 1, &c
) && c
!= '\n' ?
1439 bool found
= !text_search_range_forward(txt
, start
, end
- start
,
1440 cmd
->regex
, nsub
, match
,
1442 Filerange r
= text_range_empty();
1444 if (argv
[0][0] == 'x')
1445 r
= text_range_new(match
[0].start
, match
[0].end
);
1447 r
= text_range_new(last_start
, match
[0].start
);
1448 if (match
[0].start
== match
[0].end
) {
1449 if (last_start
== match
[0].start
) {
1453 /* in Plan 9's regexp library ^ matches the beginning
1454 * of a line, however in POSIX with REG_NEWLINE ^
1455 * matches the zero-length string immediately after a
1456 * newline. Try filtering out the last such match at EOF.
1458 if (end
== match
[0].start
&& start
> range
->start
&&
1459 text_byte_get(txt
, end
-1, &c
) && c
== '\n')
1461 start
= match
[0].end
+ 1;
1463 start
= match
[0].end
;
1466 if (argv
[0][0] == 'y')
1467 r
= text_range_new(start
, end
);
1471 if (text_range_valid(&r
)) {
1473 for (size_t i
= 0; i
< nsub
; i
++) {
1474 Register
*reg
= &vis
->registers
[VIS_REG_AMPERSAND
+i
];
1475 register_put_range(vis
, reg
, txt
, &match
[i
]);
1477 last_start
= match
[0].end
;
1484 ret
&= sam_execute(vis
, win
, cmd
->cmd
, NULL
, &r
);
1488 size_t start
= range
->start
, end
= range
->end
;
1489 while (start
< end
) {
1490 size_t next
= text_line_next(txt
, start
);
1493 Filerange r
= text_range_new(start
, next
);
1494 if (start
== next
|| !text_range_valid(&r
))
1499 ret
&= sam_execute(vis
, win
, cmd
->cmd
, NULL
, &r
);
1505 view_selections_dispose_force(sel
);
1506 return simulate
? count
: ret
;
1509 static bool cmd_extract(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1510 if (!win
|| !text_range_valid(range
))
1513 if (count_negative(cmd
->cmd
))
1514 matches
= extract(vis
, win
, cmd
, argv
, sel
, range
, true);
1515 count_init(cmd
->cmd
, matches
+1);
1516 return extract(vis
, win
, cmd
, argv
, sel
, range
, false);
1519 static bool cmd_select(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1520 Filerange r
= text_range_empty();
1522 return sam_execute(vis
, NULL
, cmd
->cmd
, NULL
, &r
);
1524 View
*view
= win
->view
;
1525 Text
*txt
= win
->file
->text
;
1526 bool multiple_cursors
= view_selections_count(view
) > 1;
1527 Selection
*primary
= view_selections_primary_get(view
);
1529 if (vis
->mode
->visual
)
1530 count_init(cmd
->cmd
, view_selections_count(view
)+1);
1532 for (Selection
*s
= view_selections(view
), *next
; s
&& ret
; s
= next
) {
1533 next
= view_selections_next(s
);
1534 size_t pos
= view_cursors_pos(s
);
1535 if (vis
->mode
->visual
) {
1536 r
= view_selections_get(s
);
1537 } else if (cmd
->cmd
->address
) {
1538 /* convert a single line range to a goto line motion */
1539 if (!multiple_cursors
&& cmd
->cmd
->cmddef
->func
== cmd_print
) {
1540 Address
*addr
= cmd
->cmd
->address
;
1541 switch (addr
->type
) {
1547 if (addr
&& addr
->type
== 'l' && !addr
->right
)
1552 r
= text_range_new(pos
, pos
);
1553 } else if (cmd
->cmd
->cmddef
->flags
& CMD_ADDRESS_POS
) {
1554 r
= text_range_new(pos
, pos
);
1555 } else if (cmd
->cmd
->cmddef
->flags
& CMD_ADDRESS_LINE
) {
1556 r
= text_object_line(txt
, pos
);
1557 } else if (cmd
->cmd
->cmddef
->flags
& CMD_ADDRESS_AFTER
) {
1558 size_t next_line
= text_line_next(txt
, pos
);
1559 r
= text_range_new(next_line
, next_line
);
1560 } else if (cmd
->cmd
->cmddef
->flags
& CMD_ADDRESS_ALL
) {
1561 r
= text_range_new(0, text_size(txt
));
1562 } else if (!multiple_cursors
&& (cmd
->cmd
->cmddef
->flags
& CMD_ADDRESS_ALL_1CURSOR
)) {
1563 r
= text_range_new(0, text_size(txt
));
1565 r
= text_range_new(pos
, text_char_next(txt
, pos
));
1567 if (!text_range_valid(&r
))
1568 r
= text_range_new(0, 0);
1569 ret
&= sam_execute(vis
, win
, cmd
->cmd
, s
, &r
);
1570 if (cmd
->cmd
->cmddef
->flags
& CMD_ONCE
)
1574 if (vis
->win
&& vis
->win
->view
== view
&& primary
!= view_selections_primary_get(view
))
1575 view_selections_primary_set(view_selections(view
));
1579 static bool cmd_print(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1580 if (!win
|| !text_range_valid(range
))
1582 View
*view
= win
->view
;
1584 sel
= view_selections_new_force(view
, range
->start
);
1587 if (range
->start
!= range
->end
) {
1588 view_selections_set(sel
, range
);
1589 view_selections_anchor(sel
, true);
1591 view_cursors_to(sel
, range
->start
);
1592 view_selection_clear(sel
);
1597 static bool cmd_files(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1599 for (Win
*wn
, *w
= vis
->windows
; w
; w
= wn
) {
1600 /* w can get freed by sam_execute() so store w->next early */
1602 if (w
->file
->internal
)
1604 bool match
= !cmd
->regex
||
1605 (w
->file
->name
&& text_regex_match(cmd
->regex
, w
->file
->name
, 0) == 0);
1606 if (match
^ (argv
[0][0] == 'Y')) {
1607 Filerange def
= text_range_new(0, 0);
1608 ret
&= sam_execute(vis
, w
, cmd
->cmd
, NULL
, &def
);
1614 static bool cmd_substitute(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1615 vis_info_show(vis
, "Use :x/pattern/ c/replacement/ instead");
1619 /* cmd_write stores win->file's contents end emits pre/post events.
1620 * If the range r covers the whole file, it is updated to account for
1621 * potential file's text mutation by a FILE_SAVE_PRE callback.
1623 static bool cmd_write(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*r
) {
1627 File
*file
= win
->file
;
1628 if (sam_transcript_error(&file
->transcript
, SAM_ERR_WRITE_CONFLICT
))
1631 Text
*text
= file
->text
;
1632 Filerange range_all
= text_range_new(0, text_size(text
));
1633 bool write_entire_file
= text_range_equal(r
, &range_all
);
1635 const char *filename
= argv
[1];
1637 filename
= file
->name
;
1639 if (file
->fd
== -1) {
1640 vis_info_show(vis
, "Filename expected");
1643 if (!strchr(argv
[0], 'q')) {
1644 vis_info_show(vis
, "No filename given, use 'wq' to write to stdout");
1648 if (!vis_event_emit(vis
, VIS_EVENT_FILE_SAVE_PRE
, file
, (char*)NULL
) && cmd
->flags
!= '!') {
1649 vis_info_show(vis
, "Rejected write to stdout by pre-save hook");
1652 /* a pre-save hook may have changed the text; need to re-take the range */
1653 if (write_entire_file
)
1654 *r
= text_range_new(0, text_size(text
));
1656 bool visual
= vis
->mode
->visual
;
1658 for (Selection
*s
= view_selections(win
->view
); s
; s
= view_selections_next(s
)) {
1659 Filerange range
= visual
? view_selections_get(s
) : *r
;
1660 ssize_t written
= text_write_range(text
, &range
, file
->fd
);
1661 if (written
== -1 || (size_t)written
!= text_range_size(&range
)) {
1662 vis_info_show(vis
, "Can not write to stdout");
1669 /* make sure the file is marked as saved i.e. not modified */
1670 text_save(text
, NULL
);
1671 vis_event_emit(vis
, VIS_EVENT_FILE_SAVE_POST
, file
, (char*)NULL
);
1675 if (!argv
[1] && cmd
->flags
!= '!') {
1676 if (vis
->mode
->visual
) {
1677 vis_info_show(vis
, "WARNING: file will be reduced to active selection");
1680 if (!write_entire_file
) {
1681 vis_info_show(vis
, "WARNING: file will be reduced to provided range");
1686 for (const char **name
= argv
[1] ? &argv
[1] : (const char*[]){ filename
, NULL
}; *name
; name
++) {
1688 char *path
= absolute_path(*name
);
1693 bool existing_file
= !stat(path
, &meta
);
1694 bool same_file
= existing_file
&& file
->name
&&
1695 file
->stat
.st_dev
== meta
.st_dev
&& file
->stat
.st_ino
== meta
.st_ino
;
1697 if (cmd
->flags
!= '!') {
1698 if (same_file
&& file
->stat
.st_mtime
&& file
->stat
.st_mtime
< meta
.st_mtime
) {
1699 vis_info_show(vis
, "WARNING: file has been changed since reading it");
1702 if (existing_file
&& !same_file
) {
1703 vis_info_show(vis
, "WARNING: file exists");
1708 if (!vis_event_emit(vis
, VIS_EVENT_FILE_SAVE_PRE
, file
, path
) && cmd
->flags
!= '!') {
1709 vis_info_show(vis
, "Rejected write to `%s' by pre-save hook", path
);
1712 /* a pre-save hook may have changed the text; need to re-take the range */
1713 if (write_entire_file
)
1714 *r
= text_range_new(0, text_size(text
));
1716 TextSave
*ctx
= text_save_begin(text
, AT_FDCWD
, path
, file
->save_method
);
1718 const char *msg
= errno
? strerror(errno
) : "try changing `:set savemethod`";
1719 vis_info_show(vis
, "Can't write `%s': %s", path
, msg
);
1723 bool failure
= false;
1724 bool visual
= vis
->mode
->visual
;
1726 for (Selection
*s
= view_selections(win
->view
); s
; s
= view_selections_next(s
)) {
1727 Filerange range
= visual
? view_selections_get(s
) : *r
;
1728 ssize_t written
= text_save_write_range(ctx
, &range
);
1729 failure
= (written
== -1 || (size_t)written
!= text_range_size(&range
));
1731 text_save_cancel(ctx
);
1739 if (failure
|| !text_save_commit(ctx
)) {
1740 vis_info_show(vis
, "Can't write `%s': %s", path
, strerror(errno
));
1745 file_name_set(file
, path
);
1748 if (same_file
|| (!existing_file
&& strcmp(file
->name
, path
) == 0))
1749 file
->stat
= text_stat(text
);
1750 vis_event_emit(vis
, VIS_EVENT_FILE_SAVE_POST
, file
, path
);
1761 static ssize_t
read_buffer(void *context
, char *data
, size_t len
) {
1762 buffer_append(context
, data
, len
);
1766 static bool cmd_filter(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1770 Buffer bufout
, buferr
;
1771 buffer_init(&bufout
);
1772 buffer_init(&buferr
);
1774 int status
= vis_pipe(vis
, win
->file
, range
, &argv
[1], &bufout
, read_buffer
, &buferr
, read_buffer
, false);
1776 if (vis
->interrupted
) {
1777 vis_info_show(vis
, "Command cancelled");
1778 } else if (status
== 0) {
1779 size_t len
= buffer_length(&bufout
);
1780 char *data
= buffer_move(&bufout
);
1781 if (!sam_change(win
, sel
, range
, data
, len
, 1))
1784 vis_info_show(vis
, "Command failed %s", buffer_content0(&buferr
));
1787 buffer_release(&bufout
);
1788 buffer_release(&buferr
);
1790 return !vis
->interrupted
&& status
== 0;
1793 static bool cmd_launch(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1794 Filerange invalid
= text_range_new(sel
? view_cursors_pos(sel
) : range
->start
, EPOS
);
1795 return cmd_filter(vis
, win
, cmd
, argv
, sel
, &invalid
);
1798 static bool cmd_pipein(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1801 Filerange filter_range
= text_range_new(range
->end
, range
->end
);
1802 bool ret
= cmd_filter(vis
, win
, cmd
, argv
, sel
, &filter_range
);
1804 ret
= sam_delete(win
, NULL
, range
);
1808 static bool cmd_pipeout(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1812 buffer_init(&buferr
);
1814 int status
= vis_pipe(vis
, win
->file
, range
, (const char*[]){ argv
[1], NULL
}, NULL
, NULL
, &buferr
, read_buffer
, false);
1816 if (vis
->interrupted
)
1817 vis_info_show(vis
, "Command cancelled");
1818 else if (status
!= 0)
1819 vis_info_show(vis
, "Command failed %s", buffer_content0(&buferr
));
1821 buffer_release(&buferr
);
1823 return !vis
->interrupted
&& status
== 0;
1826 static bool cmd_cd(Vis
*vis
, Win
*win
, Command
*cmd
, const char *argv
[], Selection
*sel
, Filerange
*range
) {
1827 const char *dir
= argv
[1];
1829 dir
= getenv("HOME");
1830 return dir
&& chdir(dir
) == 0;
1833 #include "vis-cmds.c"