shithub: riscv

ref: 2657773699da675d436705002230f2d25142f570
dir: /sys/src/cmd/acme/ecmd.c/

View raw version
#include <u.h>
#include <libc.h>
#include <draw.h>
#include <thread.h>
#include <cursor.h>
#include <mouse.h>
#include <keyboard.h>
#include <frame.h>
#include <fcall.h>
#include <plumb.h>
#include "dat.h"
#include "edit.h"
#include "fns.h"

int	Glooping;
int	nest;
char	Enoname[] = "no file name given";

Address	addr;
File	*menu;
Rangeset	sel;
extern	Text*	curtext;
Rune	*collection;
int	ncollection;

int	append(File*, Cmd*, long);
int	pdisplay(File*);
void	pfilename(File*);
void	looper(File*, Cmd*, int);
void	filelooper(Text*, Cmd*, int);
void	linelooper(File*, Cmd*);
Address	lineaddr(long, Address, int);
int	filematch(File*, String*);
File	*tofile(String*);
Rune*	cmdname(File *f, String *s, int);
void	runpipe(Text*, int, Rune*, int, int);

void
clearcollection(void)
{
	free(collection);
	collection = nil;
	ncollection = 0;
}

void
resetxec(void)
{
	Glooping = nest = 0;
	clearcollection();
}

void
mkaddr(Address *a, File *f)
{
	a->r.q0 = f->curtext->q0;
	a->r.q1 = f->curtext->q1;
	a->f = f;
}

int
cmdexec(Text *t, Cmd *cp)
{
	int i;
	Addr *ap;
	File *f;
	Window *w;
	Address dot;

	if(t == nil)
		w = nil;
	else
		w = t->w;
	if(w==nil && (cp->addr==0 || cp->addr->type!='"') &&
	    !utfrune("bBnqUXY!", cp->cmdc) &&
	    !(cp->cmdc=='D' && cp->text))
		editerror("no current window");
	i = cmdlookup(cp->cmdc);	/* will be -1 for '{' */
	f = nil;
	if(t && t->w){
		t = &t->w->body;
		f = t->file;
		f->curtext = t;
	}
	if(i>=0 && cmdtab[i].defaddr != aNo){
		if((ap=cp->addr)==0 && cp->cmdc!='\n'){
			cp->addr = ap = newaddr();
			ap->type = '.';
			if(cmdtab[i].defaddr == aAll)
				ap->type = '*';
		}else if(ap && ap->type=='"' && ap->next==0 && cp->cmdc!='\n'){
			ap->next = newaddr();
			ap->next->type = '.';
			if(cmdtab[i].defaddr == aAll)
				ap->next->type = '*';
		}
		if(cp->addr){	/* may be false for '\n' (only) */
			static Address none = {0,0,nil};
			if(f){
				mkaddr(&dot, f);
				addr = cmdaddress(ap, dot, 0);
			}else	/* a " */
				addr = cmdaddress(ap, none, 0);
			f = addr.f;
			t = f->curtext;
		}
	}
	switch(cp->cmdc){
	case '{':
		mkaddr(&dot, f);
		if(cp->addr != nil)
			dot = cmdaddress(cp->addr, dot, 0);
		for(cp = cp->cmd; cp; cp = cp->next){
			if(dot.r.q1 > t->file->nc)
				editerror("dot extends past end of buffer during { command");
			t->q0 = dot.r.q0;
			t->q1 = dot.r.q1;
			cmdexec(t, cp);
		}
		break;
	default:
		if(i < 0)
			editerror("unknown command %c in cmdexec", cp->cmdc);
		i = (*cmdtab[i].fn)(t, cp);
		return i;
	}
	return 1;
}

char*
edittext(Window *w, int q, Rune *r, int nr)
{
	File *f;

	switch(editing){
	case Inactive:
		return "permission denied";
	case Inserting:
		f = w->body.file;
		eloginsert(f, q, r, nr);
		return nil;
	case Collecting:
		collection = runerealloc(collection, ncollection+nr+1);
		runemove(collection+ncollection, r, nr);
		ncollection += nr;
		collection[ncollection] = '\0';
		return nil;
	default:
		return "unknown state in edittext";
	}
}

/* string is known to be NUL-terminated */
Rune*
filelist(Text *t, Rune *r, int nr)
{
	if(nr == 0)
		return nil;
	r = skipbl(r, nr, &nr);
	clearcollection();
	if(r[0] != '<'){
		if((collection = runestrdup(r)) != nil)
			ncollection += runestrlen(r);
	}else
		/* use < command to collect text */
		runpipe(t, '<', r+1, nr-1, Collecting);
	return collection;
}

