shithub: purgatorio

ref: a411870ee4640241e3c494367d922847da84f972
dir: /appl/wm/vixen/vixen.b/

View raw version
implement Vixen;

include "sys.m";
	sys: Sys;
	sprint: import sys;
include "draw.m";
	draw: Draw;
include "arg.m";
include "bufio.m";
	bufio: Bufio;
	Iobuf: import bufio;
include "string.m";
	str: String;
include "tk.m";
	tk: Tk;
include "tkclient.m";
	tkclient: Tkclient;
include "keyboard.m";
	kb: Keyboard;
include "regex.m";
	regex: Regex;
include "plumbmsg.m";
	plumbmsg: Plumbmsg;
	Msg: import plumbmsg;
include "names.m";
	names: Names;
include "sh.m";
	sh: Sh;

include "vixen/buffers.b";
include "vixen/change.b";
include "vixen/cmd.b";
include "vixen/ex.b";
include "vixen/filter.b";
include "vixen/interp.b";
include "vixen/misc.b";
include "vixen/subs.b";

Vixen: module {
	init:	fn(ctxt: ref Draw->Context, argv: list of string);
};


iflag: int;

# 't' for tk events
# 'k' for tk commands
# 'e' for edit
# 'x' for ex
# 'i' for interp (insert/replace, command, visual, move)
# 'd' for misc
# 'c' for cursor
# 'u' for change (undo)
# 'm' for modifications (textdel, textinsert)
debug := array[128] of {* => int 0};
startupmacro: string;  # macro to interpret at startup after opening the file

Insert, Replace, Command0, Visual, Visualline: con iota;  # modes
modes := array[] of {"insert", "replace", "command", "visual", "visual line"};


# parameter "rec" to textdel & textins.
Cnone, Cmod, Cmodrepl, Cchange, Cchangerepl,	# how to record as change, for undo
Csetcursorlo, Csetcursorhi,			# where (if) to set cursor
Csetreg: con 1<<iota;				# whether to set "last change" register
Cchangemask: con Csetcursorlo-1;
Csetcursormask: con Csetcursorlo|Csetcursorhi;

mode: int;
visualstart: ref Cursor;  # start of visual select, non-nil when mode == Visual or Visualline
visualend: ref Cursor;  # end of visual select
cmdcur: ref Cmd;  # current command
cmdprev: ref Cmd;  # previous (completed) command, for '.'
recordreg := -1;  # register currently recording to, < 0 when not recording
record: string;  # chars typed while recording, set to register when done
colsnap := -1;  # column to snap vertical movements to.  only valid >= 0

filename: string;  # may be nil initially
filefd: ref Sys->FD;  # may be nil initially
filestat: Sys->Dir;  # stat after previous read or write, to check before writing.  only valid if filefd not nil.

modified: int;  # whether text has unsaved changes
text: ref Buf;  # contents
textgen: big;   # generation of text, increased on each changed, restored on undo/redo.
textgenlast: big;  # last used gen
cursor: ref Cursor;  # current position in text

statustext: string;  # info/warning/error text to display in status bar

searchregex: Regex->Re;  # current search, set by [/?*#]
searchreverse: int;  # whether search is in reverse, for [nN]
searchcache: array of (int, int);  # cache of search results, only valid if textgen is searchcachegen.
searchcachegen := big -1;

lastfind: int;  # last find command, one of tTfF, for ';' and ','
lastfindchar: int;  # parameter to lastfind

lastmacro: int; # last macro execution, for '@@'

edithist: list of string;  # history of edit commands, hd is last typed
edithistcur := -1;  # currently selected history item, -1 when none
edithisttext: string;  # text currently prefix-searching for (nil at start, after edit field changed, and esc)

completecache: array of string;  # matches with completetext.  invalid when nil
completetext: string;  # text for which completecache is the completion
completeindex: int;  # current index in completecache results

change: ref Change;  # current change (with 1 modification) that is being created (while in insert mode)
changes: array of ref Change;  # change history, for undo.  first elem is oldest change.
changeindex: int;  # points to next new/last undone change.  may be one past end of 'changes'.

# marks & registers are index by ascii char, not all are valid though
marks := array[128] of ref Cursor;
registers := array[128] of string;
register := '"';  # register to write next text deletion to

b3start: ref Pos; # start of button3 press
b3prev: ref Pos;  # previous position while button3 down

statusvisible := 1;  # whether tk frame with status label is visible (and edit entry is not)

highlightstart, highlightend: ref Cursor;  # range to highlight for search match, can be nil
plumbvisible: int;  # whether address or last inserted text is visible (cleared on interp)

vpfd: ref Sys->FD;  # fd to /chan/vixenplumb, for handling plumbing

plumbed: int;
top: ref Tk->Toplevel;
wmctl: chan of string;
drawcontext: ref Draw->Context;

# text selection color scheme.  Green for plumbing.
Normal, Green: con iota;

tkcmds0 := array[] of {
"frame .t",
"text .t.text -background black -foreground white -yscrollcommand {.t.vscroll set}",
"scrollbar .t.vscroll -command {.t.text yview}",
"frame .s",
"label .s.status -text status",
"frame .e",
"entry .e.edit",

"bind .e.edit <Key-\n> {send edit return}",
"bind .e.edit {<Key-\t>} {send edit tab}",
"bind .e.edit <KeyPress> +{send edit press %K}",
"bind .t.text <KeyPress> {send key %K}",
"bind .t.text <ButtonPress-1> {send text b1down @%x,%y}",
"bind .t.text <ButtonRelease-1> {send text b1up @%x,%y}",
"bind .t.text <ButtonPress-3> {send text b3down @%x,%y}",
"bind .t.text <ButtonRelease-3> {send text b3up @%x,%y}",
"bind .t.text <Configure> {send text resized}",

".t.text tag configure eof -foreground blue -background white",
".t.text tag configure search -background yellow -foreground black",
".t.text tag configure plumb -background blue -foreground white",
".t.text tag raise sel",

"pack .t.vscroll -fill y -side left",
"pack .t.text -fill both -expand 1 -side right",
"pack .t -fill both -expand 1",

"pack .s.status -fill x -side left",
"pack .s -fill x -side bottom -after .t",

"pack .e.edit -fill x -expand 1 -side left",
#"pack .e -fill x -side bottom -after .t",

"pack propagate . 0",
". configure -width 700 -height 500",
"focus .t.text",
};

