xref: /inferno-os/appl/acme/ecmd.b (revision 9765fcf68d2e5e39e39c100f798b9d00202e0d0a)
1implement Editcmd;
2
3include "common.m";
4
5sys: Sys;
6utils: Utils;
7edit: Edit;
8editlog: Editlog;
9windowm: Windowm;
10look: Look;
11columnm: Columnm;
12bufferm: Bufferm;
13exec: Exec;
14dat: Dat;
15textm: Textm;
16regx: Regx;
17filem: Filem;
18rowm: Rowm;
19
20Dir: import Sys;
21Allwin, Filecheck, Tofile, Looper, Astring: import Dat;
22aNo, aDot, aAll: import Edit;
23C_nl, C_a, C_b, C_c, C_d, C_B, C_D, C_e, C_f, C_g, C_i, C_k, C_m, C_n, C_p, C_s, C_u, C_w, C_x, C_X, C_pipe, C_eq: import Edit;
24TRUE, FALSE: import Dat;
25Inactive, Inserting, Collecting: import Dat;
26BUFSIZE, Runestr: import Dat;
27Addr, Address, String, Cmd: import Edit;
28Window: import windowm;
29File: import filem;
30NRange, Range, Rangeset: import Dat;
31Text: import textm;
32Column: import columnm;
33Buffer: import bufferm;
34
35sprint: import sys;
36elogterm, elogclose, eloginsert, elogdelete, elogreplace, elogapply: import editlog;
37cmdtab, allocstring, freestring, Straddc, curtext, editing, newaddr, cmdlookup, editerror: import edit;
38error, stralloc, strfree, warning, skipbl, findbl: import utils;
39lookfile, cleanname, dirname: import look;
40undo, run: import exec;
41Ref, Lock, row, cedit: import dat;
42rxcompile, rxexecute, rxbexecute: import regx;
43allwindows: import rowm;
44
45init(mods : ref Dat->Mods)
46{
47	sys = mods.sys;
48	utils = mods.utils;
49	edit = mods.edit;
50	editlog = mods.editlog;
51	windowm = mods.windowm;
52	look = mods.look;
53	columnm = mods.columnm;
54	bufferm = mods.bufferm;
55	exec = mods.exec;
56	dat = mods.dat;
57	textm = mods.textm;
58	regx = mods.regx;
59	filem = mods.filem;
60	rowm = mods.rowm;
61
62	none.r.q0 = none.r.q1 = 0;
63	none.f = nil;
64}
65
66cmdtabexec(i: int, t: ref Text, cp: ref Cmd): int
67{
68	case (cmdtab[i].fnc){
69		C_nl	=> i = nl_cmd(t, cp);
70		C_a 	=> i = a_cmd(t, cp);
71		C_b	=> i = b_cmd(t, cp);
72		C_c	=> i = c_cmd(t, cp);
73		C_d	=> i = d_cmd(t, cp);
74		C_e	=> i = e_cmd(t, cp);
75		C_f	=> i = f_cmd(t, cp);
76		C_g	=> i = g_cmd(t, cp);
77		C_i	=> i = i_cmd(t, cp);
78		C_m	=> i = m_cmd(t, cp);
79		C_p	=> i = p_cmd(t, cp);
80		C_s	=> i = s_cmd(t, cp);
81		C_u	=> i = u_cmd(t, cp);
82		C_w	=> i = w_cmd(t, cp);
83		C_x	=> i = x_cmd(t, cp);
84		C_eq => i = eq_cmd(t, cp);
85		C_B	=> i = B_cmd(t, cp);
86		C_D	=> i = D_cmd(t, cp);
87		C_X	=> i = X_cmd(t, cp);
88		C_pipe	=> i = pipe_cmd(t, cp);
89		* =>	error("bad case in cmdtabexec");
90	}
91	return i;
92}
93
94Glooping: int;
95nest: int;
96Enoname := "no file name given";
97
98addr: Address;
99menu: ref File;
100sel: Rangeset;
101collection: string;
102ncollection: int;
103
104clearcollection()
105{
106	collection = nil;
107	ncollection = 0;
108}
109
110resetxec()
111{
112	Glooping = nest = 0;
113	clearcollection();
114}
115
116mkaddr(f: ref File): Address
117{
118	a: Address;
119
120	a.r.q0 = f.curtext.q0;
121	a.r.q1 = f.curtext.q1;
122	a.f = f;
123	return a;
124}
125
126none: Address;
127
128cmdexec(t: ref Text, cp: ref Cmd): int
129{
130	i: int;
131	ap: ref Addr;
132	f: ref File;
133	w: ref Window;
134	dot: Address;
135
136	if(t == nil)
137		w = nil;
138	else
139		w = t.w;
140	if(w==nil && (cp.addr==nil || cp.addr.typex!='"') &&
141	    utils->strchr("bBnqUXY!", cp.cmdc) < 0&&
142	    !(cp.cmdc=='D' && cp.text!=nil))
143		editerror("no current window");
144	i = cmdlookup(cp.cmdc);	# will be -1 for '{'
145	f = nil;
146	if(t!=nil && t.w!=nil){
147		t = t.w.body;
148		f = t.file;
149		f.curtext = t;
150	}
151	if(i>=0 && cmdtab[i].defaddr != aNo){
152		if((ap=cp.addr)==nil && cp.cmdc!='\n'){
153			cp.addr = ap = newaddr();
154			ap.typex = '.';
155			if(cmdtab[i].defaddr == aAll)
156				ap.typex = '*';
157		}else if(ap!=nil && ap.typex=='"' && ap.next==nil && cp.cmdc!='\n'){
158			ap.next = newaddr();
159			ap.next.typex = '.';
160			if(cmdtab[i].defaddr == aAll)
161				ap.next.typex = '*';
162		}
163		if(cp.addr!=nil){	# may be false for '\n' (only)
164			if(f!=nil){
165				dot = mkaddr(f);
166				addr = cmdaddress(ap, dot, 0);
167			}else	# a "
168				addr = cmdaddress(ap, none, 0);
169			f = addr.f;
170			t = f.curtext;
171		}
172	}
173	case(cp.cmdc){
174	'{' =>
175		dot = mkaddr(f);
176		if(cp.addr != nil)
177			dot = cmdaddress(cp.addr, dot, 0);
178		for(cp = cp.cmd; cp!=nil; cp = cp.next){
179			t.q0 = dot.r.q0;
180			t.q1 = dot.r.q1;
181			cmdexec(t, cp);
182		}
183		break;
184	* =>
185		if(i < 0)
186			editerror(sprint("unknown command %c in cmdexec", cp.cmdc));
187		i = cmdtabexec(i, t, cp);
188		return i;
189	}
190	return 1;
191}
192
193edittext(f: ref File, q: int, r: string, nr: int): string
194{
195	case(editing){
196	Inactive =>
197		return "permission denied";
198	Inserting =>
199		eloginsert(f, q, r, nr);
200		return nil;
201	Collecting =>
202		collection += r[0: nr];
203		ncollection += nr;
204		return nil;
205	* =>
206		return "unknown state in edittext";
207	}
208}
209
210# string is known to be NUL-terminated
211filelist(t: ref Text, r: string, nr: int): string
212{
213	if(nr == 0)
214		return nil;
215	(r, nr) = skipbl(r, nr);
216	if(r[0] != '<')
217		return r;
218	# use < command to collect text
219	clearcollection();
220	runpipe(t, '<', r[1:], nr-1, Collecting);
221	return collection;
222}
223
224a_cmd(t: ref Text, cp: ref Cmd): int
225{
226	return append(t.file, cp, addr.r.q1);
227}
228
229b_cmd(nil: ref Text, cp: ref Cmd): int
230{
231	f: ref File;
232
233	f = tofile(cp.text);
234	if(nest == 0)
235		pfilename(f);
236	curtext = f.curtext;
237	return TRUE;
238}
239
240B_cmd(t: ref Text, cp: ref Cmd): int
241{
242	listx, r, s: string;
243	nr: int;
244
245	listx = filelist(t, cp.text.r, cp.text.n);
246	if(listx == nil)
247		editerror(Enoname);
248	r = listx;
249	nr = len r;
250	(r, nr) = skipbl(r, nr);
251	if(nr == 0)
252		look->new(t, t, nil, 0, 0, r, 0);
253	else while(nr > 0){
254		(s, nr) = findbl(r, nr);
255		look->new(t, t, nil, 0, 0, r, len r);
256		if(nr > 0)
257			(r, nr) = skipbl(s[1:], nr-1);
258	}
259	clearcollection();
260	return TRUE;
261}
262
263c_cmd(t: ref Text, cp: ref Cmd): int
264{
265	elogreplace(t.file, addr.r.q0, addr.r.q1, cp.text.r, cp.text.n);
266	return TRUE;
267}
268
269d_cmd(t: ref Text, nil: ref Cmd): int
270{
271	if(addr.r.q1 > addr.r.q0)
272		elogdelete(t.file, addr.r.q0, addr.r.q1);
273	return TRUE;
274}
275
276D1(t: ref Text)
277{
278	if(t.w.body.file.ntext>1 || t.w.clean(FALSE, FALSE))
279		t.col.close(t.w, TRUE);
280}
281
282D_cmd(t: ref Text, cp: ref Cmd): int
283{
284	listx, r, s, n: string;
285	nr, nn: int;
286	w: ref Window;
287	dir, rs: Runestr;
288	buf: string;
289
290	listx = filelist(t, cp.text.r, cp.text.n);
291	if(listx == nil){
292		D1(t);
293		return TRUE;
294	}
295	dir = dirname(t, nil, 0);
296	r = listx;
297	nr = len r;
298	(r, nr) = skipbl(r, nr);
299	do{
300		(s, nr) = findbl(r, nr);
301		# first time through, could be empty string, meaning delete file empty name
302		nn = len r;
303		if(r[0]=='/' || nn==0 || dir.nr==0){
304			rs.r = r;
305			rs.nr = nn;
306		}else{
307			n = dir.r + "/" + r;
308			rs = cleanname(n, dir.nr+1+nn);
309		}
310		w = lookfile(rs.r, rs.nr);
311		if(w == nil){
312			buf = sprint("no such file %s", rs.r);
313			rs.r = nil;
314			editerror(buf);
315		}
316		rs.r = nil;
317		D1(w.body);
318		if(nr > 0)
319			(r, nr) = skipbl(s[1:], nr-1);
320	}while(nr > 0);
321	clearcollection();
322	dir.r = nil;
323	return TRUE;
324}
325
326readloader(f: ref File, q0: int, r: string, nr: int): int
327{
328	if(nr > 0)
329		eloginsert(f, q0, r, nr);
330	return 0;
331}
332
333e_cmd(t: ref Text , cp: ref Cmd): int
334{
335	name: string;
336	f: ref File;
337	i, q0, q1, nulls, samename, allreplaced, ok: int;
338	fd: ref Sys->FD;
339	s, tmp: string;
340	d: Dir;
341
342	f = t.file;
343	q0 = addr.r.q0;
344	q1 = addr.r.q1;
345	if(cp.cmdc == 'e'){
346		if(t.w.clean(TRUE, FALSE)==FALSE)
347			editerror("");	# winclean generated message already
348		q0 = 0;
349		q1 = f.buf.nc;
350	}
351	allreplaced = (q0==0 && q1==f.buf.nc);
352	name = cmdname(f, cp.text, cp.cmdc=='e');
353	if(name == nil)
354		editerror(Enoname);
355	i = len name;
356	samename = name == t.file.name;
357	s = name;
358	name = nil;
359	fd = sys->open(s, Sys->OREAD);
360	if(fd == nil){
361		tmp = sprint("can't open %s: %r", s);
362		s = nil;
363		editerror(tmp);
364	}
365	(ok, d) = sys->fstat(fd);
366	if(ok >=0 && (d.mode&Sys->DMDIR)){
367		fd = nil;
368		tmp = sprint("%s is a directory", s);
369		s = nil;
370		editerror(tmp);
371	}
372	elogdelete(f, q0, q1);
373	nulls = 0;
374	bufferm->loadfile(fd, q1, Dat->READL, nil, f);
375	s = nil;
376	fd = nil;
377	if(nulls)
378		warning(nil, sprint("%s: NUL bytes elided\n", s));
379	else if(allreplaced && samename)
380		f.editclean = TRUE;
381	return TRUE;
382}
383
384f_cmd(t: ref Text, cp: ref Cmd): int
385{
386	name: string;
387
388	name = cmdname(t.file, cp.text, TRUE);
389	name = nil;
390	pfilename(t.file);
391	return TRUE;
392}
393
394g_cmd(t: ref Text, cp: ref Cmd): int
395{
396	ok: int;
397
398	if(t.file != addr.f){
399		warning(nil, "internal error: g_cmd f!=addr.f\n");
400		return FALSE;
401	}
402	if(rxcompile(cp.re.r) == FALSE)
403		editerror("bad regexp in g command");
404	(ok, sel) = rxexecute(t, nil, addr.r.q0, addr.r.q1);
405	if(ok ^ cp.cmdc=='v'){
406		t.q0 = addr.r.q0;
407		t.q1 = addr.r.q1;
408		return cmdexec(t, cp.cmd);
409	}
410	return TRUE;
411}
412
413i_cmd(t: ref Text, cp: ref Cmd): int
414{
415	return append(t.file, cp, addr.r.q0);
416}
417
418# int
419# k_cmd(File *f, Cmd *cp)
420# {
421# 	USED(cp);
422#	f->mark = addr.r;
423#	return TRUE;
424# }
425
426copy(f: ref File, addr2: Address)
427{
428	p: int;
429	ni: int;
430	buf: ref Astring;
431
432	buf = stralloc(BUFSIZE);
433	for(p=addr.r.q0; p<addr.r.q1; p+=ni){
434		ni = addr.r.q1-p;
435		if(ni > BUFSIZE)
436			ni = BUFSIZE;
437		f.buf.read(p, buf, 0, ni);
438		eloginsert(addr2.f, addr2.r.q1, buf.s, ni);
439	}
440	strfree(buf);
441}
442
443move(f: ref File, addr2: Address)
444{
445	if(addr.f!=addr2.f || addr.r.q1<=addr2.r.q0){
446		elogdelete(f, addr.r.q0, addr.r.q1);
447		copy(f, addr2);
448	}else if(addr.r.q0 >= addr2.r.q1){
449		copy(f, addr2);
450		elogdelete(f, addr.r.q0, addr.r.q1);
451	}else if(addr.r.q0==addr2.r.q0 && addr.r.q1==addr2.r.q1){
452		;	# move to self; no-op
453	}else
454		editerror("move overlaps itself");
455}
456
457m_cmd(t: ref Text, cp: ref Cmd): int
458{
459	dot, addr2: Address;
460
461	dot = mkaddr(t.file);
462	addr2 = cmdaddress(cp.mtaddr, dot, 0);
463	if(cp.cmdc == 'm')
464		move(t.file, addr2);
465	else
466		copy(t.file, addr2);
467	return TRUE;
468}
469
470# int
471# n_cmd(File *f, Cmd *cp)
472# {
473#	int i;
474#	USED(f);
475#	USED(cp);
476#	for(i = 0; i<file.nused; i++){
477#		if(file.filepptr[i] == cmd)
478#			continue;
479#		f = file.filepptr[i];
480#		Strduplstr(&genstr, &f->name);
481#		filename(f);
482#	}
483#	return TRUE;
484#}
485
486p_cmd(t: ref Text, nil: ref Cmd): int
487{
488	return pdisplay(t.file);
489}
490
491s_cmd(t: ref Text, cp: ref Cmd): int
492{
493	i, j, k, c, m, n, nrp, didsub, ok: int;
494	p1, op, delta: int;
495	buf: ref String;
496	rp: array of Rangeset;
497	err: string;
498	rbuf: ref Astring;
499
500	n = cp.num;
501	op= -1;
502	if(rxcompile(cp.re.r) == FALSE)
503		editerror("bad regexp in s command");
504	nrp = 0;
505	rp = nil;
506	delta = 0;
507	didsub = FALSE;
508	for(p1 = addr.r.q0; p1<=addr.r.q1; ){
509		(ok, sel) = rxexecute(t, nil, p1, addr.r.q1);
510		if(!ok)
511			break;
512		if(sel[0].q0 == sel[0].q1){	# empty match?
513			if(sel[0].q0 == op){
514				p1++;
515				continue;
516			}
517			p1 = sel[0].q1+1;
518		}else
519			p1 = sel[0].q1;
520		op = sel[0].q1;
521		if(--n>0)
522			continue;
523		nrp++;
524		orp := rp;
525		rp = array[nrp] of Rangeset;
526		rp[0: ] = orp[0:nrp-1];
527		rp[nrp-1] = copysel(sel);
528		orp = nil;
529	}
530	rbuf = stralloc(BUFSIZE);
531	buf = allocstring(0);
532	for(m=0; m<nrp; m++){
533		buf.n = 0;
534		buf.r = nil;
535		sel = rp[m];
536		for(i = 0; i<cp.text.n; i++)
537			if((c = cp.text.r[i])=='\\' && i<cp.text.n-1){
538				c = cp.text.r[++i];
539				if('1'<=c && c<='9') {
540					j = c-'0';
541					if(sel[j].q1-sel[j].q0>BUFSIZE){
542						err = "replacement string too long";
543						rp = nil;
544						freestring(buf);
545						strfree(rbuf);
546						editerror(err);
547						return FALSE;
548					}
549					t.file.buf.read(sel[j].q0, rbuf, 0, sel[j].q1-sel[j].q0);
550					for(k=0; k<sel[j].q1-sel[j].q0; k++)
551						Straddc(buf, rbuf.s[k]);
552				}else
553				 	Straddc(buf, c);
554			}else if(c!='&')
555				Straddc(buf, c);
556			else{
557				if(sel[0].q1-sel[0].q0>BUFSIZE){
558					err = "right hand side too long in substitution";
559					rp = nil;
560					freestring(buf);
561					strfree(rbuf);
562					editerror(err);
563					return FALSE;
564				}
565				t.file.buf.read(sel[0].q0, rbuf, 0, sel[0].q1-sel[0].q0);
566				for(k=0; k<sel[0].q1-sel[0].q0; k++)
567					Straddc(buf, rbuf.s[k]);
568			}
569		elogreplace(t.file, sel[0].q0, sel[0].q1, buf.r, buf.n);
570		delta -= sel[0].q1-sel[0].q0;
571		delta += buf.n;
572		didsub = 1;
573		if(!cp.flag)
574			break;
575	}
576	rp = nil;
577	freestring(buf);
578	strfree(rbuf);
579	if(!didsub && nest==0)
580		editerror("no substitution");
581	t.q0 = addr.r.q0;
582	t.q1 = addr.r.q1+delta;
583	return TRUE;
584}
585
586u_cmd(t: ref Text, cp: ref Cmd): int
587{
588	n, oseq, flag: int;
589
590	n = cp.num;
591	flag = TRUE;
592	if(n < 0){
593		n = -n;
594		flag = FALSE;
595	}
596	oseq = -1;
597	while(n-->0 && t.file.seq!=0 && t.file.seq!=oseq){
598		oseq = t.file.seq;
599warning(nil, sprint("seq %d\n", t.file.seq));
600		undo(t, flag);
601	}
602	return TRUE;
603}
604
605w_cmd(t: ref Text, cp: ref Cmd): int
606{
607	r: string;
608	f: ref File;
609
610	f = t.file;
611	if(f.seq == dat->seq)
612		editerror("can't write file with pending modifications");
613	r = cmdname(f, cp.text, FALSE);
614	if(r == nil)
615		editerror("no name specified for 'w' command");
616	exec->putfile(f, addr.r.q0, addr.r.q1, r);
617	# r is freed by putfile
618	return TRUE;
619}
620
621x_cmd(t: ref Text, cp: ref Cmd): int
622{
623	if(cp.re!=nil)
624		looper(t.file, cp, cp.cmdc=='x');
625	else
626		linelooper(t.file, cp);
627	return TRUE;
628}
629
630X_cmd(nil: ref Text, cp: ref Cmd): int
631{
632	filelooper(cp, cp.cmdc=='X');
633	return TRUE;
634}
635
636runpipe(t: ref Text, cmd: int, cr: string, ncr: int, state: int)
637{
638	r, s: string;
639	n: int;
640	dir: Runestr;
641	w: ref Window;
642
643	(r, n) = skipbl(cr, ncr);
644	if(n == 0)
645		editerror("no command specified for >");
646	w = nil;
647	if(state == Inserting){
648		w = t.w;
649		t.q0 = addr.r.q0;
650		t.q1 = addr.r.q1;
651		if(cmd == '<' || cmd=='|')
652			elogdelete(t.file, t.q0, t.q1);
653	}
654	tmps := "z";
655	tmps[0] = cmd;
656	s = tmps + r;
657	n++;
658	dir.r = nil;
659	dir.nr = 0;
660	if(t != nil)
661		dir = dirname(t, nil, 0);
662	if(dir.nr==1 && dir.r[0]=='.'){	# sigh
663		dir.r = nil;
664		dir.nr = 0;
665	}
666	editing = state;
667	if(t!=nil && t.w!=nil)
668		t.w.refx.inc();	# run will decref
669	spawn run(w, s, dir.r, dir.nr, TRUE, nil, nil, TRUE);
670	s = nil;
671	if(t!=nil && t.w!=nil)
672		t.w.unlock();
673	row.qlock.unlock();
674	<- cedit;
675	row.qlock.lock();
676	editing = Inactive;
677	if(t!=nil && t.w!=nil)
678		t.w.lock('M');
679}
680
681pipe_cmd(t: ref Text, cp: ref Cmd): int
682{
683	runpipe(t, cp.cmdc, cp.text.r, cp.text.n, Inserting);
684	return TRUE;
685}
686
687nlcount(t: ref Text, q0: int, q1: int): int
688{
689	nl: int;
690	buf: ref Astring;
691	i, nbuf: int;
692
693	buf = stralloc(BUFSIZE);
694	nbuf = 0;
695	i = nl = 0;
696	while(q0 < q1){
697		if(i == nbuf){
698			nbuf = q1-q0;
699			if(nbuf > BUFSIZE)
700				nbuf = BUFSIZE;
701			t.file.buf.read(q0, buf, 0, nbuf);
702			i = 0;
703		}
704		if(buf.s[i++] == '\n')
705			nl++;
706		q0++;
707	}
708	strfree(buf);
709	return nl;
710}
711
712printposn(t: ref Text, charsonly: int)
713{
714	l1, l2: int;
715
716	if(t != nil && t.file != nil && t.file.name != nil)
717		warning(nil, t.file.name + ":");
718	if(!charsonly){
719		l1 = 1+nlcount(t, 0, addr.r.q0);
720		l2 = l1+nlcount(t, addr.r.q0, addr.r.q1);
721		# check if addr ends with '\n'
722		if(addr.r.q1>0 && addr.r.q1>addr.r.q0 && t.readc(addr.r.q1-1)=='\n')
723			--l2;
724		warning(nil, sprint("%ud", l1));
725		if(l2 != l1)
726			warning(nil, sprint(",%ud", l2));
727		warning(nil, "\n");
728		# warning(nil, "; ");
729		return;
730	}
731	warning(nil, sprint("#%d", addr.r.q0));
732	if(addr.r.q1 != addr.r.q0)
733		warning(nil, sprint(",#%d", addr.r.q1));
734	warning(nil, "\n");
735}
736
737eq_cmd(t: ref Text, cp: ref Cmd): int
738{
739	charsonly: int;
740
741	case(cp.text.n){
742	0 =>
743		charsonly = FALSE;
744		break;
745	1 =>
746		if(cp.text.r[0] == '#'){
747			charsonly = TRUE;
748			break;
749		}
750	* =>
751		charsonly = TRUE;
752		editerror("newline expected");
753	}
754	printposn(t, charsonly);
755	return TRUE;
756}
757
758nl_cmd(t: ref Text, cp: ref Cmd): int
759{
760	a: Address;
761	f: ref File;
762
763	f = t.file;
764	if(cp.addr == nil){
765		# First put it on newline boundaries
766		a = mkaddr(f);
767		addr = lineaddr(0, a, -1);
768		a = lineaddr(0, a, 1);
769		addr.r.q1 = a.r.q1;
770		if(addr.r.q0==t.q0 && addr.r.q1==t.q1){
771			a = mkaddr(f);
772			addr = lineaddr(1, a, 1);
773		}
774	}
775	t.show(addr.r.q0, addr.r.q1, TRUE);
776	return TRUE;
777}
778
779append(f: ref File, cp: ref Cmd, p: int): int
780{
781	if(cp.text.n > 0)
782		eloginsert(f, p, cp.text.r, cp.text.n);
783	return TRUE;
784}
785
786pdisplay(f: ref File): int
787{
788	p1, p2: int;
789	np: int;
790	buf: ref Astring;
791
792	p1 = addr.r.q0;
793	p2 = addr.r.q1;
794	if(p2 > f.buf.nc)
795		p2 = f.buf.nc;
796	buf = stralloc(BUFSIZE);
797	while(p1 < p2){
798		np = p2-p1;
799		if(np>BUFSIZE-1)
800			np = BUFSIZE-1;
801		f.buf.read(p1, buf, 0, np);
802		warning(nil, sprint("%s", buf.s[0:np]));
803		p1 += np;
804	}
805	strfree(buf);
806	f.curtext.q0 = addr.r.q0;
807	f.curtext.q1 = addr.r.q1;
808	return TRUE;
809}
810
811pfilename(f: ref File)
812{
813	dirty: int;
814	w: ref Window;
815
816	w = f.curtext.w;
817	# same check for dirty as in settag, but we know ncache==0
818	dirty = !w.isdir && !w.isscratch && f.mod;
819	warning(nil, sprint("%c%c%c %s\n", " '"[dirty],
820		'+', " ."[curtext!=nil && curtext.file==f], f.name));
821}
822
823loopcmd(f: ref File, cp: ref Cmd, rp: array of Range, nrp: int)
824{
825	i: int;
826
827	for(i=0; i<nrp; i++){
828		f.curtext.q0 = rp[i].q0;
829		f.curtext.q1 = rp[i].q1;
830		cmdexec(f.curtext, cp);
831	}
832}
833
834looper(f: ref File, cp: ref Cmd, xy: int)
835{
836	p, op, nrp, ok: int;
837	r, tr: Range;
838	rp: array of  Range;
839
840	r = addr.r;
841	if(xy)
842		op = -1;
843	else
844		op = r.q0;
845	nest++;
846	if(rxcompile(cp.re.r) == FALSE)
847		editerror(sprint("bad regexp in %c command", cp.cmdc));
848	nrp = 0;
849	rp = nil;
850	for(p = r.q0; p<=r.q1; ){
851		(ok, sel) = rxexecute(f.curtext, nil, p, r.q1);
852		if(!ok){ # no match, but y should still run
853			if(xy || op>r.q1)
854				break;
855			tr.q0 = op;
856			tr.q1 = r.q1;
857			p = r.q1+1;	# exit next loop
858		}else{
859			if(sel[0].q0==sel[0].q1){	# empty match?
860				if(sel[0].q0==op){
861					p++;
862					continue;
863				}
864				p = sel[0].q1+1;
865			}else
866				p = sel[0].q1;
867			if(xy)
868				tr = sel[0];
869			else{
870				tr.q0 = op;
871				tr.q1 = sel[0].q0;
872			}
873		}
874		op = sel[0].q1;
875		nrp++;
876		orp := rp;
877		rp = array[nrp] of Range;
878		rp[0: ] = orp[0: nrp-1];
879		rp[nrp-1] = tr;
880		orp = nil;
881	}
882	loopcmd(f, cp.cmd, rp, nrp);
883	rp = nil;
884	--nest;
885}
886
887linelooper(f: ref File, cp: ref Cmd)
888{
889	nrp, p: int;
890	r, linesel: Range;
891	a, a3: Address;
892	rp: array of Range;
893
894	nest++;
895	nrp = 0;
896	rp = nil;
897	r = addr.r;
898	a3.f = f;
899	a3.r.q0 = a3.r.q1 = r.q0;
900	a = lineaddr(0, a3, 1);
901	linesel = a.r;
902	for(p = r.q0; p<r.q1; p = a3.r.q1){
903		a3.r.q0 = a3.r.q1;
904		if(p!=r.q0 || linesel.q1==p){
905			a = lineaddr(1, a3, 1);
906			linesel = a.r;
907		}
908		if(linesel.q0 >= r.q1)
909			break;
910		if(linesel.q1 >= r.q1)
911			linesel.q1 = r.q1;
912		if(linesel.q1 > linesel.q0)
913			if(linesel.q0>=a3.r.q1 && linesel.q1>a3.r.q1){
914				a3.r = linesel;
915				nrp++;
916				orp := rp;
917				rp = array[nrp] of Range;
918				rp[0: ] = orp[0: nrp-1];
919				rp[nrp-1] = linesel;
920				orp = nil;
921				continue;
922			}
923		break;
924	}
925	loopcmd(f, cp.cmd, rp, nrp);
926	rp = nil;
927	--nest;
928}
929
930loopstruct: ref Looper;
931
932alllooper(w: ref Window, lp: ref Looper)
933{
934	t: ref Text;
935	cp: ref Cmd;
936
937	cp = lp.cp;
938#	if(w.isscratch || w.isdir)
939#		return;
940	t = w.body;
941	# only use this window if it's the current window for the file
942	if(t.file.curtext != t)
943		return;
944#	if(w.nopen[QWevent] > 0)
945#		return;
946	# no auto-execute on files without names
947	if(cp.re==nil && t.file.name==nil)
948		return;
949	if(cp.re==nil || filematch(t.file, cp.re)==lp.XY){
950		olpw := lp.w;
951		lp.w = array[lp.nw+1] of ref Window;
952		lp.w[0: ] = olpw[0: lp.nw];
953		lp.w[lp.nw++] = w;
954		olpw = nil;
955	}
956}
957
958filelooper(cp: ref Cmd, XY: int)
959{
960	i: int;
961
962	if(Glooping++)
963		editerror(sprint("can't nest %c command", "YX"[XY]));
964	nest++;
965
966	if(loopstruct == nil)
967		loopstruct = ref Looper;
968	loopstruct.cp = cp;
969	loopstruct.XY = XY;
970	if(loopstruct.w != nil)	# error'ed out last time
971		loopstruct.w = nil;
972	loopstruct.w = nil;
973	loopstruct.nw = 0;
974	aw := ref Allwin.LP(loopstruct);
975	allwindows(Edit->ALLLOOPER, aw);
976	aw = nil;
977	for(i=0; i<loopstruct.nw; i++)
978		cmdexec(loopstruct.w[i].body, cp.cmd);
979	loopstruct.w = nil;
980
981	--Glooping;
982	--nest;
983}
984
985nextmatch(f: ref File, r: ref String, p: int, sign: int)
986{
987	ok: int;
988
989	if(rxcompile(r.r) == FALSE)
990		editerror("bad regexp in command address");
991	if(sign >= 0){
992		(ok, sel) = rxexecute(f.curtext, nil, p, 16r7FFFFFFF);
993		if(!ok)
994			editerror("no match for regexp");
995		if(sel[0].q0==sel[0].q1 && sel[0].q0==p){
996			if(++p>f.buf.nc)
997				p = 0;
998			(ok, sel) = rxexecute(f.curtext, nil, p, 16r7FFFFFFF);
999			if(!ok)
1000				editerror("address");
1001		}
1002	}else{
1003		(ok, sel) = rxbexecute(f.curtext, p);
1004		if(!ok)
1005			editerror("no match for regexp");
1006		if(sel[0].q0==sel[0].q1 && sel[0].q1==p){
1007			if(--p<0)
1008				p = f.buf.nc;
1009			(ok, sel) = rxbexecute(f.curtext, p);
1010			if(!ok)
1011				editerror("address");
1012		}
1013	}
1014}
1015
1016cmdaddress(ap: ref Addr, a: Address, sign: int): Address
1017{
1018	f := a.f;
1019	a1, a2: Address;
1020
1021	do{
1022		case(ap.typex){
1023		'l' or
1024		'#' =>
1025			if(ap.typex == '#')
1026				a = charaddr(ap.num, a, sign);
1027			else
1028				a = lineaddr(ap.num, a, sign);
1029			break;
1030
1031		'.' =>
1032			a = mkaddr(f);
1033			break;
1034
1035		'$' =>
1036			a.r.q0 = a.r.q1 = f.buf.nc;
1037			break;
1038
1039		'\'' =>
1040editerror("can't handle '");
1041#			a.r = f.mark;
1042			break;
1043
1044		'?' =>
1045			sign = -sign;
1046			if(sign == 0)
1047				sign = -1;
1048			if(sign >= 0)
1049				v := a.r.q1;
1050			else
1051				v = a.r.q0;
1052			nextmatch(f, ap.re, v, sign);
1053			a.r = sel[0];
1054			break;
1055
1056		'/' =>
1057			if(sign >= 0)
1058				v := a.r.q1;
1059			else
1060				v = a.r.q0;
1061			nextmatch(f, ap.re, v, sign);
1062			a.r = sel[0];
1063			break;
1064
1065		'"' =>
1066			f = matchfile(ap.re);
1067			a = mkaddr(f);
1068			break;
1069
1070		'*' =>
1071			a.r.q0 = 0;
1072			a.r.q1 = f.buf.nc;
1073			return a;
1074
1075		',' or
1076		';' =>
1077			if(ap.left!=nil)
1078				a1 = cmdaddress(ap.left, a, 0);
1079			else{
1080				a1.f = a.f;
1081				a1.r.q0 = a1.r.q1 = 0;
1082			}
1083			if(ap.typex == ';'){
1084				f = a1.f;
1085				a = a1;
1086				f.curtext.q0 = a1.r.q0;
1087				f.curtext.q1 = a1.r.q1;
1088			}
1089			if(ap.next!=nil)
1090				a2 = cmdaddress(ap.next, a, 0);
1091			else{
1092				a2.f = a.f;
1093				a2.r.q0 = a2.r.q1 = f.buf.nc;
1094			}
1095			if(a1.f != a2.f)
1096				editerror("addresses in different files");
1097			a.f = a1.f;
1098			a.r.q0 = a1.r.q0;
1099			a.r.q1 = a2.r.q1;
1100			if(a.r.q1 < a.r.q0)
1101				editerror("addresses out of order");
1102			return a;
1103
1104		'+' or
1105		'-' =>
1106			sign = 1;
1107			if(ap.typex == '-')
1108				sign = -1;
1109			if(ap.next==nil || ap.next.typex=='+' || ap.next.typex=='-')
1110				a = lineaddr(1, a, sign);
1111			break;
1112		* =>
1113			error("cmdaddress");
1114			return a;
1115		}
1116	}while((ap = ap.next)!=nil);	# assign =
1117	return a;
1118}
1119
1120alltofile(w: ref Window, tp: ref Tofile)
1121{
1122	t: ref Text;
1123
1124	if(tp.f != nil)
1125		return;
1126	if(w.isscratch || w.isdir)
1127		return;
1128	t = w.body;
1129	# only use this window if it's the current window for the file
1130	if(t.file.curtext != t)
1131		return;
1132#	if(w.nopen[QWevent] > 0)
1133#		return;
1134	if(tp.r.r == t.file.name)
1135		tp.f = t.file;
1136}
1137
1138tofile(r: ref String): ref File
1139{
1140	t: ref Tofile;
1141	rr: String;
1142
1143	(rr.r, r.n) = skipbl(r.r, r.n);
1144	t = ref Tofile;
1145	t.f = nil;
1146	t.r = ref String;
1147	*t.r = rr;
1148	aw := ref Allwin.FF(t);
1149	allwindows(Edit->ALLTOFILE, aw);
1150	aw = nil;
1151	if(t.f == nil)
1152		editerror(sprint("no such file\"%s\"", rr.r));
1153	return t.f;
1154}
1155
1156allmatchfile(w: ref Window, tp: ref Tofile)
1157{
1158	t: ref Text;
1159
1160	if(w.isscratch || w.isdir)
1161		return;
1162	t = w.body;
1163	# only use this window if it's the current window for the file
1164	if(t.file.curtext != t)
1165		return;
1166#	if(w.nopen[QWevent] > 0)
1167#		return;
1168	if(filematch(w.body.file, tp.r)){
1169		if(tp.f != nil)
1170			editerror(sprint("too many files match \"%s\"", tp.r.r));
1171		tp.f = w.body.file;
1172	}
1173}
1174
1175matchfile(r: ref String): ref File
1176{
1177	tf: ref Tofile;
1178
1179	tf = ref Tofile;
1180	tf.f = nil;
1181	tf.r = r;
1182	aw := ref Allwin.FF(tf);
1183	allwindows(Edit->ALLMATCHFILE, aw);
1184	aw = nil;
1185
1186	if(tf.f == nil)
1187		editerror(sprint("no file matches \"%s\"", r.r));
1188	return tf.f;
1189}
1190
1191filematch(f: ref File, r: ref String): int
1192{
1193	buf: string;
1194	w: ref Window;
1195	match, dirty: int;
1196	s: Rangeset;
1197
1198	# compile expr first so if we get an error, we haven't allocated anything
1199	if(rxcompile(r.r) == FALSE)
1200		editerror("bad regexp in file match");
1201	w = f.curtext.w;
1202	# same check for dirty as in settag, but we know ncache==0
1203	dirty = !w.isdir && !w.isscratch && f.mod;
1204	buf = sprint("%c%c%c %s\n", " '"[dirty],
1205		'+', " ."[curtext!=nil && curtext.file==f], f.name);
1206	(match, s) = rxexecute(nil, buf, 0, len buf);
1207	buf = nil;
1208	return match;
1209}
1210
1211charaddr(l: int, addr: Address, sign: int): Address
1212{
1213	if(sign == 0)
1214		addr.r.q0 = addr.r.q1 = l;
1215	else if(sign < 0)
1216		addr.r.q1 = addr.r.q0 -= l;
1217	else if(sign > 0)
1218		addr.r.q0 = addr.r.q1 += l;
1219	if(addr.r.q0<0 || addr.r.q1>addr.f.buf.nc)
1220		editerror("address out of range");
1221	return addr;
1222}
1223
1224lineaddr(l: int, addr: Address, sign: int): Address
1225{
1226	n: int;
1227	c: int;
1228	f := addr.f;
1229	a: Address;
1230	p: int;
1231
1232	a.f = f;
1233	if(sign >= 0){
1234		if(l == 0){
1235			if(sign==0 || addr.r.q1==0){
1236				a.r.q0 = a.r.q1 = 0;
1237				return a;
1238			}
1239			a.r.q0 = addr.r.q1;
1240			p = addr.r.q1-1;
1241		}else{
1242			if(sign==0 || addr.r.q1==0){
1243				p = 0;
1244				n = 1;
1245			}else{
1246				p = addr.r.q1-1;
1247				n = f.curtext.readc(p++)=='\n';
1248			}
1249			while(n < l){
1250				if(p >= f.buf.nc)
1251					editerror("address out of range");
1252				if(f.curtext.readc(p++) == '\n')
1253					n++;
1254			}
1255			a.r.q0 = p;
1256		}
1257		while(p < f.buf.nc && f.curtext.readc(p++)!='\n')
1258			;
1259		a.r.q1 = p;
1260	}else{
1261		p = addr.r.q0;
1262		if(l == 0)
1263			a.r.q1 = addr.r.q0;
1264		else{
1265			for(n = 0; n<l; ){	# always runs once
1266				if(p == 0){
1267					if(++n != l)
1268						editerror("address out of range");
1269				}else{
1270					c = f.curtext.readc(p-1);
1271					if(c != '\n' || ++n != l)
1272						p--;
1273				}
1274			}
1275			a.r.q1 = p;
1276			if(p > 0)
1277				p--;
1278		}
1279		while(p > 0 && f.curtext.readc(p-1)!='\n')	# lines start after a newline
1280			p--;
1281		a.r.q0 = p;
1282	}
1283	return a;
1284}
1285
1286allfilecheck(w: ref Window, fp: ref Filecheck)
1287{
1288	f: ref File;
1289
1290	f = w.body.file;
1291	if(w.body.file == fp.f)
1292		return;
1293	if(fp.r == f.name)
1294		warning(nil, sprint("warning: duplicate file name \"%s\"\n", fp.r));
1295}
1296
1297cmdname(f: ref File, str: ref String , set: int): string
1298{
1299	r, s: string;
1300	n: int;
1301	fc: ref Filecheck;
1302	newname: Runestr;
1303
1304	r = nil;
1305	n = str.n;
1306	s = str.r;
1307	if(n == 0){
1308		# no name; use existing
1309		if(f.name == nil)
1310			return nil;
1311		return f.name;
1312	}
1313	(s, n) = skipbl(s, n);
1314	if(n == 0)
1315		;
1316	else{
1317		if(s[0] == '/'){
1318			r = s;
1319		}else{
1320			newname = dirname(f.curtext, s, n);
1321			r = newname.r;
1322			n = newname.nr;
1323		}
1324		fc = ref Filecheck;
1325		fc.f = f;
1326		fc.r = r;
1327		fc.nr = n;
1328		aw := ref Allwin.FC(fc);
1329		allwindows(Edit->ALLFILECHECK, aw);
1330		aw = nil;
1331		if(f.name == nil)
1332			set = TRUE;
1333	}
1334
1335	if(set && r[0: n] != f.name){
1336		f.mark();
1337		f.mod = TRUE;
1338		f.curtext.w.dirty = TRUE;
1339		f.curtext.w.setname(r, n);
1340	}
1341	return r;
1342}
1343
1344copysel(rs: Rangeset): Rangeset
1345{
1346	nrs := array[NRange] of Range;
1347	for(i := 0; i < NRange; i++)
1348		nrs[i] = rs[i];
1349	return nrs;
1350}
1351