int
a_cmd(Text *t, Cmd *cp)
{
	return append(t->file, cp, addr.r.q1);
}

int
b_cmd(Text*, Cmd *cp)
{
	File *f;

	f = tofile(cp->text);
	if(nest == 0)
		pfilename(f);
	curtext = f->curtext;
	return TRUE;
}

int
B_cmd(Text *t, Cmd *cp)
{
	Rune *list, *r, *s;
	int nr;

	list = filelist(t, cp->text->r, cp->text->n);
	if(list == nil)
		editerror(Enoname);
	r = list;
	nr = runestrlen(r);
	r = skipbl(r, nr, &nr);
	if(nr == 0)
		new(t, t, nil, 0, 0, r, 0);
	else while(nr > 0){
		s = findbl(r, nr, &nr);
		*s = '\0';
		new(t, t, nil, 0, 0, r, runestrlen(r));
		if(nr > 0)
			r = skipbl(s+1, nr-1, &nr);
	}
	clearcollection();
	return TRUE;
}

int
c_cmd(Text *t, Cmd *cp)
{
	elogreplace(t->file, addr.r.q0, addr.r.q1, cp->text->r, cp->text->n);
	t->q0 = addr.r.q0;
	t->q1 = addr.r.q0;
	return TRUE;
}

int
d_cmd(Text *t, Cmd*)
{
	if(addr.r.q1 > addr.r.q0)
		elogdelete(t->file, addr.r.q0, addr.r.q1);
	t->q0 = addr.r.q0;
	t->q1 = addr.r.q0;
	return TRUE;
}

void
D1(Text *t)
{
	if(t->w->body.file->ntext>1 || winclean(t->w, FALSE))
		colclose(t->col, t->w, TRUE);
}

int
D_cmd(Text *t, Cmd *cp)
{
	Rune *list, *r, *s, *n;
	int nr, nn;
	Window *w;
	Runestr dir, rs;
	char buf[128];

	list = filelist(t, cp->text->r, cp->text->n);
	if(list == nil){
		D1(t);
		return TRUE;
	}
	dir = dirname(t, nil, 0);
	r = list;
	nr = runestrlen(r);
	r = skipbl(r, nr, &nr);
	do{
		s = findbl(r, nr, &nr);
		*s = '\0';
		/* first time through, could be empty string, meaning delete file empty name */
		nn = runestrlen(r);
		if(r[0]=='/' || nn==0 || dir.nr==0){
			rs.r = runestrdup(r);
			rs.nr = nn;
		}else{
			n = runemalloc(dir.nr+1+nn);
			runemove(n, dir.r, dir.nr);
			n[dir.nr] = '/';
			runemove(n+dir.nr+1, r, nn);
			rs = cleanrname((Runestr){n, dir.nr+1+nn});
		}
		w = lookfile(rs.r, rs.nr);
		if(w == nil){
			snprint(buf, sizeof buf, "no such file %.*S", rs.nr, rs.r);
			free(rs.r);
			editerror(buf);
		}
		free(rs.r);
		D1(&w->body);
		if(nr > 0)
			r = skipbl(s+1, nr-1, &nr);
	}while(nr > 0);
	clearcollection();
	free(dir.r);
	return TRUE;
}

static int
readloader(void *v, uint q0, Rune *r, int nr)
{
	if(nr > 0)
		eloginsert(v, q0, r, nr);
	return 0;
}

int
e_cmd(Text *t, Cmd *cp)
{
	Rune *name;
	File *f;
	int i, isdir, q0, q1, fd, nulls, samename, allreplaced;
	char *s, tmp[128];
	Dir *d;

	f = t->file;
	q0 = addr.r.q0;
	q1 = addr.r.q1;
	if(cp->cmdc == 'e'){
		if(winclean(t->w, TRUE)==FALSE)
			editerror("");	/* winclean generated message already */
		q0 = 0;
		q1 = f->nc;
	}
	allreplaced = (q0==0 && q1==f->nc);
	name = cmdname(f, cp->text, cp->cmdc=='e');
	if(name == nil)
		editerror(Enoname);
	i = runestrlen(name);
	samename = runeeq(name, i, t->file->name, t->file->nname);
	s = runetobyte(name, i);
	free(name);
	fd = open(s, OREAD);
	if(fd < 0){
		snprint(tmp, sizeof tmp, "can't open %s: %r", s);
		free(s);
		editerror(tmp);
	}
	d = dirfstat(fd);
	isdir = (d!=nil && (d->qid.type&QTDIR));
	free(d);
	if(isdir){
		close(fd);
		snprint(tmp, sizeof tmp, "%s is a directory", s);
		free(s);
		editerror(tmp);
	}
	elogdelete(f, q0, q1);
	nulls = 0;
	loadfile(fd, q1, &nulls, readloader, f);
	free(s);
	close(fd);
	if(nulls)
		warning(nil, "%s: NUL bytes elided\n", s);
	else if(allreplaced && samename)
		f->editclean = TRUE;
	return TRUE;
}