tkaddeof()
{
	tkcmd(".t.text insert end \u0003");
	tkcmd(".t.text tag add eof {end -1c} end");
}

tkbinds()
{
	tkcmd(sprint("bind .e.edit <Key-%c> {send edit esc}", kb->Esc));

	tkcmd(sprint("bind .e.edit <Key-%c> {send edit up}", kb->Up));
	tkcmd(sprint("bind .e.edit <Key-%c> {send edit down}", kb->Down));

	binds := array[] of {'a', '<', 'b', 'd', 'e','>', 'f', 'h', 'k', 'n', 'o', 'p', 'u', 'v', 'w'};
	for(i := 0; i < len binds; i++)
		tkcmd(sprint("bind .t.text <Control-\\%c> {}", binds[i]));
	binds = array[] of {
		kb->Home, kb->Left, kb->End, kb->Right,
		kb->Del, kb->Down, kb->Up, kb->Pgdown, kb->Pgup
	};
	for(i = 0; i < len binds; i++)
		tkcmd(sprint("bind .t.text <Key-\\%c> {send key %%K}", binds[i]));

	binds = array[] of {'h', 'w', 'u', 'f', 'b', 'd', 'y', 'e', 'l', 'g', 'r', 'n', 'p'};
	for(i = 0; i < len binds; i++)
		tkcmd(sprint("bind .t.text <Control-\\%c> {send key %x}", binds[i], kb->APP|binds[i]));
}


