vis

a vi-like editor based on Plan 9's structural regular expressions

git clone https://9o.is/git/vis.git

sam.c

(53269B)


      1 /*
      2  * Heavily inspired (and partially based upon) the X11 version of
      3  * Rob Pike's sam text editor originally written for Plan 9.
      4  *
      5  *  Copyright © 2016-2020 Marc André Tanner <mat at brain-dump.org>
      6  *  Copyright © 1998 by Lucent Technologies
      7  *
      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.
     13  *
     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.
     18  */
     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"
     35 
     36 #define MAX_ARGV 8
     37 
     38 typedef struct Address Address;
     39 typedef struct Command Command;
     40 typedef struct CommandDef CommandDef;
     41 
     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? */
     55 };
     56 
     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 */
     63 };
     64 
     65 typedef struct {
     66 	int start, end; /* interval [n,m] */
     67 	bool mod;       /* % every n-th match, implies n == m */
     68 } Count;
     69 
     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 */
     80 };
     81 
     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 */
    108 };
    109 
    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*);
    145 
    146 static const CommandDef cmds[] = {
    147 	//      name            help
    148 	//      flags, default command, implementation
    149 	{
    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
    218 	},
    219 	/* vi(m) related commands */
    220 	{
    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
    268 	},
    269 	{ NULL, VIS_HELP(NULL) CMD_NONE, NULL, NULL },
    270 };
    271 
    272 static const CommandDef cmddef_select = {
    273 	NULL, VIS_HELP(NULL) CMD_NONE, NULL, cmd_select
    274 };
    275 
    276 /* :set command options */
    277 typedef struct {
    278 	const char *names[3];            /* name and optional alias */
    279 	enum VisOption flags;            /* option type, etc. */
    280 	VIS_HELP_DECL(const char *help;) /* short, one line help text */
    281 	VisOptionFunction *func;         /* option handler, NULL for builtins */
    282 	void *context;                   /* context passed to option handler function */
    283 } OptionDef;
    284 
    285 enum {
    286 	OPTION_SHELL,
    287 	OPTION_ESCDELAY,
    288 	OPTION_AUTOINDENT,
    289 	OPTION_EXPANDTAB,
    290 	OPTION_TABWIDTH,
    291 	OPTION_SHOW_SPACES,
    292 	OPTION_SHOW_TABS,
    293 	OPTION_SHOW_NEWLINES,
    294 	OPTION_SHOW_EOF,
    295 	OPTION_STATUSBAR,
    296 	OPTION_NUMBER,
    297 	OPTION_NUMBER_RELATIVE,
    298 	OPTION_CURSOR_LINE,
    299 	OPTION_COLOR_COLUMN,
    300 	OPTION_SAVE_METHOD,
    301 	OPTION_LOAD_METHOD,
    302 	OPTION_CHANGE_256COLORS,
    303 	OPTION_LAYOUT,
    304 	OPTION_IGNORECASE,
    305 	OPTION_BREAKAT,
    306 	OPTION_WRAP_COLUMN,
    307 };
    308 
    309 static const OptionDef options[] = {
    310 	[OPTION_SHELL] = {
    311 		{ "shell" },
    312 		VIS_OPTION_TYPE_STRING,
    313 		VIS_HELP("Shell to use for external commands (default: $SHELL, /etc/passwd, /bin/sh)")
    314 	},
    315 	[OPTION_ESCDELAY] = {
    316 		{ "escdelay" },
    317 		VIS_OPTION_TYPE_NUMBER,
    318 		VIS_HELP("Milliseconds to wait to distinguish <Escape> from terminal escape sequences")
    319 	},
    320 	[OPTION_AUTOINDENT] = {
    321 		{ "autoindent", "ai" },
    322 		VIS_OPTION_TYPE_BOOL,
    323 		VIS_HELP("Copy leading white space from previous line")
    324 	},
    325 	[OPTION_EXPANDTAB] = {
    326 		{ "expandtab", "et" },
    327 		VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
    328 		VIS_HELP("Replace entered <Tab> with `tabwidth` spaces")
    329 	},
    330 	[OPTION_TABWIDTH] = {
    331 		{ "tabwidth", "tw" },
    332 		VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
    333 		VIS_HELP("Number of spaces to display (and insert if `expandtab` is enabled) for a tab")
    334 	},
    335 	[OPTION_SHOW_SPACES] = {
    336 		{ "showspaces" },
    337 		VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
    338 		VIS_HELP("Display replacement symbol instead of a space")
    339 	},
    340 	[OPTION_SHOW_TABS] = {
    341 		{ "showtabs" },
    342 		VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
    343 		VIS_HELP("Display replacement symbol for tabs")
    344 	},
    345 	[OPTION_SHOW_NEWLINES] = {
    346 		{ "shownewlines" },
    347 		VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
    348 		VIS_HELP("Display replacement symbol for newlines")
    349 	},
    350 	[OPTION_SHOW_EOF] = {
    351 		{ "showeof" },
    352 		VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
    353 		VIS_HELP("Display replacement symbol for lines after the end of the file")
    354 	},
    355 	[OPTION_STATUSBAR] = {
    356 		{ "statusbar", "sb" },
    357 		VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
    358 		VIS_HELP("Display status bar")
    359 	},
    360 	[OPTION_NUMBER] = {
    361 		{ "numbers", "nu" },
    362 		VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
    363 		VIS_HELP("Display absolute line numbers")
    364 	},
    365 	[OPTION_NUMBER_RELATIVE] = {
    366 		{ "relativenumbers", "rnu" },
    367 		VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
    368 		VIS_HELP("Display relative line numbers")
    369 	},
    370 	[OPTION_CURSOR_LINE] = {
    371 		{ "cursorline", "cul" },
    372 		VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW,
    373 		VIS_HELP("Highlight current cursor line")
    374 	},
    375 	[OPTION_COLOR_COLUMN] = {
    376 		{ "colorcolumn", "cc" },
    377 		VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
    378 		VIS_HELP("Highlight a fixed column")
    379 	},
    380 	[OPTION_SAVE_METHOD] = {
    381 		{ "savemethod" },
    382 		VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW,
    383 		VIS_HELP("Save method to use for current file 'auto', 'atomic' or 'inplace'")
    384 	},
    385 	[OPTION_LOAD_METHOD] = {
    386 		{ "loadmethod" },
    387 		VIS_OPTION_TYPE_STRING,
    388 		VIS_HELP("How to load existing files 'auto', 'read' or 'mmap'")
    389 	},
    390 	[OPTION_CHANGE_256COLORS] = {
    391 		{ "change256colors" },
    392 		VIS_OPTION_TYPE_BOOL,
    393 		VIS_HELP("Change 256 color palette to support 24bit colors")
    394 	},
    395 	[OPTION_LAYOUT] = {
    396 		{ "layout" },
    397 		VIS_OPTION_TYPE_STRING,
    398 		VIS_HELP("Vertical or horizontal window layout")
    399 	},
    400 	[OPTION_IGNORECASE] = {
    401 		{ "ignorecase", "ic" },
    402 		VIS_OPTION_TYPE_BOOL,
    403 		VIS_HELP("Ignore case when searching")
    404 	},
    405 	[OPTION_BREAKAT] = {
    406 		{ "breakat", "brk" },
    407 		VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW,
    408 		VIS_HELP("Characters which might cause a word wrap")
    409 	},
    410 	[OPTION_WRAP_COLUMN] = {
    411 		{ "wrapcolumn", "wc" },
    412 		VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
    413 		VIS_HELP("Wrap lines at minimum of window width and wrapcolumn")
    414 	},
    415 };
    416 
    417 bool sam_init(Vis *vis) {
    418 	if (!(vis->cmds = map_new()))
    419 		return false;
    420 	bool ret = true;
    421 	for (const CommandDef *cmd = cmds; cmd && cmd->name; cmd++)
    422 		ret &= map_put(vis->cmds, cmd->name, cmd);
    423 
    424 	if (!(vis->options = map_new()))
    425 		return false;
    426 	for (int i = 0; i < LENGTH(options); i++) {
    427 		for (const char *const *name = options[i].names; *name; name++)
    428 			ret &= map_put(vis->options, *name, &options[i]);
    429 	}
    430 
    431 	return ret;
    432 }
    433 
    434 const char *sam_error(enum SamError err) {
    435 	static const char *error_msg[] = {
    436 		[SAM_ERR_OK]              = "Success",
    437 		[SAM_ERR_MEMORY]          = "Out of memory",
    438 		[SAM_ERR_ADDRESS]         = "Bad address",
    439 		[SAM_ERR_NO_ADDRESS]      = "Command takes no address",
    440 		[SAM_ERR_UNMATCHED_BRACE] = "Unmatched `}'",
    441 		[SAM_ERR_REGEX]           = "Bad regular expression",
    442 		[SAM_ERR_TEXT]            = "Bad text",
    443 		[SAM_ERR_SHELL]           = "Shell command expected",
    444 		[SAM_ERR_COMMAND]         = "Unknown command",
    445 		[SAM_ERR_EXECUTE]         = "Error executing command",
    446 		[SAM_ERR_NEWLINE]         = "Newline expected",
    447 		[SAM_ERR_MARK]            = "Invalid mark",
    448 		[SAM_ERR_CONFLICT]        = "Conflicting changes",
    449 		[SAM_ERR_WRITE_CONFLICT]  = "Can not write while changing",
    450 		[SAM_ERR_LOOP_INVALID_CMD]  = "Destructive command in looping construct",
    451 		[SAM_ERR_GROUP_INVALID_CMD] = "Destructive command in group",
    452 		[SAM_ERR_COUNT]           = "Invalid count",
    453 	};
    454 
    455 	size_t idx = err;
    456 	return idx < LENGTH(error_msg) ? error_msg[idx] : NULL;
    457 }
    458 
    459 static void change_free(Change *c) {
    460 	if (!c)
    461 		return;
    462 	free((char*)c->data);
    463 	free(c);
    464 }
    465 
    466 static Change *change_new(Transcript *t, enum ChangeType type, Filerange *range, Win *win, Selection *sel) {
    467 	if (!text_range_valid(range))
    468 		return NULL;
    469 	Change **prev, *next;
    470 	if (t->latest && t->latest->range.end <= range->start) {
    471 		prev = &t->latest->next;
    472 		next = t->latest->next;
    473 	} else {
    474 		prev = &t->changes;
    475 		next = t->changes;
    476 	}
    477 	while (next && next->range.end <= range->start) {
    478 		prev = &next->next;
    479 		next = next->next;
    480 	}
    481 	if (next && next->range.start < range->end) {
    482 		t->error = SAM_ERR_CONFLICT;
    483 		return NULL;
    484 	}
    485 	Change *new = calloc(1, sizeof *new);
    486 	if (new) {
    487 		new->type = type;
    488 		new->range = *range;
    489 		new->sel = sel;
    490 		new->win = win;
    491 		new->next = next;
    492 		*prev = new;
    493 		t->latest = new;
    494 	}
    495 	return new;
    496 }
    497 
    498 static void sam_transcript_init(Transcript *t) {
    499 	memset(t, 0, sizeof *t);
    500 }
    501 
    502 static bool sam_transcript_error(Transcript *t, enum SamError error) {
    503 	if (t->changes)
    504 		t->error = error;
    505 	return t->error;
    506 }
    507 
    508 static void sam_transcript_free(Transcript *t) {
    509 	for (Change *c = t->changes, *next; c; c = next) {
    510 		next = c->next;
    511 		change_free(c);
    512 	}
    513 }
    514 
    515 static bool sam_insert(Win *win, Selection *sel, size_t pos, const char *data, size_t len, int count) {
    516 	Filerange range = text_range_new(pos, pos);
    517 	Change *c = change_new(&win->file->transcript, TRANSCRIPT_INSERT, &range, win, sel);
    518 	if (c) {
    519 		c->data = data;
    520 		c->len = len;
    521 		c->count = count;
    522 	}
    523 	return c;
    524 }
    525 
    526 static bool sam_delete(Win *win, Selection *sel, Filerange *range) {
    527 	return change_new(&win->file->transcript, TRANSCRIPT_DELETE, range, win, sel);
    528 }
    529 
    530 static bool sam_change(Win *win, Selection *sel, Filerange *range, const char *data, size_t len, int count) {
    531 	Change *c = change_new(&win->file->transcript, TRANSCRIPT_CHANGE, range, win, sel);
    532 	if (c) {
    533 		c->data = data;
    534 		c->len = len;
    535 		c->count = count;
    536 	}
    537 	return c;
    538 }
    539 
    540 static Address *address_new(void) {
    541 	Address *addr = calloc(1, sizeof *addr);
    542 	if (addr)
    543 		addr->number = EPOS;
    544 	return addr;
    545 }
    546 
    547 static void address_free(Address *addr) {
    548 	if (!addr)
    549 		return;
    550 	text_regex_free(addr->regex);
    551 	address_free(addr->left);
    552 	address_free(addr->right);
    553 	free(addr);
    554 }
    555 
    556 static void skip_spaces(const char **s) {
    557 	while (**s == ' ' || **s == '\t')
    558 		(*s)++;
    559 }
    560 
    561 static char *parse_until(const char **s, const char *until, const char *escchars, int type){
    562 	Buffer buf = {0};
    563 	size_t len = strlen(until);
    564 	bool escaped = false;
    565 
    566 	for (; **s && (!memchr(until, **s, len) || escaped); (*s)++) {
    567 		if (type != CMD_SHELL && !escaped && **s == '\\') {
    568 			escaped = true;
    569 			continue;
    570 		}
    571 
    572 		char c = **s;
    573 
    574 		if (escaped) {
    575 			escaped = false;
    576 			if (c == '\n')
    577 				continue;
    578 			if (c == 'n') {
    579 				c = '\n';
    580 			} else if (c == 't') {
    581 				c = '\t';
    582 			} else if (type != CMD_REGEX && type != CMD_TEXT && c == '\\') {
    583 				// ignore one of the back slashes
    584 			} else {
    585 				bool delim = memchr(until, c, len);
    586 				bool esc = escchars && memchr(escchars, c, strlen(escchars));
    587 				if (!delim && !esc)
    588 					buffer_append(&buf, "\\", 1);
    589 			}
    590 		}
    591 
    592 		if (!buffer_append(&buf, &c, 1)) {
    593 			buffer_release(&buf);
    594 			return NULL;
    595 		}
    596 	}
    597 
    598 	buffer_terminate(&buf);
    599 
    600 	return buf.data;
    601 }
    602 
    603 static char *parse_delimited(const char **s, int type) {
    604 	char delim[2] = { **s, '\0' };
    605 	if (!delim[0] || isspace((unsigned char)delim[0]))
    606 		return NULL;
    607 	(*s)++;
    608 	char *chunk = parse_until(s, delim, NULL, type);
    609 	if (**s == delim[0])
    610 		(*s)++;
    611 	return chunk;
    612 }
    613 
    614 static int parse_number(const char **s) {
    615 	char *end = NULL;
    616 	int number = strtoull(*s, &end, 10);
    617 	if (end == *s)
    618 		return 0;
    619 	*s = end;
    620 	return number;
    621 }
    622 
    623 static char *parse_text(const char **s, Count *count) {
    624 	skip_spaces(s);
    625 	const char *before = *s;
    626 	count->start = parse_number(s);
    627 	if (*s == before)
    628 		count->start = 1;
    629 	if (**s != '\n') {
    630 		before = *s;
    631 		char *text = parse_delimited(s, CMD_TEXT);
    632 		return (!text && *s != before) ? strdup("") : text;
    633 	}
    634 
    635 	Buffer buf = {0};
    636 	const char *start = *s + 1;
    637 	bool dot = false;
    638 
    639 	for ((*s)++; **s && (!dot || **s != '\n'); (*s)++)
    640 		dot = (**s == '.');
    641 
    642 	if (!dot || !buffer_put(&buf, start, *s - start - 1) ||
    643 	    !buffer_append(&buf, "\0", 1)) {
    644 		buffer_release(&buf);
    645 		return NULL;
    646 	}
    647 
    648 	return buf.data;
    649 }
    650 
    651 static char *parse_shellcmd(Vis *vis, const char **s) {
    652 	skip_spaces(s);
    653 	char *cmd = parse_until(s, "\n", NULL, false);
    654 	if (!cmd) {
    655 		const char *last_cmd = register_get(vis, &vis->registers[VIS_REG_SHELL], NULL);
    656 		return last_cmd ? strdup(last_cmd) : NULL;
    657 	}
    658 	register_put0(vis, &vis->registers[VIS_REG_SHELL], cmd);
    659 	return cmd;
    660 }
    661 
    662 static void parse_argv(const char **s, const char *argv[], size_t maxarg) {
    663 	for (size_t i = 0; i < maxarg; i++) {
    664 		skip_spaces(s);
    665 		if (**s == '"' || **s == '\'')
    666 			argv[i] = parse_delimited(s, CMD_ARGV);
    667 		else
    668 			argv[i] = parse_until(s, " \t\n", "\'\"", CMD_ARGV);
    669 	}
    670 }
    671 
    672 static bool valid_cmdname(const char *s) {
    673 	unsigned char c = (unsigned char)*s;
    674 	return c && !isspace(c) && !isdigit(c) && (!ispunct(c) || c == '_' || (c == '-' && valid_cmdname(s+1)));
    675 }
    676 
    677 static char *parse_cmdname(const char **s) {
    678 	Buffer buf = {0};
    679 
    680 	skip_spaces(s);
    681 	while (valid_cmdname(*s))
    682 		buffer_append(&buf, (*s)++, 1);
    683 
    684 	buffer_terminate(&buf);
    685 
    686 	return buf.data;
    687 }
    688 
    689 static Regex *parse_regex(Vis *vis, const char **s) {
    690 	const char *before = *s;
    691 	char *pattern = parse_delimited(s, CMD_REGEX);
    692 	if (!pattern && *s == before)
    693 		return NULL;
    694 	Regex *regex = vis_regex(vis, pattern);
    695 	free(pattern);
    696 	return regex;
    697 }
    698 
    699 static enum SamError parse_count(const char **s, Count *count) {
    700 	count->mod = **s == '%';
    701 
    702 	if (count->mod) {
    703 		(*s)++;
    704 		int n = parse_number(s);
    705 		if (!n)
    706 			return SAM_ERR_COUNT;
    707 		count->start = n;
    708 		count->end = n;
    709 		return SAM_ERR_OK;
    710 	}
    711 
    712 	const char *before = *s;
    713 	if (!(count->start = parse_number(s)) && *s != before)
    714 		return SAM_ERR_COUNT;
    715 	if (**s != ',') {
    716 		count->end = count->start ? count->start : INT_MAX;
    717 		return SAM_ERR_OK;
    718 	} else {
    719 		(*s)++;
    720 	}
    721 	before = *s;
    722 	if (!(count->end = parse_number(s)) && *s != before)
    723 		return SAM_ERR_COUNT;
    724 	if (!count->end)
    725 		count->end = INT_MAX;
    726 	return SAM_ERR_OK;
    727 }
    728 
    729 static Address *address_parse_simple(Vis *vis, const char **s, enum SamError *err) {
    730 
    731 	skip_spaces(s);
    732 
    733 	Address addr = {
    734 		.type = **s,
    735 		.regex = NULL,
    736 		.number = EPOS,
    737 		.left = NULL,
    738 		.right = NULL,
    739 	};
    740 
    741 	switch (addr.type) {
    742 	case '#': /* character #n */
    743 		(*s)++;
    744 		addr.number = parse_number(s);
    745 		break;
    746 	case '0': case '1': case '2': case '3': case '4': /* line n */
    747 	case '5': case '6': case '7': case '8': case '9':
    748 		addr.type = 'l';
    749 		addr.number = parse_number(s);
    750 		break;
    751 	case '\'':
    752 		(*s)++;
    753 		if ((addr.number = vis_mark_from(vis, **s)) == VIS_MARK_INVALID) {
    754 			*err = SAM_ERR_MARK;
    755 			return NULL;
    756 		}
    757 		(*s)++;
    758 		break;
    759 	case '/': /* regexp forwards */
    760 	case '?': /* regexp backwards */
    761 		addr.regex = parse_regex(vis, s);
    762 		if (!addr.regex) {
    763 			*err = SAM_ERR_REGEX;
    764 			return NULL;
    765 		}
    766 		break;
    767 	case '$': /* end of file */
    768 	case '.':
    769 	case '+':
    770 	case '-':
    771 	case '%':
    772 		(*s)++;
    773 		break;
    774 	default:
    775 		return NULL;
    776 	}
    777 
    778 	if ((addr.right = address_parse_simple(vis, s, err))) {
    779 		switch (addr.right->type) {
    780 		case '.':
    781 		case '$':
    782 			return NULL;
    783 		case '#':
    784 		case 'l':
    785 		case '/':
    786 		case '?':
    787 			if (addr.type != '+' && addr.type != '-') {
    788 				Address *plus = address_new();
    789 				if (!plus) {
    790 					address_free(addr.right);
    791 					return NULL;
    792 				}
    793 				plus->type = '+';
    794 				plus->right = addr.right;
    795 				addr.right = plus;
    796 			}
    797 			break;
    798 		}
    799 	}
    800 
    801 	Address *ret = address_new();
    802 	if (!ret) {
    803 		address_free(addr.right);
    804 		return NULL;
    805 	}
    806 	*ret = addr;
    807 	return ret;
    808 }
    809 
    810 static Address *address_parse_compound(Vis *vis, const char **s, enum SamError *err) {
    811 	Address addr = { 0 }, *left = address_parse_simple(vis, s, err), *right = NULL;
    812 	skip_spaces(s);
    813 	addr.type = **s;
    814 	switch (addr.type) {
    815 	case ',': /* a1,a2 */
    816 	case ';': /* a1;a2 */
    817 		(*s)++;
    818 		right = address_parse_compound(vis, s, err);
    819 		if (right && (right->type == ',' || right->type == ';') && !right->left) {
    820 			*err = SAM_ERR_ADDRESS;
    821 			goto fail;
    822 		}
    823 		break;
    824 	default:
    825 		return left;
    826 	}
    827 
    828 	addr.left = left;
    829 	addr.right = right;
    830 
    831 	Address *ret = address_new();
    832 	if (ret) {
    833 		*ret = addr;
    834 		return ret;
    835 	}
    836 
    837 fail:
    838 	address_free(left);
    839 	address_free(right);
    840 	return NULL;
    841 }
    842 
    843 static Command *command_new(const char *name) {
    844 	Command *cmd = calloc(1, sizeof(Command));
    845 	if (!cmd)
    846 		return NULL;
    847 	if (name && !(cmd->argv[0] = strdup(name))) {
    848 		free(cmd);
    849 		return NULL;
    850 	}
    851 	return cmd;
    852 }
    853 
    854 static void command_free(Command *cmd) {
    855 	if (!cmd)
    856 		return;
    857 
    858 	for (Command *c = cmd->cmd, *next; c; c = next) {
    859 		next = c->next;
    860 		command_free(c);
    861 	}
    862 
    863 	for (const char **args = cmd->argv; *args; args++)
    864 		free((void*)*args);
    865 	address_free(cmd->address);
    866 	text_regex_free(cmd->regex);
    867 	free(cmd);
    868 }
    869 
    870 static const CommandDef *command_lookup(Vis *vis, const char *name) {
    871 	return map_closest(vis->cmds, name);
    872 }
    873 
    874 static Command *command_parse(Vis *vis, const char **s, enum SamError *err) {
    875 	if (!**s) {
    876 		*err = SAM_ERR_COMMAND;
    877 		return NULL;
    878 	}
    879 	Command *cmd = command_new(NULL);
    880 	if (!cmd)
    881 		return NULL;
    882 
    883 	cmd->address = address_parse_compound(vis, s, err);
    884 	skip_spaces(s);
    885 
    886 	cmd->argv[0] = parse_cmdname(s);
    887 
    888 	if (!cmd->argv[0]) {
    889 		char name[2] = { **s ? **s : 'p', '\0' };
    890 		if (**s)
    891 			(*s)++;
    892 		if (!(cmd->argv[0] = strdup(name)))
    893 			goto fail;
    894 	}
    895 
    896 	const CommandDef *cmddef = command_lookup(vis, cmd->argv[0]);
    897 	if (!cmddef) {
    898 		*err = SAM_ERR_COMMAND;
    899 		goto fail;
    900 	}
    901 
    902 	cmd->cmddef = cmddef;
    903 
    904 	if (strcmp(cmd->argv[0], "{") == 0) {
    905 		Command *prev = NULL, *next;
    906 		int level = vis->nesting_level++;
    907 		do {
    908 			while (**s == ' ' || **s == '\t' || **s == '\n')
    909 				(*s)++;
    910 			next = command_parse(vis, s, err);
    911 			if (*err)
    912 				goto fail;
    913 			if (prev)
    914 				prev->next = next;
    915 			else
    916 				cmd->cmd = next;
    917 		} while ((prev = next));
    918 		if (level != vis->nesting_level) {
    919 			*err = SAM_ERR_UNMATCHED_BRACE;
    920 			goto fail;
    921 		}
    922 	} else if (strcmp(cmd->argv[0], "}") == 0) {
    923 		if (vis->nesting_level-- == 0) {
    924 			*err = SAM_ERR_UNMATCHED_BRACE;
    925 			goto fail;
    926 		}
    927 		command_free(cmd);
    928 		return NULL;
    929 	}
    930 
    931 	if (cmddef->flags & CMD_ADDRESS_NONE && cmd->address) {
    932 		*err = SAM_ERR_NO_ADDRESS;
    933 		goto fail;
    934 	}
    935 
    936 	if (cmddef->flags & CMD_FORCE && **s == '!') {
    937 		cmd->flags = '!';
    938 		(*s)++;
    939 	}
    940 
    941 	if ((cmddef->flags & CMD_COUNT) && (*err = parse_count(s, &cmd->count)))
    942 		goto fail;
    943 
    944 	if (cmddef->flags & CMD_REGEX) {
    945 		if ((cmddef->flags & CMD_REGEX_DEFAULT) && (!**s || **s == ' ')) {
    946 			skip_spaces(s);
    947 		} else {
    948 			const char *before = *s;
    949 			cmd->regex = parse_regex(vis, s);
    950 			if (!cmd->regex && (*s != before || !(cmddef->flags & CMD_COUNT))) {
    951 				*err = SAM_ERR_REGEX;
    952 				goto fail;
    953 			}
    954 		}
    955 	}
    956 
    957 	if (cmddef->flags & CMD_SHELL && !(cmd->argv[1] = parse_shellcmd(vis, s))) {
    958 		*err = SAM_ERR_SHELL;
    959 		goto fail;
    960 	}
    961 
    962 	if (cmddef->flags & CMD_TEXT && !(cmd->argv[1] = parse_text(s, &cmd->count))) {
    963 		*err = SAM_ERR_TEXT;
    964 		goto fail;
    965 	}
    966 
    967 	if (cmddef->flags & CMD_ARGV) {
    968 		parse_argv(s, &cmd->argv[1], MAX_ARGV-2);
    969 		cmd->argv[MAX_ARGV-1] = NULL;
    970 	}
    971 
    972 	if (cmddef->flags & CMD_CMD) {
    973 		skip_spaces(s);
    974 		if (cmddef->defcmd && (**s == '\n' || **s == '}' || **s == '\0')) {
    975 			if (**s == '\n')
    976 				(*s)++;
    977 			if (!(cmd->cmd = command_new(cmddef->defcmd)))
    978 				goto fail;
    979 			cmd->cmd->cmddef = command_lookup(vis, cmddef->defcmd);
    980 		} else {
    981 			if (!(cmd->cmd = command_parse(vis, s, err)))
    982 				goto fail;
    983 			if (strcmp(cmd->argv[0], "X") == 0 || strcmp(cmd->argv[0], "Y") == 0) {
    984 				Command *sel = command_new("select");
    985 				if (!sel)
    986 					goto fail;
    987 				sel->cmd = cmd->cmd;
    988 				sel->cmddef = &cmddef_select;
    989 				cmd->cmd = sel;
    990 			}
    991 		}
    992 	}
    993 
    994 	return cmd;
    995 fail:
    996 	command_free(cmd);
    997 	return NULL;
    998 }
    999 
   1000 static Command *sam_parse(Vis *vis, const char *cmd, enum SamError *err) {
   1001 	vis->nesting_level = 0;
   1002 	const char **s = &cmd;
   1003 	Command *c = command_parse(vis, s, err);
   1004 	if (!c)
   1005 		return NULL;
   1006 	while (**s == ' ' || **s == '\t' || **s == '\n')
   1007 		(*s)++;
   1008 	if (**s) {
   1009 		*err = SAM_ERR_NEWLINE;
   1010 		command_free(c);
   1011 		return NULL;
   1012 	}
   1013 
   1014 	Command *sel = command_new("select");
   1015 	if (!sel) {
   1016 		command_free(c);
   1017 		return NULL;
   1018 	}
   1019 	sel->cmd = c;
   1020 	sel->cmddef = &cmddef_select;
   1021 	return sel;
   1022 }
   1023 
   1024 static Filerange address_line_evaluate(Address *addr, File *file, Filerange *range, int sign) {
   1025 	Text *txt = file->text;
   1026 	size_t offset = addr->number != EPOS ? addr->number : 1;
   1027 	size_t start = range->start, end = range->end, line;
   1028 	if (sign > 0) {
   1029 		char c;
   1030 		if (start < end && text_byte_get(txt, end-1, &c) && c == '\n')
   1031 			end--;
   1032 		line = text_lineno_by_pos(txt, end);
   1033 		line = text_pos_by_lineno(txt, line + offset);
   1034 	} else if (sign < 0) {
   1035 		line = text_lineno_by_pos(txt, start);
   1036 		line = offset < line ? text_pos_by_lineno(txt, line - offset) : 0;
   1037 	} else {
   1038 		if (addr->number == 0)
   1039 			return text_range_new(0, 0);
   1040 		line = text_pos_by_lineno(txt, addr->number);
   1041 	}
   1042 
   1043 	if (addr->type == 'g')
   1044 		return text_range_new(line, line);
   1045 	else
   1046 		return text_range_new(line, text_line_next(txt, line));
   1047 }
   1048 
   1049 static Filerange address_evaluate(Address *addr, File *file, Selection *sel, Filerange *range, int sign) {
   1050 	Filerange ret = text_range_empty();
   1051 
   1052 	do {
   1053 		switch (addr->type) {
   1054 		case '#':
   1055 			if (sign > 0)
   1056 				ret.start = ret.end = range->end + addr->number;
   1057 			else if (sign < 0)
   1058 				ret.start = ret.end = range->start - addr->number;
   1059 			else
   1060 				ret = text_range_new(addr->number, addr->number);
   1061 			break;
   1062 		case 'l':
   1063 		case 'g':
   1064 			ret = address_line_evaluate(addr, file, range, sign);
   1065 			break;
   1066 		case '\'':
   1067 		{
   1068 			size_t pos = EPOS;
   1069 			Array *marks = &file->marks[addr->number];
   1070 			size_t idx = sel ? view_selections_number(sel) : 0;
   1071 			SelectionRegion *sr = array_get(marks, idx);
   1072 			if (sr)
   1073 				pos = text_mark_get(file->text, sr->cursor);
   1074 			ret = text_range_new(pos, pos);
   1075 			break;
   1076 		}
   1077 		case '?':
   1078 			sign = sign == 0 ? -1 : -sign;
   1079 			/* fall through */
   1080 		case '/':
   1081 			if (sign >= 0)
   1082 				ret = text_object_search_forward(file->text, range->end, addr->regex);
   1083 			else
   1084 				ret = text_object_search_backward(file->text, range->start, addr->regex);
   1085 			break;
   1086 		case '$':
   1087 		{
   1088 			size_t size = text_size(file->text);
   1089 			ret = text_range_new(size, size);
   1090 			break;
   1091 		}
   1092 		case '.':
   1093 			ret = *range;
   1094 			break;
   1095 		case '+':
   1096 		case '-':
   1097 			sign = addr->type == '+' ? +1 : -1;
   1098 			if (!addr->right || addr->right->type == '+' || addr->right->type == '-')
   1099 				ret = address_line_evaluate(addr, file, range, sign);
   1100 			break;
   1101 		case ',':
   1102 		case ';':
   1103 		{
   1104 			Filerange left, right;
   1105 			if (addr->left)
   1106 				left = address_evaluate(addr->left, file, sel, range, 0);
   1107 			else
   1108 				left = text_range_new(0, 0);
   1109 
   1110 			if (addr->type == ';')
   1111 				range = &left;
   1112 
   1113 			if (addr->right) {
   1114 				right = address_evaluate(addr->right, file, sel, range, 0);
   1115 			} else {
   1116 				size_t size = text_size(file->text);
   1117 				right = text_range_new(size, size);
   1118 			}
   1119 			/* TODO: enforce strict ordering? */
   1120 			return text_range_union(&left, &right);
   1121 		}
   1122 		case '%':
   1123 			return text_range_new(0, text_size(file->text));
   1124 		}
   1125 		if (text_range_valid(&ret))
   1126 			range = &ret;
   1127 	} while ((addr = addr->right));
   1128 
   1129 	return ret;
   1130 }
   1131 
   1132 static bool count_evaluate(Command *cmd) {
   1133 	Count *count = &cmd->count;
   1134 	if (count->mod)
   1135 		return count->start ? cmd->iteration % count->start == 0 : true;
   1136 	return count->start <= cmd->iteration && cmd->iteration <= count->end;
   1137 }
   1138 
   1139 static bool sam_execute(Vis *vis, Win *win, Command *cmd, Selection *sel, Filerange *range) {
   1140 	bool ret = true;
   1141 	if (cmd->address && win)
   1142 		*range = address_evaluate(cmd->address, win->file, sel, range, 0);
   1143 
   1144 	cmd->iteration++;
   1145 	switch (cmd->argv[0][0]) {
   1146 	case '{':
   1147 	{
   1148 		for (Command *c = cmd->cmd; c && ret; c = c->next)
   1149 			ret &= sam_execute(vis, win, c, NULL, range);
   1150 		view_selections_dispose_force(sel);
   1151 		break;
   1152 	}
   1153 	default:
   1154 		ret = cmd->cmddef->func(vis, win, cmd, cmd->argv, sel, range);
   1155 		break;
   1156 	}
   1157 	return ret;
   1158 }
   1159 
   1160 static enum SamError validate(Command *cmd, bool loop, bool group) {
   1161 	if (cmd->cmddef->flags & CMD_DESTRUCTIVE) {
   1162 		if (loop)
   1163 			return SAM_ERR_LOOP_INVALID_CMD;
   1164 		if (group)
   1165 			return SAM_ERR_GROUP_INVALID_CMD;
   1166 	}
   1167 
   1168 	group |= (cmd->cmddef->flags & CMD_GROUP);
   1169 	loop  |= (cmd->cmddef->flags & CMD_LOOP);
   1170 	for (Command *c = cmd->cmd; c; c = c->next) {
   1171 		enum SamError err = validate(c, loop, group);
   1172 		if (err != SAM_ERR_OK)
   1173 			return err;
   1174 	}
   1175 	return SAM_ERR_OK;
   1176 }
   1177 
   1178 static enum SamError command_validate(Command *cmd) {
   1179 	return validate(cmd, false, false);
   1180 }
   1181 
   1182 static bool count_negative(Command *cmd) {
   1183 	if (cmd->count.start < 0 || cmd->count.end < 0)
   1184 		return true;
   1185 	for (Command *c = cmd->cmd; c; c = c->next) {
   1186 		if (c->cmddef->func != cmd_extract && c->cmddef->func != cmd_select) {
   1187 			if (count_negative(c))
   1188 				return true;
   1189 		}
   1190 	}
   1191 	return false;
   1192 }
   1193 
   1194 static void count_init(Command *cmd, int max) {
   1195 	Count *count = &cmd->count;
   1196 	cmd->iteration = 0;
   1197 	if (count->start < 0)
   1198 		count->start += max;
   1199 	if (count->end < 0)
   1200 		count->end += max;
   1201 	for (Command *c = cmd->cmd; c; c = c->next) {
   1202 		if (c->cmddef->func != cmd_extract && c->cmddef->func != cmd_select)
   1203 			count_init(c, max);
   1204 	}
   1205 }
   1206 
   1207 enum SamError sam_cmd(Vis *vis, const char *s) {
   1208 	enum SamError err = SAM_ERR_OK;
   1209 	if (!s)
   1210 		return err;
   1211 
   1212 	Command *cmd = sam_parse(vis, s, &err);
   1213 	if (!cmd) {
   1214 		if (err == SAM_ERR_OK)
   1215 			err = SAM_ERR_MEMORY;
   1216 		return err;
   1217 	}
   1218 
   1219 	err = command_validate(cmd);
   1220 	if (err != SAM_ERR_OK) {
   1221 		command_free(cmd);
   1222 		return err;
   1223 	}
   1224 
   1225 	for (File *file = vis->files; file; file = file->next) {
   1226 		if (file->internal)
   1227 			continue;
   1228 		sam_transcript_init(&file->transcript);
   1229 	}
   1230 
   1231 	bool visual = vis->mode->visual;
   1232 	size_t primary_pos = vis->win ? view_cursor_get(&vis->win->view) : EPOS;
   1233 	Filerange range = text_range_empty();
   1234 	sam_execute(vis, vis->win, cmd, NULL, &range);
   1235 
   1236 	for (File *file = vis->files; file; file = file->next) {
   1237 		if (file->internal)
   1238 			continue;
   1239 		Transcript *t = &file->transcript;
   1240 		if (t->error != SAM_ERR_OK) {
   1241 			err = t->error;
   1242 			sam_transcript_free(t);
   1243 			continue;
   1244 		}
   1245 		vis_file_snapshot(vis, file);
   1246 		ptrdiff_t delta = 0;
   1247 		for (Change *c = t->changes; c; c = c->next) {
   1248 			c->range.start += delta;
   1249 			c->range.end += delta;
   1250 			if (c->type & TRANSCRIPT_DELETE) {
   1251 				text_delete_range(file->text, &c->range);
   1252 				delta -= text_range_size(&c->range);
   1253 				if (c->sel && c->type == TRANSCRIPT_DELETE) {
   1254 					if (visual)
   1255 						view_selections_dispose_force(c->sel);
   1256 					else
   1257 						view_cursors_to(c->sel, c->range.start);
   1258 				}
   1259 			}
   1260 			if (c->type & TRANSCRIPT_INSERT) {
   1261 				for (int i = 0; i < c->count; i++) {
   1262 					text_insert(file->text, c->range.start, c->data, c->len);
   1263 					delta += c->len;
   1264 				}
   1265 				Filerange r = text_range_new(c->range.start,
   1266 				                             c->range.start + c->len * c->count);
   1267 				if (c->sel) {
   1268 					if (visual) {
   1269 						view_selections_set(c->sel, &r);
   1270 						c->sel->anchored = true;
   1271 					} else {
   1272 						if (memchr(c->data, '\n', c->len))
   1273 							view_cursors_to(c->sel, r.start);
   1274 						else
   1275 							view_cursors_to(c->sel, r.end);
   1276 					}
   1277 				} else if (visual) {
   1278 					Selection *sel = view_selections_new(&c->win->view, r.start);
   1279 					if (sel) {
   1280 						view_selections_set(sel, &r);
   1281 						sel->anchored = true;
   1282 					}
   1283 				}
   1284 			}
   1285 		}
   1286 		sam_transcript_free(&file->transcript);
   1287 		vis_file_snapshot(vis, file);
   1288 	}
   1289 
   1290 	for (Win *win = vis->windows; win; win = win->next)
   1291 		view_selections_normalize(&win->view);
   1292 
   1293 	if (vis->win) {
   1294 		if (primary_pos != EPOS && view_selection_disposed(&vis->win->view))
   1295 			view_cursors_to(vis->win->view.selection, primary_pos);
   1296 		view_selections_primary_set(view_selections(&vis->win->view));
   1297 		vis_jumplist_save(vis);
   1298 		bool completed = true;
   1299 		for (Selection *s = view_selections(&vis->win->view); s; s = view_selections_next(s)) {
   1300 			if (s->anchored) {
   1301 				completed = false;
   1302 				break;
   1303 			}
   1304 		}
   1305 		vis_mode_switch(vis, completed ? VIS_MODE_NORMAL : VIS_MODE_VISUAL);
   1306 	}
   1307 	command_free(cmd);
   1308 	return err;
   1309 }
   1310 
   1311 /* process text input, substitute register content for backreferences etc. */
   1312 Buffer text(Vis *vis, const char *text) {
   1313 	Buffer buf = {0};
   1314 	for (size_t len = strcspn(text, "\\&"); *text; len = strcspn(++text, "\\&")) {
   1315 		buffer_append(&buf, text, len);
   1316 		text += len;
   1317 		enum VisRegister regid = VIS_REG_INVALID;
   1318 		switch (text[0]) {
   1319 		case '&':
   1320 			regid = VIS_REG_AMPERSAND;
   1321 			break;
   1322 		case '\\':
   1323 			if ('1' <= text[1] && text[1] <= '9') {
   1324 				regid = VIS_REG_1 + text[1] - '1';
   1325 				text++;
   1326 			} else if (text[1] == '\\' || text[1] == '&') {
   1327 				text++;
   1328 			}
   1329 			break;
   1330 		case '\0':
   1331 			goto out;
   1332 		}
   1333 
   1334 		const char *data;
   1335 		size_t reglen = 0;
   1336 		if (regid != VIS_REG_INVALID) {
   1337 			data = register_get(vis, &vis->registers[regid], &reglen);
   1338 		} else {
   1339 			data = text;
   1340 			reglen = 1;
   1341 		}
   1342 		buffer_append(&buf, data, reglen);
   1343 	}
   1344 out:
   1345 	return buf;
   1346 }
   1347 
   1348 static bool cmd_insert(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1349 	if (!win)
   1350 		return false;
   1351 	Buffer buf = text(vis, argv[1]);
   1352 	bool ret = sam_insert(win, sel, range->start, buf.data, buf.len, cmd->count.start);
   1353 	if (!ret)
   1354 		free(buf.data);
   1355 	return ret;
   1356 }
   1357 
   1358 static bool cmd_append(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 	bool ret = sam_insert(win, sel, range->end, buf.data, buf.len, cmd->count.start);
   1363 	if (!ret)
   1364 		free(buf.data);
   1365 	return ret;
   1366 }
   1367 
   1368 static bool cmd_change(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1369 	if (!win)
   1370 		return false;
   1371 	Buffer buf = text(vis, argv[1]);
   1372 	bool ret = sam_change(win, sel, range, buf.data, buf.len, cmd->count.start);
   1373 	if (!ret)
   1374 		free(buf.data);
   1375 	return ret;
   1376 }
   1377 
   1378 static bool cmd_delete(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1379 	return win && sam_delete(win, sel, range);
   1380 }
   1381 
   1382 static bool cmd_guard(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1383 	if (!win)
   1384 		return false;
   1385 	bool match = false;
   1386 	RegexMatch captures[1];
   1387 	size_t len = text_range_size(range);
   1388 	if (!cmd->regex)
   1389 		match = true;
   1390 	else if (!text_search_range_forward(win->file->text, range->start, len, cmd->regex, 1, captures, 0))
   1391 		match = captures[0].start < range->end;
   1392 	if ((count_evaluate(cmd) && match) ^ (argv[0][0] == 'v'))
   1393 		return sam_execute(vis, win, cmd->cmd, sel, range);
   1394 	view_selections_dispose_force(sel);
   1395 	return true;
   1396 }
   1397 
   1398 static int extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range, bool simulate) {
   1399 	bool ret = true;
   1400 	int count = 0;
   1401 	Text *txt = win->file->text;
   1402 
   1403 	if (cmd->regex) {
   1404 		size_t start = range->start, end = range->end;
   1405 		size_t last_start = argv[0][0] == 'x' ? EPOS : start;
   1406 		size_t nsub = 1 + text_regex_nsub(cmd->regex);
   1407 		if (nsub > MAX_REGEX_SUB)
   1408 			nsub = MAX_REGEX_SUB;
   1409 		RegexMatch match[MAX_REGEX_SUB];
   1410 		while (start <= end) {
   1411 			char c;
   1412 			int flags = start > range->start &&
   1413 			            text_byte_get(txt, start - 1, &c) && c != '\n' ?
   1414 			            REG_NOTBOL : 0;
   1415 			bool found = !text_search_range_forward(txt, start, end - start,
   1416 			                                        cmd->regex, nsub, match,
   1417 			                                        flags);
   1418 			Filerange r = text_range_empty();
   1419 			if (found) {
   1420 				if (argv[0][0] == 'x')
   1421 					r = text_range_new(match[0].start, match[0].end);
   1422 				else
   1423 					r = text_range_new(last_start, match[0].start);
   1424 				if (match[0].start == match[0].end) {
   1425 					if (last_start == match[0].start) {
   1426 						start++;
   1427 						continue;
   1428 					}
   1429 					/* in Plan 9's regexp library ^ matches the beginning
   1430 					 * of a line, however in POSIX with REG_NEWLINE ^
   1431 					 * matches the zero-length string immediately after a
   1432 					 * newline. Try filtering out the last such match at EOF.
   1433 					 */
   1434 					if (end == match[0].start && start > range->start &&
   1435 					    text_byte_get(txt, end-1, &c) && c == '\n')
   1436 						break;
   1437 					start = match[0].end + 1;
   1438 				} else {
   1439 					start = match[0].end;
   1440 				}
   1441 			} else {
   1442 				if (argv[0][0] == 'y')
   1443 					r = text_range_new(start, end);
   1444 				start = end + 1;
   1445 			}
   1446 
   1447 			if (text_range_valid(&r)) {
   1448 				if (found) {
   1449 					for (size_t i = 0; i < nsub; i++) {
   1450 						Register *reg = &vis->registers[VIS_REG_AMPERSAND+i];
   1451 						register_put_range(vis, reg, txt, &match[i]);
   1452 					}
   1453 					last_start = match[0].end;
   1454 				} else {
   1455 					last_start = start;
   1456 				}
   1457 				if (simulate)
   1458 					count++;
   1459 				else
   1460 					ret &= sam_execute(vis, win, cmd->cmd, NULL, &r);
   1461 			}
   1462 		}
   1463 	} else {
   1464 		size_t start = range->start, end = range->end;
   1465 		while (start < end) {
   1466 			size_t next = text_line_next(txt, start);
   1467 			if (next > end)
   1468 				next = end;
   1469 			Filerange r = text_range_new(start, next);
   1470 			if (start == next || !text_range_valid(&r))
   1471 				break;
   1472 			if (simulate)
   1473 				count++;
   1474 			else
   1475 				ret &= sam_execute(vis, win, cmd->cmd, NULL, &r);
   1476 			start = next;
   1477 		}
   1478 	}
   1479 
   1480 	if (!simulate)
   1481 		view_selections_dispose_force(sel);
   1482 	return simulate ? count : ret;
   1483 }
   1484 
   1485 static bool cmd_extract(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1486 	if (!win || !text_range_valid(range))
   1487 		return false;
   1488 	int matches = 0;
   1489 	if (count_negative(cmd->cmd))
   1490 		matches = extract(vis, win, cmd, argv, sel, range, true);
   1491 	count_init(cmd->cmd, matches+1);
   1492 	return extract(vis, win, cmd, argv, sel, range, false);
   1493 }
   1494 
   1495 static bool cmd_select(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1496 	Filerange r = text_range_empty();
   1497 	if (!win)
   1498 		return sam_execute(vis, NULL, cmd->cmd, NULL, &r);
   1499 	bool ret = true;
   1500 	View *view = &win->view;
   1501 	Text *txt = win->file->text;
   1502 	bool multiple_cursors = view->selection_count > 1;
   1503 	Selection *primary = view_selections_primary_get(view);
   1504 
   1505 	if (vis->mode->visual)
   1506 		count_init(cmd->cmd, view->selection_count + 1);
   1507 
   1508 	for (Selection *s = view_selections(view), *next; s && ret; s = next) {
   1509 		next = view_selections_next(s);
   1510 		size_t pos = view_cursors_pos(s);
   1511 		if (vis->mode->visual) {
   1512 			r = view_selections_get(s);
   1513 		} else if (cmd->cmd->address) {
   1514 			/* convert a single line range to a goto line motion */
   1515 			if (!multiple_cursors && cmd->cmd->cmddef->func == cmd_print) {
   1516 				Address *addr = cmd->cmd->address;
   1517 				switch (addr->type) {
   1518 				case '+':
   1519 				case '-':
   1520 					addr = addr->right;
   1521 					/* fall through */
   1522 				case 'l':
   1523 					if (addr && addr->type == 'l' && !addr->right)
   1524 						addr->type = 'g';
   1525 					break;
   1526 				}
   1527 			}
   1528 			r = text_range_new(pos, pos);
   1529 		} else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_POS) {
   1530 			r = text_range_new(pos, pos);
   1531 		} else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_LINE) {
   1532 			r = text_object_line(txt, pos);
   1533 		} else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_AFTER) {
   1534 			size_t next_line = text_line_next(txt, pos);
   1535 			r = text_range_new(next_line, next_line);
   1536 		} else if (cmd->cmd->cmddef->flags & CMD_ADDRESS_ALL) {
   1537 			r = text_range_new(0, text_size(txt));
   1538 		} else if (!multiple_cursors && (cmd->cmd->cmddef->flags & CMD_ADDRESS_ALL_1CURSOR)) {
   1539 			r = text_range_new(0, text_size(txt));
   1540 		} else {
   1541 			r = text_range_new(pos, text_char_next(txt, pos));
   1542 		}
   1543 		if (!text_range_valid(&r))
   1544 			r = text_range_new(0, 0);
   1545 		ret &= sam_execute(vis, win, cmd->cmd, s, &r);
   1546 		if (cmd->cmd->cmddef->flags & CMD_ONCE)
   1547 			break;
   1548 	}
   1549 
   1550 	if (vis->win && &vis->win->view == view && primary != view_selections_primary_get(view))
   1551 		view_selections_primary_set(view_selections(view));
   1552 	return ret;
   1553 }
   1554 
   1555 static bool cmd_print(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1556 	if (!win || !text_range_valid(range))
   1557 		return false;
   1558 	if (!sel)
   1559 		sel = view_selections_new_force(&win->view, range->start);
   1560 	if (!sel)
   1561 		return false;
   1562 	if (range->start != range->end) {
   1563 		view_selections_set(sel, range);
   1564 		sel->anchored = true;
   1565 	} else {
   1566 		view_cursors_to(sel, range->start);
   1567 		view_selection_clear(sel);
   1568 	}
   1569 	return true;
   1570 }
   1571 
   1572 static bool cmd_files(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1573 	bool ret = true;
   1574 	for (Win *wn, *w = vis->windows; w; w = wn) {
   1575 		/* w can get freed by sam_execute() so store w->next early */
   1576 		wn = w->next;
   1577 		if (w->file->internal)
   1578 			continue;
   1579 		bool match = !cmd->regex ||
   1580 		             (w->file->name && text_regex_match(cmd->regex, w->file->name, 0) == 0);
   1581 		if (match ^ (argv[0][0] == 'Y')) {
   1582 			Filerange def = text_range_new(0, 0);
   1583 			ret &= sam_execute(vis, w, cmd->cmd, NULL, &def);
   1584 		}
   1585 	}
   1586 	return ret;
   1587 }
   1588 
   1589 static bool cmd_substitute(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1590 	vis_info_show(vis, "Use :x/pattern/ c/replacement/ instead");
   1591 	return false;
   1592 }
   1593 
   1594 /* cmd_write stores win->file's contents end emits pre/post events.
   1595  * If the range r covers the whole file, it is updated to account for
   1596  * potential file's text mutation by a FILE_SAVE_PRE callback.
   1597  */
   1598 static bool cmd_write(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *r) {
   1599 	if (!win)
   1600 		return false;
   1601 
   1602 	File *file = win->file;
   1603 	if (sam_transcript_error(&file->transcript, SAM_ERR_WRITE_CONFLICT))
   1604 		return false;
   1605 
   1606 	Text *text = file->text;
   1607 	Filerange range_all = text_range_new(0, text_size(text));
   1608 	bool write_entire_file = text_range_equal(r, &range_all);
   1609 
   1610 	const char *filename = argv[1];
   1611 	if (!filename)
   1612 		filename = file->name;
   1613 	if (!filename) {
   1614 		if (file->fd == -1) {
   1615 			vis_info_show(vis, "Filename expected");
   1616 			return false;
   1617 		}
   1618 		if (!strchr(argv[0], 'q')) {
   1619 			vis_info_show(vis, "No filename given, use 'wq' to write to stdout");
   1620 			return false;
   1621 		}
   1622 
   1623 		if (!vis_event_emit(vis, VIS_EVENT_FILE_SAVE_PRE, file, (char*)NULL) && cmd->flags != '!') {
   1624 			vis_info_show(vis, "Rejected write to stdout by pre-save hook");
   1625 			return false;
   1626 		}
   1627 		/* a pre-save hook may have changed the text; need to re-take the range */
   1628 		if (write_entire_file)
   1629 			*r = text_range_new(0, text_size(text));
   1630 
   1631 		bool visual = vis->mode->visual;
   1632 
   1633 		for (Selection *s = view_selections(&win->view); s; s = view_selections_next(s)) {
   1634 			Filerange range = visual ? view_selections_get(s) : *r;
   1635 			ssize_t written = text_write_range(text, &range, file->fd);
   1636 			if (written == -1 || (size_t)written != text_range_size(&range)) {
   1637 				vis_info_show(vis, "Can not write to stdout");
   1638 				return false;
   1639 			}
   1640 			if (!visual)
   1641 				break;
   1642 		}
   1643 
   1644 		/* make sure the file is marked as saved i.e. not modified */
   1645 		text_save(text, NULL);
   1646 		vis_event_emit(vis, VIS_EVENT_FILE_SAVE_POST, file, (char*)NULL);
   1647 		return true;
   1648 	}
   1649 
   1650 	if (!argv[1] && cmd->flags != '!') {
   1651 		if (vis->mode->visual) {
   1652 			vis_info_show(vis, "WARNING: file will be reduced to active selection");
   1653 			return false;
   1654 		}
   1655 		if (!write_entire_file) {
   1656 			vis_info_show(vis, "WARNING: file will be reduced to provided range");
   1657 			return false;
   1658 		}
   1659 	}
   1660 
   1661 	for (const char **name = argv[1] ? &argv[1] : (const char*[]){ filename, NULL }; *name; name++) {
   1662 
   1663 		char *path = absolute_path(*name);
   1664 		if (!path)
   1665 			return false;
   1666 
   1667 		struct stat meta;
   1668 		bool existing_file = !stat(path, &meta);
   1669 		bool same_file = existing_file && file->name &&
   1670 		                 file->stat.st_dev == meta.st_dev && file->stat.st_ino == meta.st_ino;
   1671 
   1672 		if (cmd->flags != '!') {
   1673 			if (same_file && file->stat.st_mtime && file->stat.st_mtime < meta.st_mtime) {
   1674 				vis_info_show(vis, "WARNING: file has been changed since reading it");
   1675 				goto err;
   1676 			}
   1677 			if (existing_file && !same_file) {
   1678 				vis_info_show(vis, "WARNING: file exists");
   1679 				goto err;
   1680 			}
   1681 		}
   1682 
   1683 		if (!vis_event_emit(vis, VIS_EVENT_FILE_SAVE_PRE, file, path) && cmd->flags != '!') {
   1684 			vis_info_show(vis, "Rejected write to `%s' by pre-save hook", path);
   1685 			goto err;
   1686 		}
   1687 		/* a pre-save hook may have changed the text; need to re-take the range */
   1688 		if (write_entire_file)
   1689 			*r = text_range_new(0, text_size(text));
   1690 
   1691 		TextSave *ctx = text_save_begin(text, AT_FDCWD, path, file->save_method);
   1692 		if (!ctx) {
   1693 			const char *msg = errno ? strerror(errno) : "try changing `:set savemethod`";
   1694 			vis_info_show(vis, "Can't write `%s': %s", path, msg);
   1695 			goto err;
   1696 		}
   1697 
   1698 		bool failure = false;
   1699 		bool visual = vis->mode->visual;
   1700 
   1701 		for (Selection *s = view_selections(&win->view); s; s = view_selections_next(s)) {
   1702 			Filerange range = visual ? view_selections_get(s) : *r;
   1703 			ssize_t written = text_save_write_range(ctx, &range);
   1704 			failure = (written == -1 || (size_t)written != text_range_size(&range));
   1705 			if (failure) {
   1706 				text_save_cancel(ctx);
   1707 				break;
   1708 			}
   1709 
   1710 			if (!visual)
   1711 				break;
   1712 		}
   1713 
   1714 		if (failure || !text_save_commit(ctx)) {
   1715 			vis_info_show(vis, "Can't write `%s': %s", path, strerror(errno));
   1716 			goto err;
   1717 		}
   1718 
   1719 		if (!file->name) {
   1720 			file_name_set(file, path);
   1721 			same_file = true;
   1722 		}
   1723 		if (same_file || (!existing_file && strcmp(file->name, path) == 0))
   1724 			file->stat = text_stat(text);
   1725 		vis_event_emit(vis, VIS_EVENT_FILE_SAVE_POST, file, path);
   1726 		free(path);
   1727 		continue;
   1728 
   1729 	err:
   1730 		free(path);
   1731 		return false;
   1732 	}
   1733 	return true;
   1734 }
   1735 
   1736 static bool cmd_filter(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1737 	if (!win)
   1738 		return false;
   1739 
   1740 	Buffer bufout = {0}, buferr = {0};
   1741 
   1742 	int status = vis_pipe(vis, win->file, range, &argv[1], &bufout, read_into_buffer, &buferr,
   1743 	                      read_into_buffer, false);
   1744 
   1745 	if (vis->interrupted) {
   1746 		vis_info_show(vis, "Command cancelled");
   1747 	} else if (status == 0) {
   1748 		char *data  = bufout.data;
   1749 		bufout.data = 0;
   1750 		if (!sam_change(win, sel, range, data, bufout.len, 1))
   1751 			free(data);
   1752 	} else {
   1753 		vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
   1754 	}
   1755 
   1756 	buffer_release(&bufout);
   1757 	buffer_release(&buferr);
   1758 
   1759 	return !vis->interrupted && status == 0;
   1760 }
   1761 
   1762 static bool cmd_launch(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1763 	Filerange invalid = text_range_new(sel ? view_cursors_pos(sel) : range->start, EPOS);
   1764 	return cmd_filter(vis, win, cmd, argv, sel, &invalid);
   1765 }
   1766 
   1767 static bool cmd_pipein(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1768 	if (!win)
   1769 		return false;
   1770 	Filerange filter_range = text_range_new(range->end, range->end);
   1771 	bool ret = cmd_filter(vis, win, cmd, argv, sel, &filter_range);
   1772 	if (ret)
   1773 		ret = sam_delete(win, NULL, range);
   1774 	return ret;
   1775 }
   1776 
   1777 static bool cmd_pipeout(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1778 	if (!win)
   1779 		return false;
   1780 	Buffer buferr = {0};
   1781 
   1782 	int status = vis_pipe(vis, win->file, range, (const char*[]){ argv[1], NULL }, NULL, NULL,
   1783 	                      &buferr, read_into_buffer, false);
   1784 
   1785 	if (vis->interrupted)
   1786 		vis_info_show(vis, "Command cancelled");
   1787 	else if (status != 0)
   1788 		vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
   1789 
   1790 	buffer_release(&buferr);
   1791 
   1792 	return !vis->interrupted && status == 0;
   1793 }
   1794 
   1795 static bool cmd_cd(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
   1796 	const char *dir = argv[1];
   1797 	if (!dir)
   1798 		dir = getenv("HOME");
   1799 	return dir && chdir(dir) == 0;
   1800 }
   1801 
   1802 #include "vis-cmds.c"