int
f_cmd(Text *t, Cmd *cp)
{
	Rune *name;
	String *str;
	String empty;

	if(cp->text == nil){
		empty.n = 0;
		empty.r = L"";
		str = &empty;
	}else
		str = cp->text;
	name = cmdname(t->file, str, TRUE);
	free(name);
	pfilename(t->file);
	return TRUE;
}

int
g_cmd(Text *t, Cmd *cp)
{
	if(t->file != addr.f){
		warning(nil, "internal error: g_cmd f!=addr.f\n");
		return FALSE;
	}
	if(rxcompile(cp->re->r) == FALSE)
		editerror("bad regexp in g command");
	if(rxexecute(t, nil, addr.r.q0, addr.r.q1, &sel) ^ cp->cmdc=='v'){
		t->q0 = addr.r.q0;
		t->q1 = addr.r.q1;
		return cmdexec(t, cp->cmd);
	}
	return TRUE;
}

int
i_cmd(Text *t, Cmd *cp)
{
	return append(t->file, cp, addr.r.q0);
}

void
copy(File *f, Address addr2)
{
	long p;
	int ni;
	Rune *buf;

	buf = fbufalloc();
	for(p=addr.r.q0; p<addr.r.q1; p+=ni){
		ni = addr.r.q1-p;
		if(ni > RBUFSIZE)
			ni = RBUFSIZE;
		bufread(f, p, buf, ni);
		eloginsert(addr2.f, addr2.r.q1, buf, ni);
	}
	fbuffree(buf);
}

void
move(File *f, Address addr2)
{
	if(addr.f!=addr2.f || addr.r.q1<=addr2.r.q0){
		elogdelete(f, addr.r.q0, addr.r.q1);
		copy(f, addr2);
	}else if(addr.r.q0 >= addr2.r.q1){
		copy(f, addr2);
		elogdelete(f, addr.r.q0, addr.r.q1);
	}else if(addr.r.q0==addr2.r.q0 && addr.r.q1==addr2.r.q1){
		;	/* move to self; no-op */
	}else
		editerror("move overlaps itself");
}

int
m_cmd(Text *t, Cmd *cp)
{
	Address dot, addr2;

	mkaddr(&dot, t->file);
	addr2 = cmdaddress(cp->mtaddr, dot, 0);
	if(cp->cmdc == 'm')
		move(t->file, addr2);
	else
		copy(t->file, addr2);
	return TRUE;
}

int
p_cmd(Text *t, Cmd*)
{
	return pdisplay(t->file);
}

int
s_cmd(Text *t, Cmd *cp)
{
	int i, j, k, c, m, n, nrp, didsub;
	long p1, op, delta;
	String *buf;
	Rangeset *rp;
	char *err;
	Rune *rbuf;

	n = cp->num;
	op= -1;
	if(rxcompile(cp->re->r) == FALSE)
		editerror("bad regexp in s command");
	nrp = 0;
	rp = nil;
	delta = 0;
	didsub = FALSE;
	for(p1 = addr.r.q0; p1<=addr.r.q1 && rxexecute(t, nil, p1, addr.r.q1, &sel); ){
		if(sel.r[0].q0 == sel.r[0].q1){	/* empty match? */
			if(sel.r[0].q0 == op){
				p1++;
				continue;
			}
			p1 = sel.r[0].q1+1;
		}else
			p1 = sel.r[0].q1;
		op = sel.r[0].q1;
		if(--n>0)
			continue;
		nrp++;
		rp = erealloc(rp, nrp*sizeof(Rangeset));
		rp[nrp-1] = sel;
	}
	rbuf = fbufalloc();
	buf = allocstring(0);
	for(m=0; m<nrp; m++){
		buf->n = 0;
		buf->r[0] = L'\0';
		sel = rp[m];
		for(i = 0; i<cp->text->n; i++)
			if((c = cp->text->r[i])=='\\' && i<cp->text->n-1){
				c = cp->text->r[++i];
				if('1'<=c && c<='9') {
					j = c-'0';
					if(sel.r[j].q1-sel.r[j].q0>RBUFSIZE){
						err = "replacement string too long";
						goto Err;
					}
					bufread(t->file, sel.r[j].q0, rbuf, sel.r[j].q1-sel.r[j].q0);
					for(k=0; k<sel.r[j].q1-sel.r[j].q0; k++)
						Straddc(buf, rbuf[k]);
				}else
				 	Straddc(buf, c);
			}else if(c!='&')
				Straddc(buf, c);
			else{
				if(sel.r[0].q1-sel.r[0].q0>RBUFSIZE){
					err = "right hand side too long in substitution";
					goto Err;
				}
				bufread(t->file, sel.r[0].q0, rbuf, sel.r[0].q1-sel.r[0].q0);
				for(k=0; k<sel.r[0].q1-sel.r[0].q0; k++)
					Straddc(buf, rbuf[k]);
			}
		elogreplace(t->file, sel.r[0].q0, sel.r[0].q1,  buf->r, buf->n);
		delta -= sel.r[0].q1-sel.r[0].q0;
		delta += buf->n;
		didsub = 1;
		if(!cp->flag)
			break;
	}
	free(rp);
	freestring(buf);
	fbuffree(rbuf);
	if(!didsub && nest==0)
		editerror("no substitution");
	t->q0 = addr.r.q0;
	t->q1 = addr.r.q1;
	return TRUE;

Err:
	free(rp);
	freestring(buf);
	fbuffree(rbuf);
	editerror(err);
	return FALSE;
}