init(ctxt: ref Draw->Context, args: list of string)
{
	sys = load Sys Sys->PATH;
	if(ctxt == nil)
		fail("no window context");
	drawcontext = ctxt;
	draw = load Draw Draw->PATH;
	arg := load Arg Arg->PATH;
	bufio = load Bufio Bufio->PATH;
	str = load String String->PATH;
	tk = load Tk Tk->PATH;
	tkclient = load Tkclient Tkclient->PATH;
	regex = load Regex Regex->PATH;
	plumbmsg = load Plumbmsg Plumbmsg->PATH;
	names = load Names Names->PATH;
	sh = load Sh Sh->PATH;
	sh->initialise();

	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);

	arg->init(args);
	arg->setusage(arg->progname()+" [-d debug] [-c macro] [-i] path");
	while((c := arg->opt()) != 0)
		case c {
		'c' =>	startupmacro = arg->arg();
		'd' =>
			s := arg->arg();
			for(i := 0; i < len s; i++)
				case x := s[i] {
				'+' =>		debug = array[128] of {* => 1};
				'a' to 'z' =>	debug[x]++;
				* =>		fail(sprint("debug char %c a-z or +", x));
				}
		'i' =>	iflag++;
		* =>	arg->usage();
		}
	args = arg->argv();
	case len args {
	0 =>	{}
	1 =>	filename = hd args;
	* =>	arg->usage();
	}

	plumbed = plumbmsg->init(1, nil, 0) >= 0;

	vpc := chan of (string, string);
	vpfd = sys->open("/chan/vixenplumb", Sys->ORDWR);
	if(vpfd != nil)
		spawn vixenplumbreader(vpfd, vpc);
	else
		warn(sprint("no plumbing, open /chan/vixenplumb: %r"));

	text = text.new();
	cursor = text.pos(Pos(1, 0));
	xmarkput('`', cursor);
	cmdcur = Cmd.new();
	xregput('!', "mk");

	openerr: string;
	if(filename != nil) {
		filefd = sys->open(filename, Sys->ORDWR);
		if(filefd == nil)
			openerr = sprint("%r");
		else {
			ok: int;
			(ok, filestat) = sys->fstat(filefd);
			if(ok < 0) {
				openerr = sprint("stat: %r");
				filefd = nil;
			}
		}
		# if filefd is nil, we warn that this is a new file when tk is initialized
	}

	tkclient->init();
	(top, wmctl) = tkclient->toplevel(ctxt, "", "vixen", Tkclient->Appl);

	textc := chan of string;
	keyc := chan of string;
	editc := chan of string;
	tk->namechan(top, textc, "text");
	tk->namechan(top, keyc, "key");
	tk->namechan(top, editc, "edit");
	tkcmds(tkcmds0);
	tkselcolor(Normal);
	tkbinds();

	if(filename != nil)
		filenameset(filename);

	if(filename != nil && filefd == nil) {
		(ok, dir) := sys->stat(filename);
		if(ok < 0)
			statuswarn(sprint("new file %q", filename));
		else if(dir.mode & Sys->DMDIR)
			statuswarn(sprint("%q is directory", filename));
		else
			statuswarn(sprint("open: %s", openerr));
	}
	if(iflag)
		oerr := textfill(sys->fildes(0));
	else if(filefd != nil)
		oerr = textfill(filefd);
	if(oerr != nil)
		statuswarn("reading: "+oerr);
	tkaddeof();
	up();

	modeset(Command0);

	if(startupmacro != nil) {
		cmdcur = Cmd.mk(startupmacro);
		interpx();
	}

	tkclient->onscreen(top, nil);
	tkclient->startinput(top, "kbd"::"ptr"::nil);

	for(;;) alt {
	s := <-top.ctxt.kbd =>
		tk->keyboard(top, s);

	s := <-top.ctxt.ptr =>
		tk->pointer(top, *s);

	s := <-top.ctxt.ctl or
	s = <-top.wreq =>
		tkclient->wmctl(top, s);

	menu := <-wmctl =>
		case menu {
		"exit" =>	quit();
		* =>		tkclient->wmctl(top, menu);
		}

	txt := <-textc =>
		# special keys/mouse from text widget
		say('t', sprint("text: %q", txt));
		(nil, t) := sys->tokenize(txt, " ");
		case hd t {
		"b1down" =>
			v := tkcmd(".t.text index "+hd tl t);
			if(str->prefix("!", v))
				break;
			pos := Pos.parse(v);
			modeset(Command0);
			cursorset(text.pos(pos));
			tkselectionset(cursor.pos, cursor.pos);
			tkselcolor(Normal);
		"b1up" =>
			v := tkcmd(".t.text index "+hd tl t);
			if(str->prefix("!", v))
				break;
			nc := text.pos(Pos.parse(v));
			ranges := tkcmd(".t.text tag ranges sel");
			if(ranges != nil) {
				(nil, l) := sys->tokenize(ranges, " ");
				if(len l != 2) {
					tkcmd(".t.text tag remove sel "+ranges);
					warn(sprint("bad selection range %q?", ranges));
					continue;
				}
				modeset(Visual);
				visualstart = text.pos(Pos.parse(hd l));
				cursor = text.pos(Pos.parse(hd tl l));
				if(Cursor.cmp(nc, cursor) < 0)
					(cursor, visualstart) = (visualstart, cursor);
				visualend = cursor.clone();
				cursorset(cursor);
			}
		"b3down" =>
			v := tkcmd(".t.text index "+hd tl t);
			if(str->prefix("!", v))
				break;
			pos := ref Pos.parse(v);
			if(b3start == nil) {
				tkselectionset(cursor.pos, cursor.pos);
				tkselcolor(Green);
				b3start = b3prev = pos;
			} else if(!Pos.eq(*pos, *b3prev)) {
				(a, b) := Pos.order(*pos, *b3start);
				tkselectionset(a, b);
				b3prev = pos;
			}
			say('t', sprint("b3down at char %s", (*pos).text()));
		"b3up" =>
			v := tkcmd(".t.text index "+hd tl t);
			if(str->prefix("!", v))
				break;
			pos := Pos.parse(v);
			say('t', sprint("b3up at char %s", pos.text()));
			if(Pos.eq(*b3start, pos)) {
				cx := text.pos(pos);
				(cs, ce) := cx.pathpattern(0);
				if(cs == nil)
					statuswarn("not a path");
				else
					plumb(text.get(cs, ce), nil, plumbdir());
			} else {
				cs := text.pos(*b3start);
				ce := text.pos(pos);
				(cs, ce) = Cursor.order(cs, ce);
				plumb(text.get(cs, ce), nil, plumbdir());
			}
			b3start = b3prev = nil;
			case mode {
			Visual or
			Visualline =>
				cursorset(cursor);
				visualset();
			* =>
				tkcmd(sprint(".t.text tag remove sel 1.0 end"));
			}
			tkselcolor(Normal);
		"resized" =>
			tkcmd(".t.text see insert");
		* =>
			warn(sprint("text unhandled, %q", txt));
		}
		up();

	s := <-keyc =>
		# keys from text widget
		say('t', sprint("cmd: %q", s));
		(x, rem) := str->toint(s, 16);
		if(rem != nil) {
			warn(sprint("bogus char code %q, ignoring", s));
			continue;
		}
		key(x);
		interpx();

	e := <-editc =>
		# special keys from edit widget
		say('t', sprint("edit: %q", e));
		editinput(e);
		up();

	(s, err) := <-vpc =>
		say('d', sprint("vpc, s %q, err %q", s, err));
		if(err != nil) {
			statuswarn("vixenplumb failed: "+err);
			continue;
		}
		if(iflag) {
			ps := text.end();
			textins(Cchange, ps, s);
			tkplumbshow(ps.pos, text.end().pos);
			tkcmd(sprint(".t.text see %s", ps.pos.text()));
		} else {
			nc: ref Cursor;
			(nc, err) = address(Cmd.mk(s), cursor);
			if(err != nil) {
				statuswarn(sprint("bad address from vixenplumb: %q: %s", s, err));
			} else {
				cursorset(nc);
				tkplumbshow(nc.mvcol(0).pos, nc.mvlineend(1).pos);
				statuswarn(sprint("new address from vixenplumb: %s", s));
			}
		}
		tkclient->wmctl(top, "raise");
		tkclient->wmctl(top, "kbdfocus 1");
		tkclient->onscreen(top, "onscreen");
		up();
	}
}

filenameset(s: string)
{
	filename = names->cleanname(names->rooted(workdir(), s));
	if(isdir(filename) && (filename != nil && filename[len filename-1] != '/'))
		filename[len filename] = '/';
	tkclient->settitle(top, "vixen "+filename);
	if(vpfd != nil) {
		f := filename;
		if(f[len f-1] == '/')
			f = f[:len f-1];
		if(sys->fprint(vpfd, "%s", f) < 0)
			statuswarn(sprint("telling vixenplumb about filename: %r"));
	}
}

vixenplumbreader(fd: ref Sys->FD, vpc: chan of (string, string))
{
	buf := array[8*1024] of byte;  # Iomax in vixenplumb
	for(;;) {
		n := sys->read(fd, buf, len buf);
		if(n <= 0) {
			err := "eof";
			if(n < 0)
				err = sprint("%r");
			vpc <-= (nil, err);
			break;
		}
		s := string buf[:n];
		vpc <-= (s, nil);
	}
}