int
u_cmd(Text *t, Cmd *cp)
{
	int n, oseq, flag;

	n = cp->num;
	flag = TRUE;
	if(n < 0){
		n = -n;
		flag = FALSE;
	}
	oseq = -1;
	while(n-->0 && t->file->seq!=0 && t->file->seq!=oseq){
		oseq = t->file->seq;
		undo(t, nil, nil, flag, 0, nil, 0);
	}
	return TRUE;
}

int
w_cmd(Text *t, Cmd *cp)
{
	Rune *r;
	File *f;

	f = t->file;
	if(f->seq == seq)
		editerror("can't write file with pending modifications");
	r = cmdname(f, cp->text, FALSE);
	if(r == nil)
		editerror("no name specified for 'w' command");
	putfile(f, addr.r.q0, addr.r.q1, r, runestrlen(r));
	/* r is freed by putfile */
	return TRUE;
}

int
x_cmd(Text *t, Cmd *cp)
{
	if(cp->re)
		looper(t->file, cp, cp->cmdc=='x');
	else
		linelooper(t->file, cp);
	return TRUE;
}

int
X_cmd(Text *t, Cmd *cp)
{
	filelooper(t, cp, cp->cmdc=='X');
	return TRUE;
}

void
runpipe(Text *t, int cmd, Rune *cr, int ncr, int state)
{
	Rune *r, *s;
	int n;
	Runestr dir;
	Window *w;

	r = skipbl(cr, ncr, &n);
	if(n == 0)
		editerror("no command specified for %c", cmd);
	w = nil;
	if(state == Inserting){
		w = t->w;
		t->q0 = addr.r.q0;
		t->q1 = addr.r.q1;
		if(cmd == '<' || cmd=='|')
			elogdelete(t->file, t->q0, t->q1);
	}
	s = runemalloc(n+2);
	s[0] = cmd;
	runemove(s+1, r, n);
	n++;
	dir.r = nil;
	dir.nr = 0;
	if(t != nil)
		dir = dirname(t, nil, 0);
	if(dir.nr==1 && dir.r[0]=='.'){	/* sigh */
		free(dir.r);
		dir.r = nil;
		dir.nr = 0;
	}
	editing = state;
	if(t!=nil && t->w!=nil)
		incref(t->w);	/* run will decref */
	run(w, runetobyte(s, n), dir.r, dir.nr, TRUE, nil, nil, TRUE);
	free(s);
	if(t!=nil && t->w!=nil)
		winunlock(t->w);
	qunlock(&row);
	recvul(cedit);
	qlock(&row);
	editing = Inactive;
	if(t!=nil && t->w!=nil)
		winlock(t->w, 'M');
}

int
pipe_cmd(Text *t, Cmd *cp)
{
	runpipe(t, cp->cmdc, cp->text->r, cp->text->n, Inserting);
	return TRUE;
}

long
nlcount(Text *t, long q0, long q1, long *pnr)
{
	long nl, start;
	Rune *buf;
	int i, nbuf;

	buf = fbufalloc();
	nbuf = 0;
	i = nl = 0;
	start = q0;
	while(q0 < q1){
		if(i == nbuf){
			nbuf = q1-q0;
			if(nbuf > RBUFSIZE)
				nbuf = RBUFSIZE;
			bufread(t->file, q0, buf, nbuf);
			i = 0;
		}
		if(buf[i++] == '\n'){
			start = q0+1;
			nl++;
		}
		q0++;
	}
	fbuffree(buf);
	if(pnr != nil)
		*pnr = q0 - start;
	return nl;
}