editinput(e: string)
{
	case e {
	"return" =>
		s := tkcmd(".e.edit get");
		if(s == nil)
			raise "empty string from entry";
		say('e', sprint("edit command: %q", s));
		s = s[1:];  # first char has already been read
		tkcmd(".e.edit delete 0 end");
		edithistput(s);
		for(i := 0; i < len s; i++)
			key(s[i]);
		key('\n');
		interpx();
		tkcmd("focus .t.text");
	"tab" =>
		Completebreak: con " \t!\"\'#$%&'()*+,:;<=>?@\\]^_`{|}~";
		s := tkcmd(".e.edit get");
		i := int tkcmd(".e.edit index insert");
		while(i-1 >= 0 && !str->in(s[i-1], Completebreak))
			--i;
		s = s[i:];
		r: string;
		++completeindex;
		if(completecache != nil && completeindex >= len completecache) {
			r = completetext;
			completecache = nil;
		} else {
			if(completecache == nil) {
				err: string;
				(completecache, err) = complete(s);
				if(err != nil)
					return statuswarn("complete: "+err);
				if(len completecache == 0)
					return statuswarn("no match");
				completeindex = 0;
				completetext = s;
			}
			r = completecache[completeindex];
			if(len completecache == 1)
				completecache = nil;
		}
		tkcmd(sprint(".e.edit delete %d end", i));
		tkcmd(".e.edit insert end '"+r);
	"up" or
	"down" =>
		# if up/down was down without esc or text editing afterwards,
		# we use the originally typed text to search, not what's currently in the edit field.
		a := l2a(rev(edithist));
		say('e', sprint("edithist, edithistcur=%d:", edithistcur));
		for(i := 0; i < len a; i++)
			say('e', sprint("%3d %s", i, a[i]));
		editnavigate(e == "up");
	"esc" =>
		editesc();
	* =>
		if(str->prefix("press ", e)) {
			(x, rem) := str->toint(e[len "press ":], 16);
			if(rem != nil)
				return warn(sprint("bad edit press %q", e));

			# key presses are interpreted by tk widget first, then sent here.
			# on e.g. ^h of last char, we see an empty string in the entry, so we abort.
			if(x != '\n' && tkcmd(".e.edit get") == nil)
				editesc();

			# we get up/down and other specials too, they don't change the text
			if((x & kb->Spec) != kb->Spec && x != '\t') {
				edithistcur = -1;
				edithisttext = nil;
				completecache = nil;
			}
		} else
			warn(sprint("unhandled edit command %q", e));
	}
}

# key from text widget or from macro execute
key(x: int)
{
	if(recordreg >= 0)
		record[len record] = x;
	cmdcur.put(x);
}


editesc()
{
	tkcmd(".e.edit delete 0 end");
	edithistcur = -1;
	edithisttext = nil;
	tkcmd("focus .t.text");
	key(kb->Esc);
	interpx();
}

editset0(index: int, s: string)
{
	edithistcur = index;
	tkcmd(sprint("focus .e.edit; .e.edit delete 0 end; .e.edit insert 0 '%s", s));
}

editset(s: string)
{
	editset0(-1, s);
}

xeditget(c: ref Cmd, pre: string): string
{
	if(statusvisible) {
		tkcmd("pack forget .s; pack .e -fill x -side bottom -after .t");
		statusvisible = 0;
	}

	if(!c.more())
		raise "edit:"+pre;

	if(c.char() == kb->Esc)
		xabort(nil);
	s: string;
Read:
	for(;;)
		case x := c.get() {
		-1 =>
			# text from .e.entry has a newline, but don't require one from -c or '@'
			break Read;
		'\n' =>
			say('e', sprint("xeditget, returning %q", s));
			break Read;
		* =>
			s[len s] = x;
		}
	r := pre[0];
	if(r == '?')
		r = '/';
	xregput(r, s);
	return s; 
}

editnavigate(up: int)
{
	if(edithisttext == nil)
		edithisttext = tkcmd(".e.edit get");
	a := l2a(rev(edithist));
	if(up) {
		for(i := edithistcur+1; i < len a; ++i)
			if(str->prefix(edithisttext, a[i]))
				return editset0(i, a[i]);
	} else {
		for(i := edithistcur-1; i >= 0; --i)
			if(str->prefix(edithisttext, a[i]))
				return editset0(i, a[i]);
	}
	statuswarn("no match");
}

edithistput(s: string)
{
	if(s != nil) {
		edithist = s::edithist;
		edithistcur = -1;
	}
}

complete(pre: string): (array of string, string)
{
	(path, f) := str->splitstrr(pre, "/");
say('e', sprint("complete, pre %q, path %q, f %q", pre, path, f));
	dir := path;
	if(path == nil)
		dir = ".";
	fd := sys->open(dir, Sys->OREAD);
	if(fd == nil)
		return (nil, sprint("open: %r"));
	l: list of string;
	for(;;) {
		(n, a) := sys->dirread(fd);
		if(n == 0)
			break;
		if(n < 0)
			return (nil, sprint("dirread: %r"));
		for(i := 0; i < n; i++)
			if(str->prefix(f, a[i].name)) {
				s := path+a[i].name;
				if(a[i].mode & Sys->DMDIR)
					s += "/";
				l = s::l;
			}
	}
	return (l2a(rev(l)), nil);
}

# return directory to plumb from:  dir where filename is in, or workdir if no filename is set
plumbdir(): string
{
	if(filename == nil)
		return workdir();
	return names->dirname(filename);
}