enum {
	PosnLine = 0,
	PosnChars = 1,
	PosnLineChars = 2,
};

void
printposn(Text *t, int mode)
{
	long l1, l2, r1, r2;

	if (t != nil && t->file != nil && t->file->name != nil)
		warning(nil, "%.*S:", t->file->nname, t->file->name);
	switch(mode) {
	case PosnChars:
		warning(nil, "#%d", addr.r.q0);
		if(addr.r.q1 != addr.r.q0)
			warning(nil, ",#%d", addr.r.q1);
		warning(nil, "\n");
		return;
	default:
	case PosnLine:
		l1 = 1+nlcount(t, 0, addr.r.q0, nil);
		l2 = l1+nlcount(t, addr.r.q0, addr.r.q1, nil);
		/* check if addr ends with '\n' */
		if(addr.r.q1>0 && addr.r.q1>addr.r.q0 && textreadc(t, addr.r.q1-1)=='\n')
			--l2;
		warning(nil, "%lud", l1);
		if(l2 != l1)
			warning(nil, ",%lud", l2);
		warning(nil, "\n");
		return;
	case PosnLineChars:
		l1 = 1+nlcount(t, 0, addr.r.q0, &r1);
		l2 = l1+nlcount(t, addr.r.q0, addr.r.q1, &r2);
		if(l2 == l1)
			r2 += r1;
		warning(nil, "%lud+#%lud", l1, r1);
		if(l2 != l1)
			warning(nil, ",%lud+#%lud", l2, r2);
		warning(nil, "\n");
		return;
	}
}

int
eq_cmd(Text *t, Cmd *cp)
{
	int mode;

	switch(cp->text->n){
	case 0:
		mode = PosnLine;
		break;
	case 1:
		if(cp->text->r[0] == '#'){
			mode = PosnChars;
			break;
		}
		if(cp->text->r[0] == '+'){
			mode = PosnLineChars;
			break;
		}
	default:
		SET(mode);
		editerror("newline expected");
	}
	printposn(t, mode);
	return TRUE;
}

int
nl_cmd(Text *t, Cmd *cp)
{
	Address a;
	File *f;

	f = t->file;
	if(cp->addr == 0){
		/* First put it on newline boundaries */
		mkaddr(&a, f);
		addr = lineaddr(0, a, -1);
		a = lineaddr(0, a, 1);
		addr.r.q1 = a.r.q1;
		if(addr.r.q0==t->q0 && addr.r.q1==t->q1){
			mkaddr(&a, f);
			addr = lineaddr(1, a, 1);
		}
	}
	textshow(t, addr.r.q0, addr.r.q1, 1);
	return TRUE;
}

int
append(File *f, Cmd *cp, long p)
{
	if(cp->text->n > 0)
		eloginsert(f, p, cp->text->r, cp->text->n);
	f->curtext->q0 = p;
	f->curtext->q1 = p;
	return TRUE;
}

int
pdisplay(File *f)
{
	long p1, p2;
	int np;
	Rune *buf;

	p1 = addr.r.q0;
	p2 = addr.r.q1;
	if(p2 > f->nc)
		p2 = f->nc;
	buf = fbufalloc();
	while(p1 < p2){
		np = p2-p1;
		if(np>RBUFSIZE-1)
			np = RBUFSIZE-1;
		bufread(f, p1, buf, np);
		buf[np] = L'\0';
		warning(nil, "%S", buf);
		p1 += np;
	}
	fbuffree(buf);
	f->curtext->q0 = addr.r.q0;
	f->curtext->q1 = addr.r.q1;
	return TRUE;
}

void
pfilename(File *f)
{
	int dirty;
	Window *w;

	w = f->curtext->w;
	/* same check for dirty as in settag, but we know ncache==0 */
	dirty = !w->isdir && !w->isscratch && f->mod;
	warning(nil, "%c%c%c %.*S\n", " '"[dirty],
		'+', " ."[curtext!=nil && curtext->file==f], f->nname, f->name);
}

void
loopcmd(File *f, Cmd *cp, Range *rp, long nrp)
{
	long i;

	for(i=0; i<nrp; i++){
		f->curtext->q0 = rp[i].q0;
		f->curtext->q1 = rp[i].q1;
		cmdexec(f->curtext, cp);
	}
}

void
looper(File *f, Cmd *cp, int xy)
{
	long p, op, nrp;
	Range r, tr;
	Range *rp;

	r = addr.r;
	op= xy? -1 : r.q0;
	nest++;
	if(rxcompile(cp->re->r) == FALSE)
		editerror("bad regexp in %c command", cp->cmdc);
	nrp = 0;
	rp = nil;
	for(p = r.q0; p<=r.q1; ){
		if(!rxexecute(f->curtext, nil, p, r.q1, &sel)){ /* no match, but y should still run */
			if(xy || op>r.q1)
				break;
			tr.q0 = op, tr.q1 = r.q1;
			p = r.q1+1;	/* exit next loop */
		}else{
			if(sel.r[0].q0==sel.r[0].q1){	/* empty match? */
				if(sel.r[0].q0==op){
					p++;
					continue;
				}
				p = sel.r[0].q1+1;
			}else
				p = sel.r[0].q1;
			if(xy)
				tr = sel.r[0];
			else
				tr.q0 = op, tr.q1 = sel.r[0].q0;
		}
		op = sel.r[0].q1;
		nrp++;
		rp = erealloc(rp, nrp*sizeof(Range));
		rp[nrp-1] = tr;
	}
	loopcmd(f, cp->cmd, rp, nrp);
	free(rp);
	--nest;
}

void
linelooper(File *f, Cmd *cp)
{
	long nrp, p;
	Range r, linesel;
	Address a, a3;
	Range *rp;

	nest++;
	nrp = 0;
	rp = nil;
	r = addr.r;
	a3.f = f;
	a3.r.q0 = a3.r.q1 = r.q0;
	a = lineaddr(0, a3, 1);
	linesel = a.r;
	for(p = r.q0; p<r.q1; p = a3.r.q1){
		a3.r.q0 = a3.r.q1;
		if(p!=r.q0 || linesel.q1==p){
			a = lineaddr(1, a3, 1);
			linesel = a.r;
		}
		if(linesel.q0 >= r.q1)
			break;
		if(linesel.q1 >= r.q1)
			linesel.q1 = r.q1;
		if(linesel.q1 > linesel.q0)
			if(linesel.q0>=a3.r.q1 && linesel.q1>a3.r.q1){
				a3.r = linesel;
				nrp++;
				rp = erealloc(rp, nrp*sizeof(Range));
				rp[nrp-1] = linesel;
				continue;
			}
		break;
	}
	loopcmd(f, cp->cmd, rp, nrp);
	free(rp);
	--nest;
}

struct Looper
{
	Cmd *cp;
	int	XY;
	Window	**w;
	int	nw;
} loopstruct;	/* only one; X and Y can't nest */

void
alllooper(Window *w, void *v)
{
	Text *t;
	struct Looper *lp;
	Cmd *cp;

	lp = v;
	cp = lp->cp;
//	if(w->isscratch || w->isdir)
//		return;
	t = &w->body;
	/* only use this window if it's the current window for the file */
	if(t->file->curtext != t)
		return;
//	if(w->nopen[QWevent] > 0)
//		return;
	/* no auto-execute on files without names */
	if(cp->re==nil && t->file->nname==0)
		return;
	if(cp->re==nil || filematch(t->file, cp->re)==lp->XY){
		lp->w = erealloc(lp->w, (lp->nw+1)*sizeof(Window*));
		lp->w[lp->nw++] = w;
	}
}

void
alllocker(Window *w, void *v)
{
	if(v)
		incref(w);
	else
		winclose(w);
}

void
filelooper(Text *t, Cmd *cp, int XY)
{
	int i;
	Text *targ;

	if(Glooping++)
		editerror("can't nest %c command", "YX"[XY]);
	nest++;

	loopstruct.cp = cp;
	loopstruct.XY = XY;
	if(loopstruct.w)	/* error'ed out last time */
		free(loopstruct.w);
	loopstruct.w = nil;
	loopstruct.nw = 0;
	allwindows(alllooper, &loopstruct);
	/*
	 * add a ref to all windows to keep safe windows accessed by X
	 * that would not otherwise have a ref to hold them up during
	 * the shenanigans.  note this with globalincref so that any
	 * newly created windows start with an extra reference.
	 */
	allwindows(alllocker, (void*)1);
	globalincref = 1;
	/*
	 * Unlock the window running the X command.
	 * We'll need to lock and unlock each target window in turn.
	 */
	if(t && t->w)
		winunlock(t->w);
	for(i=0; i<loopstruct.nw; i++){
		targ = &loopstruct.w[i]->body;
		if(targ && targ->w)
			winlock(targ->w, cp->cmdc);
		cmdexec(targ, cp->cmd);
		if(targ && targ->w)
			winunlock(targ->w);
	}
	if(t && t->w)
		winlock(t->w, cp->cmdc);
	allwindows(alllocker, (void*)0);
	globalincref = 0;
	free(loopstruct.w);
	loopstruct.w = nil;

	--Glooping;
	--nest;
}