plumb(s, kind, dir: string)
{
	if(!plumbed)
		return statuswarn("cannot plumb");
	if(kind == nil)
		kind = "text";
	msg := ref Msg("vixen", "", dir, kind, "", array of byte s);
	say('d', sprint("plumbing %s", string msg.pack()));
	msg.send();
}

changesave()
{
	if(change == nil)
		return;
	changeadd(change);
	change = nil;
}

changeadd(c: ref Change)
{
	if(changeindex < len changes) {
		changes = changes[:changeindex+1];
	} else {
		n := array[len changes+1] of ref Change;
		n[:] = changes;
		changes = n;
	}
	if(c.ogen == c.ngen)
		raise "storing a change with same orig as new gen?";
	c.ngen = textgen;
	say('u', "changeadd, storing:");
	say('u', c.text());
	changes[changeindex++] = c;
}

apply(c: ref Change): int
{
	say('u', "apply:");
	say('u', c.text());
	for(l := c.l; l != nil; l = tl l)
		pick m := hd l {
		Ins =>	textins(Cnone, text.pos(m.p), m.s);
		Del =>	textdel(Cnone, text.pos(m.p), text.cursor(m.o+len m.s));
		}
	textgen = c.ngen;
	cursorset(text.pos(c.beginpos()));
	return 1;
}

undo()
{
	say('u', sprint("undo, changeindex=%d, len changes=%d", changeindex, len changes));
	if(changeindex == 0)
		return statuswarn("already at oldest change");
	if(apply(changes[changeindex-1].invert()))
		--changeindex;
}

redo()
{
	say('u', "redo");
	if(changeindex >= len changes)
		return statuswarn("already at newest change");;
	c := ref *changes[changeindex];
	c.l = rev(c.l);
	if(apply(c))
		++changeindex;
}


searchset(s: string): int
{
	searchcachegen = big -1;
	searchcache = nil;
	err: string;
	(searchregex, err) = regex->compile(s, 0);
	if(err != nil) {
		searchregex = nil;
		statuswarn("bad pattern");
		return 0;
	}
	return 1;
}

searchall(re: Regex->Re): array of (int, int)
{
	if(textgen == searchcachegen)
		return searchcache;

	l: list of (int, int);
	o := 0;
	s := text.str();
	sol := 1;
	while(o < len s) {
		for(e := o; e < len s && s[e] != '\n'; ++e)
			{}
		r := regex->executese(re, s, (o, e), sol, 1);
		if(len r >= 1 && r[0].t0 >= 0) {
			l = r[0]::l;
			o = r[0].t1;
			sol = 0;
		} else {
			sol = 1;
			o = e+1;
		}
	}
	r := array[len l] of (int, int);
	for(i := len r-1; i >= 0; --i) {
		r[i] = hd l;
		l = tl l;
	}
	searchcache = r;
	searchcachegen = textgen;
	return r;
}

search(rev, srev: int, re: Regex->Re, cr: ref Cursor): (ref Cursor, ref Cursor)
{
	if(re == nil) {
		statuswarn("no search pattern set");
		return (nil, nil);
	}
	if(srev)
		rev = !rev;
	
	r := searchall(re);
	if(len r == 0 || r[0].t0 < 0) {
		statuswarn("pattern not found");
		return (nil, nil);
	}
	i: int;
	if(rev) {
		for(i = len r-1; i >= 0; i--)
			if(r[i].t0 < cr.o)
				break;
		if(i < 0) {
			i = len r-1;
			statuswarn("search wrapped");
		}
	} else {
		for(i = 0; i < len r; i++)
			if(r[i].t0 > cr.o)
				break;
		if(i >= len r) {
			i = 0;
			statuswarn("search wrapped");
		}
	}
	return (text.cursor(r[i].t0), text.cursor(r[i].t1));
}


xregset(c: int)
{
	# we don't know if it will be for get or set yet, so % is valid
	if(c != '%')
		xregcanput(c);
	register = c;
}

xregget(c: int): string
{
	(s, err) := regget(c);
	if(err == nil && s == nil)
		err = sprint("register %c empty", c);
	if(err != nil)
		xabort(err);
	return s;
}

xregcanput(c: int)
{
	case c {
	'a' to 'z' or
	'/' or
	':' or
	'.' or
	'"' or
	'A' to 'Z' or
	'*' or
	'!' =>	return;
	'%' =>	xabort("register % is read-only");
	* =>	xabort(sprint("bad register %c", c));
	}
}

xregput(x: int, s: string)
{
	err := regput(x, s);
	if(err != nil)
		xabort(err);
}

regget(c: int): (string, string)
{
	r: string;
	case c {
	'a' to 'z' or
	'/' or
	':' or
	'.' or
	'"' or
	'!' =>		r = registers[c];
	'A' to 'Z' =>	r = registers[c-'A'+'a'];
	'%' =>		r = filename;
	'*' =>		r = tkclient->snarfget();
	* =>		return (nil, sprint("bad register %c", c));
	}
	return (r, nil);
}

regput(c: int, s: string): string
{
	case c {
	'a' to 'z' or
	'/' or
	':' or
	'.' or
	'"' or
	'!' =>
		registers[c] = s;
	'A' to 'Z' =>
		registers[c-'A'+'a'] += s;
	'%' =>
		return "register % is read-only";
	'*' =>
		tkclient->snarfput(s);
		return nil;
	* =>	
		return sprint("bad register %c", c);
	}
	return nil;
}


markget(c: int): (ref Cursor, string)
{
	m: ref Cursor;
	case c {
	'a' to 'z' or
	'`' or
	'\'' or
	'.' or
	'^' =>	m = marks[c];
	'<' or
	'>' =>
		if(mode != Visual && mode != Visualline)
			return (nil, "selection not set");
		(vs, ve) := visualrange();
		case c {
		'<' =>	m = vs;
		'>' =>	m = ve;
		}
	* =>
		return (nil, sprint("bad mark %c", c));
	}
	if(m == nil)
		return (nil, sprint("mark %c not set", c));
	return (m, nil);
}

xmarkget(c: int): ref Cursor
{
	(m, err) := markget(c);
	if(err != nil)
		xabort(err);
	return m;
}

xmarkput(c: int, m: ref Cursor)
{
	m = m.clone();
	case c {
	'a' to 'z' or
	'.' or
	'^' =>	marks[c] = m;
	'`' or
	'\'' =>	marks['`'] = marks['\''] = m;
	# < and > cannot be set explicitly
	* =>	xabort(sprint("bad mark %c", c));
	}
}

# fix marks, cs-ce have just been deleted (and their positions are no longer valid!)
markfixdel(cs, ce: ref Cursor)
{
	for(i := 0; i < len marks; i++) {
		m := marks[i];
		if(m == nil || m.o < cs.o)
			continue;
		if(m.o < ce.o)
			marks[i] = nil;
		else
			marks[i] = text.cursor(m.o-Cursor.diff(cs, ce));
	}
}

# fix marks, n bytes have just been inserted at cs
markfixins(cs: ref Cursor, n: int)
{
	for(i := 0; i < len marks; i++) {
		m := marks[i];
		if(m == nil || m.o < cs.o)
			continue;
		marks[i] = text.cursor(m.o+n);
	}
}


# 'q' was received while in command or visual mode.
recordq(c: ref Cmd)
{
	say('d', sprint("recordq, recordreg %c, record %q, c %s", recordreg, record, c.text()));
	if(recordreg >= 0) {
		xregput(recordreg, record[:len record-1]); # strip last 'q' at end
		say('d', sprint("register %c now %q", recordreg, registers[recordreg]));
		record = nil;
		recordreg = -1;
	} else {
		y := c.xget();
		xregcanput(y);
		recordreg = y;
	}
}

# whether text was inserted/replaced
inserted(): int
{
	if(change != nil)
		pick m := hd change.l {
		Ins =>
			return m.o+len m.s == cursor.o;
		}
	return 0;
}

textrepl(rec: int, a, b: ref Cursor, s: string)
{
	if(a == nil)
		a = cursor;
	if(b == nil)
		b = cursor;
	textdel(rec, a, b);
	textins(rec, a, s);
}

# delete from a to b.
# rec indicates whether a Change must be recorded,
# where the cursor should be,
# and whether the last change register should be set.
textdel(rec: int, a, b: ref Cursor)
{
	tkhighlightclear();

	if(a == nil)
		a = cursor;
	if(b == nil)
		b = cursor;

	setreg := rec & Csetreg;
	setcursor := rec & Csetcursormask;

	swap := Cursor.cmp(a, b) > 0;
	if(swap)
		(a, b) = (b, a);
	s := text.get(a, b);

	rec &= Cchangemask;
Change:
	case rec {
	Cnone =>
		{}
	Cmodrepl =>
		say('m', sprint("textdel, Cmodrepl, s %q, a %s, b %s", s, a.text(), b.text()));
		if(change == nil)
			return statuswarn("beep!");
		pick m := hd change.l {
		Ins =>
			say('m', "textdel, last was insert");
			if(m.o+len m.s != b.o)
				raise "delete during replace should be at end of previous insert";
			if(len s > len m.s) {
				a = text.cursor(b.o-len m.s);
				s = text.get(a, b);
			}
			m.s = m.s[:len m.s-len s];
			# we check below whether we have to remove this Mod.Ins
		Del =>
			say('m', "textdel, last was del");
			return statuswarn("beep!");
		}
	Cmod or
	Cchange =>
		if(change != nil)
			pick m := hd change.l {
			Ins =>
				if(m.o+len m.s == b.o) {
					if(len s > len m.s) {
						a = text.cursor(b.o-len m.s);
						s = text.get(a, b);
					}
					m.s = m.s[:len m.s-len s];
					if(m.s == nil) {
						change.l = tl change.l;
						if(change.l == nil)
							change = nil;
					}
					break Change;
				}
			Del =>
				if(rec != Cmod && rec != Cmodrepl && m.o == a.o) {
					m.s += s;
					break Change;
				}
			}
		if(rec == Cmod)
			return statuswarn("beep!");
		if(change == nil)
			change = ref Change (0, nil, textgen, ~big 0);
		change.l = ref Mod.Del (a.o, a.pos, s)::change.l;
	Cchangerepl =>
		raise "should not happen";
	* =>
		raise "bad rec";
	}
	if(setreg)
		xregput(register, s);
	tkcmd(sprint(".t.text delete %s %s", a.pos.text(), b.pos.text()));
	text.del(a, b);
	textgen = textgenlast++;;
	markfixdel(a, b);
	if(rec != Cnone)
		xmarkput('.', a);

	if(rec == Cmodrepl) {
		# Mod.Del may be absent, eg when replace was started at end of file
		if(tl change.l != nil) {
			pick m := hd tl change.l {
			Del =>
				# if a is in this del, remove till end of it, and insert at the cursor
				if(a.o >= m.o && a.o < m.o+len m.s) {
					nn := a.o-m.o;
					os := m.s[nn:];
					m.s = m.s[:nn];
					text.ins(a, os);
					textgen = textgenlast++;
					markfixins(a, len os);
					tkcmd(sprint(".t.text insert %s '%s", a.pos.text(), os));
					if(a.o+len os >= text.chars())
						tkcmd(sprint(".t.text tag remove eof %s {%s +%dc}", a.pos.text(), a.pos.text(), len os));
				}
			}
		}
		pick m := hd change.l {
		Ins =>
			if(m.s == nil)
				change.l = tl change.l;
		}
		pick m := hd change.l {
		Del =>
			if(m.s == nil)
				change.l = tl change.l;
		}
		if(change.l == nil)
			change = nil;
	}
	if(setcursor) {
		n: ref Cursor;
		case setcursor {
		0 =>		{}
		Csetcursorlo =>	n = a;
		Csetcursorhi =>	n = b;
		* =>		raise "bad rec";
		}
		cursorset(n);
	}
}