void
nextmatch(File *f, String *r, long p, int sign)
{
	if(rxcompile(r->r) == FALSE)
		editerror("bad regexp in command address");
	if(sign >= 0){
		if(!rxexecute(f->curtext, nil, p, 0x7FFFFFFFL, &sel))
			editerror("no match for regexp");
		if(sel.r[0].q0==sel.r[0].q1 && sel.r[0].q0==p){
			if(++p>f->nc)
				p = 0;
			if(!rxexecute(f->curtext, nil, p, 0x7FFFFFFFL, &sel))
				editerror("address");
		}
	}else{
		if(!rxbexecute(f->curtext, p, &sel))
			editerror("no match for regexp");
		if(sel.r[0].q0==sel.r[0].q1 && sel.r[0].q1==p){
			if(--p<0)
				p = f->nc;
			if(!rxbexecute(f->curtext, p, &sel))
				editerror("address");
		}
	}
}

File	*matchfile(String*);
Address	charaddr(long, Address, int);
Address	lineaddr(long, Address, int);

Address
cmdaddress(Addr *ap, Address a, int sign)
{
	File *f = a.f;
	Address a1, a2;

	do{
		switch(ap->type){
		case 'l':
		case '#':
			a = (*(ap->type=='#'?charaddr:lineaddr))(ap->num, a, sign);
			break;

		case '.':
			mkaddr(&a, f);
			break;

		case '$':
			a.r.q0 = a.r.q1 = f->nc;
			break;

		case '\'':
editerror("can't handle '");
//			a.r = f->mark;
			break;

		case '?':
			sign = -sign;
			if(sign == 0)
				sign = -1;
			/* fall through */
		case '/':
			nextmatch(f, ap->re, sign>=0? a.r.q1 : a.r.q0, sign);
			a.r = sel.r[0];
			break;

		case '"':
			f = matchfile(ap->re);
			mkaddr(&a, f);
			break;

		case '*':
			a.r.q0 = 0, a.r.q1 = f->nc;
			return a;

		case ',':
		case ';':
			if(ap->left)
				a1 = cmdaddress(ap->left, a, 0);
			else
				a1.f = a.f, a1.r.q0 = a1.r.q1 = 0;
			if(ap->type == ';'){
				f = a1.f;
				a = a1;
				f->curtext->q0 = a1.r.q0;
				f->curtext->q1 = a1.r.q1;
			}
			if(ap->next)
				a2 = cmdaddress(ap->next, a, 0);
			else
				a2.f = a.f, a2.r.q0 = a2.r.q1 = f->nc;
			if(a1.f != a2.f)
				editerror("addresses in different files");
			a.f = a1.f, a.r.q0 = a1.r.q0, a.r.q1 = a2.r.q1;
			if(a.r.q1 < a.r.q0)
				editerror("addresses out of order");
			return a;

		case '+':
		case '-':
			sign = 1;
			if(ap->type == '-')
				sign = -1;
			if(ap->next==0 || ap->next->type=='+' || ap->next->type=='-')
				a = lineaddr(1L, a, sign);
			break;
		default:
			error("cmdaddress");
			return a;
		}
	}while(ap = ap->next);	/* assign = */
	return a;
}

struct Tofile{
	File		*f;
	String	*r;
};

void
alltofile(Window *w, void *v)
{
	Text *t;
	struct Tofile *tp;

	tp = v;
	if(tp->f != nil)
		return;
	if(w->isscratch || w->isdir)
		return;
	t = &w->body;
	/* only use this window if it's the current window for the file */
	if(t->file->curtext != t)
		return;
//	if(w->nopen[QWevent] > 0)
//		return;
	if(runeeq(tp->r->r, tp->r->n, t->file->name, t->file->nname))
		tp->f = t->file;
}

File*
tofile(String *r)
{
	struct Tofile t;
	String rr;

	rr.r = skipbl(r->r, r->n, &rr.n);
	t.f = nil;
	t.r = &rr;
	allwindows(alltofile, &t);
	if(t.f == nil)
		editerror("no such file\"%S\"", rr.r);
	return t.f;
}

void
allmatchfile(Window *w, void *v)
{
	struct Tofile *tp;
	Text *t;

	tp = v;
	if(w->isscratch || w->isdir)
		return;
	t = &w->body;
	/* only use this window if it's the current window for the file */
	if(t->file->curtext != t)
		return;
//	if(w->nopen[QWevent] > 0)
//		return;
	if(filematch(w->body.file, tp->r)){
		if(tp->f != nil)
			editerror("too many files match \"%S\"", tp->r->r);
		tp->f = w->body.file;
	}
}

File*
matchfile(String *r)
{
	struct Tofile tf;

	tf.f = nil;
	tf.r = r;
	allwindows(allmatchfile, &tf);

	if(tf.f == nil)
		editerror("no file matches \"%S\"", r->r);
	return tf.f;
}

int
filematch(File *f, String *r)
{
	char *buf;
	Rune *rbuf;
	Window *w;
	int match, i, dirty;
	Rangeset s;

	/* compile expr first so if we get an error, we haven't allocated anything */
	if(rxcompile(r->r) == FALSE)
		editerror("bad regexp in file match");
	buf = fbufalloc();
	w = f->curtext->w;
	/* same check for dirty as in settag, but we know ncache==0 */
	dirty = !w->isdir && !w->isscratch && f->mod;
	snprint(buf, BUFSIZE, "%c%c%c %.*S\n", " '"[dirty],
		'+', " ."[curtext!=nil && curtext->file==f], f->nname, f->name);
	rbuf = bytetorune(buf, &i);
	fbuffree(buf);
	match = rxexecute(nil, rbuf, 0, i, &s);
	free(rbuf);
	return match;
}

Address
charaddr(long l, Address addr, int sign)
{
	if(sign == 0)
		addr.r.q0 = addr.r.q1 = l;
	else if(sign < 0)
		addr.r.q1 = addr.r.q0 -= l;
	else if(sign > 0)
		addr.r.q0 = addr.r.q1 += l;
	if(addr.r.q0<0 || addr.r.q1>addr.f->nc)
		editerror("address out of range");
	return addr;
}

Address
lineaddr(long l, Address addr, int sign)
{
	int n;
	int c;
	File *f = addr.f;
	Address a;
	long p;

	a.f = f;
	if(sign >= 0){
		if(l == 0){
			if(sign==0 || addr.r.q1==0){
				a.r.q0 = a.r.q1 = 0;
				return a;
			}
			a.r.q0 = addr.r.q1;
			p = addr.r.q1-1;
		}else{
			if(sign==0 || addr.r.q1==0){
				p = 0;
				n = 1;
			}else{
				p = addr.r.q1-1;
				n = textreadc(f->curtext, p++)=='\n';
			}
			while(n < l){
				if(p >= f->nc)
					editerror("address out of range");
				if(textreadc(f->curtext, p++) == '\n')
					n++;
			}
			a.r.q0 = p;
		}
		while(p < f->nc && textreadc(f->curtext, p++)!='\n')
			;
		a.r.q1 = p;
	}else{
		p = addr.r.q0;
		if(l == 0)
			a.r.q1 = addr.r.q0;
		else{
			for(n = 0; n<l; ){	/* always runs once */
				if(p == 0){
					if(++n != l)
						editerror("address out of range");
				}else{
					c = textreadc(f->curtext, p-1);
					if(c != '\n' || ++n != l)
						p--;
				}
			}
			a.r.q1 = p;
			if(p > 0)
				p--;
		}
		while(p > 0 && textreadc(f->curtext, p-1)!='\n')	/* lines start after a newline */
			p--;
		a.r.q0 = p;
	}
	return a;
}

struct Filecheck
{
	File	*f;
	Rune	*r;
	int nr;
};

void
allfilecheck(Window *w, void *v)
{
	struct Filecheck *fp;
	File *f;

	fp = v;
	f = w->body.file;
	if(w->body.file == fp->f)
		return;
	if(runeeq(fp->r, fp->nr, f->name, f->nname))
		warning(nil, "warning: duplicate file name \"%.*S\"\n", fp->nr, fp->r);
}

Rune*
cmdname(File *f, String *str, int set)
{
	Rune *r, *s;
	int n;
	struct Filecheck fc;
	Runestr newname;

	r = nil;
	n = str->n;
	s = str->r;
	if(n == 0){
		/* no name; use existing */
		if(f->nname == 0)
			return nil;
		r = runemalloc(f->nname+1);
		runemove(r, f->name, f->nname);
		return r;
	}
	s = skipbl(s, n, &n);
	if(n == 0)
		goto Return;

	if(s[0] == '/'){
		r = runemalloc(n+1);
		runemove(r, s, n);
	}else{
		newname = dirname(f->curtext, runestrdup(s), n);
		n = newname.nr;
		r = runemalloc(n+1);	/* NUL terminate */
		runemove(r, newname.r, n);
		free(newname.r);
	}
	fc.f = f;
	fc.r = r;
	fc.nr = n;
	allwindows(allfilecheck, &fc);
	if(f->nname == 0)
		set = TRUE;

    Return:
	if(set && !runeeq(r, n, f->name, f->nname)){
		filemark(f);
		f->mod = TRUE;
		f->curtext->w->dirty = TRUE;
		winsetname(f->curtext->w, r, n);
	}
	return r;
}