textins(rec: int, c: ref Cursor, s: string)
{
	tkhighlightclear();

	if(c == nil)
		c = cursor;

	setcursor := rec&Csetcursormask;
	rec &= Cchangemask;

Change:
	case rec {
	Cnone =>
		{}
	Cmod or 
	Cmodrepl =>
		raise "should not happen";
	Cchange or
	Cchangerepl =>
		ins := 0;
		if(change != nil) {
			pick m := hd change.l {
			Ins =>
				if(m.o+len m.s == c.o) {
					m.s += s;
					ins = 1;
				}
			}
		}
		if(!ins) {
			if(change == nil)
				change = ref Change (0, nil, textgen, ~big 0);
			change.l = ref Mod.Ins (c.o, c.pos, s)::change.l;
		}
		if(rec == Cchangerepl) {
			n := min(len s, len text.str()-c.o);
			if(n > 0) {
				(a, b) := (text.cursor(c.o), text.cursor(c.o+n));
				tkcmd(sprint(".t.text delete %s %s", a.pos.text(), b.pos.text()));
				os := text.del(a, b);
				textgen = textgenlast++;
				markfixdel(a, b);
				if(tl change.l != nil) {
					pick m := hd tl change.l {
					Del =>
						if(c.o == m.o+len m.s) {
							m.s += os;
							break Change;
						}
					}
				}
				m := ref Mod.Del (c.o, c.pos, os);
				change.l = hd change.l::m::tl change.l;
			}
		}
	* =>
		raise "bad rec";
	}

	tkcmd(sprint(".t.text insert %s '%s", c.pos.text(), s));
	if(c.o+len s >= text.chars())
		tkcmd(sprint(".t.text tag remove eof %s {%s +%dc}", c.pos.text(), c.pos.text(), len s));
	nc := text.ins(c, s);
	textgen = textgenlast++;
	markfixins(c, len s);
	case setcursor {
	0 =>	{}
	Csetcursorlo =>	cursorset(c);
	Csetcursorhi =>	cursorset(nc);
	* =>	raise "bad rec";
	}

	modified = 1;
	say('m', sprint("textins, inserted %q, cursor now %s", s, cursor.text()));
}


textfill(fd: ref Sys->FD): string
{
	b := bufio->fopen(fd, Sys->OREAD);
	if(b == nil)
		return sprint("fopen: %r");
	s: string;
	n := 0;
	for(;;) {
		case x := b.getc() {
		Bufio->EOF =>
			tkcmd(".t.text insert end '"+s);
			text.set(s);
			return nil;
		bufio->ERROR =>
			return sprint("read: %r");
		* =>
			s[n++] = x;
		}
	}
}

writemodifiedquit(force: int)
{
	if(modified) {
		if(filename == nil)
			return statuswarn("no filename set");
		err := textwrite(force, filename, nil, nil);
		if(err != nil)
			return statuswarn(err);
		modified = 0;
	}
	if(modified && !force)
		return statuswarn("unsaved changes");
	quit();
}

# write cs-ce to f (force makes it overwrite f when it exists or when cs/ce is not nil).
textwrite(force: int, f: string, cs, ce: ref Cursor): string
{
	fd: ref Sys->FD;
	if(f == nil)
		return "no filename set";
	if(filefd == nil || f != filename) {
		fd = sys->open(f, Sys->ORDWR);
		if(fd != nil && !force)
			return "file already exists";
		if(fd == nil)
			fd = sys->create(f, Sys->ORDWR, 8r666);
		if(fd == nil)
			return sprint("create: %r");
		if(f == filename)
			filefd = fd;
	} else {
		(ok, st) := sys->fstat(filefd);
		if(ok < 0)
			return sprint("stat: %r");
		if(!force) {
			if(st.qid.vers != filestat.qid.vers)
				return sprint("file's qid version has changed, not writing");
			if(st.mtime != filestat.mtime || st.length != filestat.length)
				return sprint("file's length or mtime has changed, not writing");
		}
		sys->seek(filefd, big 0, Sys->SEEKSTART);
		d := sys->nulldir;
		d.length = big 0;
		if(sys->fwstat(filefd, d) < 0)
			return sprint("truncate %q: %r", f);
		fd = filefd;
	}
	err := bufwritefd(text, cs, ce, fd);
	if(filefd != nil) {
		ok: int;
		(ok, filestat) = sys->fstat(filefd);
		if(ok < 0)
			return sprint("stat after write: %r");
	}
	return err;
}

textappend(f: string, cs, ce: ref Cursor): string
{
	if(cs == nil)
		s := text.str();
	else
		s = text.get(cs, ce);
	b := bufio->open(f, Sys->OWRITE);
	if(b == nil)
		return sprint("open: %r");
	b.seek(big 0, bufio->SEEKEND);
	if(b.puts(s) == Bufio->ERROR || b.flush() == Bufio->ERROR)
		return sprint("write: %r");
	return nil;
}

readfile(f: string): (string, string)
{
	b := bufio->open(f, Bufio->OREAD);
	if(b == nil)
		return (nil, sprint("open: %r"));
	s := "";
	for(;;)
	case c := b.getc() {
	Bufio->EOF =>	return (s, nil);
	Bufio->ERROR =>	return (nil, sprint("read: %r"));
	* =>		s[len s] = c;
	}
}


statuswarn(s: string)
{
	say('d', "statuswarn: "+s);
	statustext = s;
	statusset();
}

statusset()
{
	s := sprint("%9s ", "("+modes[mode]+")");
	if(recordreg >= 0)
		s += "recording ";
	if(filename == nil)
		s += "(no filename)";
	else
		s += sprint("%q", names->basename(filename, nil));
	s += sprint(", %4d lines, %5d chars, pos %s", text.lines(), text.chars(), cursor.pos.text());
	if(cmdcur.rem() != nil)
		s += ", "+cmdcur.rem();
	if(statustext != nil)
		s += ", "+statustext;
	tkcmd(sprint(".s.status configure -text '%s", s));
	if(!statusvisible) {
		tkcmd("pack forget .e; pack .s -fill x -side bottom -after .t");
		statusvisible = 1;
	}
}

visualrange(): (ref Cursor, ref Cursor)
{
	(a, b) := Cursor.order(visualstart.clone(), visualend.clone());
	if(mode == Visualline) {
		a = a.mvcol(0);
		b = b.mvlineend(1);
	}
	return (a, b);
}

visualset()
{
	(a, b) := visualrange();
	tkselectionset(a.pos, b.pos);
}

tkselectionset(a, b: Pos)
{
	say('t', sprint("selectionset, from %s to %s", a.text(), b.text()));
	tkcmd(".t.text tag remove sel 1.0 end");
	tkcmd(sprint(".t.text tag add sel %s %s", a.text(), b.text()));
}


redraw()
{
	(spos, nil) := tkvisible();
	tkcmd(".t.text delete 1.0 end");
	tkcmd(".t.text insert 1.0 '"+text.str());
	tkaddeof();
	case mode {
	Visual or
	Visualline =>
		visualset();
	}
	cursorset(cursor);
	if(highlightstart != nil)
		tkhighlight(highlightstart, highlightend);
	plumbvisible = 0;
	tkcmd(sprint(".t.text see %s", spos.text()));
}

tkhighlightclear()
{
	if(highlightstart != nil) {
		tkcmd(".t.text tag remove search 1.0 end");
		highlightstart = highlightend = nil;
	}
}

tkhighlight(s, e: ref Cursor)
{
	tkhighlightclear();
	tkcmd(sprint(".t.text tag add search %s %s", s.pos.text(), e.pos.text()));
	(highlightstart, highlightend) = (s, e);
}

tkplumbclear()
{
	if(plumbvisible) {
		tkcmd(".t.text tag remove plumb 1.0 end");
		plumbvisible = 0;
	}
}

tkplumbshow(s, e: Pos)
{
	tkplumbclear();
	tkcmd(sprint(".t.text tag add plumb %s %s", s.text(), e.text()));
	plumbvisible = 1;
}

tkinsertset(p: Pos)
{
	tkcmd(sprint(".t.text mark set insert %s", p.text()));
}

cursorset0(c: ref Cursor, see: int)
{
	say('c', sprint("new cursor: %s", c.text()));
	cursor = c;
	tkinsertset(c.pos);
	if(see)
		tkcmd(sprint(".t.text see %s", c.pos.text()));
}

cursorset(c: ref Cursor)
{
	cursorset0(c, 1);
}

up()
{
	tkcmd("update");
}

tkvisibletop(): Pos
{
	return Pos.parse(tkcmd(".t.text index @0,0"));
}

tkvisiblebottom(): Pos
{
	height := tkcmd(".t.text cget -actheight");
	s := tkcmd(sprint(".t.text index @0,%d", int height-1));
	return Pos.parse(s);
}

tkvisible(): (Pos, Pos)
{
	return (tkvisibletop(), tkvisiblebottom());
}

tklinesvisible(): int
{
	(a, b) := tkvisible();
	return b.l+1-a.l;
}

tkselcolor(w: int)
{
	case w {
	Normal =>	tkcmd(".t.text tag configure sel -background white -foreground black");
	Green =>	tkcmd(".t.text tag configure sel -background green -foreground white");
	}
}

tkcmd(s: string): string
{
	say('k', s);
	r := tk->cmd(top, s);
	if(r != nil && r[0] == '!')
		warn(sprint("tkcmd: %q: %s", s, r));
	if(r != nil)
		say('k', " -> "+r);
	return r;
}

tkcmds(a: array of string)
{
	for(i := 0; i < len a; i++)
		tkcmd(a[i]);
}

quit()
{
	killgrp(pid());
	exit;
}

pid(): int
{
	return sys->pctl(0, nil);
}

progctl(pid: int, s: string)
{
	sys->fprint(sys->open(sprint("/prog/%d/ctl", pid), sys->OWRITE), "%s", s);
}

kill(pid: int)
{
	progctl(pid, "kill");
}

killgrp(pid: int)
{
	progctl(pid, "killgrp");
}

min(a, b: int): int
{
	if(a < b)
		return a;
	return b;
}

max(a, b: int): int
{
	if(a > b)
		return a;
	return b;
}

abs(a: int): int
{
	if(a < 0)
		a = -a;
	return a;
}

l2a[T](l: list of T): array of T
{
	a := array[len l] of T;
	i := 0;
	for(; l != nil; l = tl l)
		a[i++] = hd l;
	return a;
}

rev[T](l: list of T): list of T
{
	r: list of T;
	for(; l != nil; l = tl l)
		r = hd l::r;
	return r;
}

warn(s: string)
{
	sys->fprint(sys->fildes(2), "%s\n", s);
}

say(c: int, s: string)
{
	if(debug[c])
		warn(s);
}

fail(s: string)
{
	warn(s);
	killgrp(pid());
	raise "fail:"+s;
}