implement Editlog; include "common.m"; sys: Sys; utils: Utils; buffm: Bufferm; filem: Filem; textm: Textm; edit: Edit; sprint, fprint: import sys; FALSE, TRUE, BUFSIZE, Empty, Null, Delete, Insert, Replace, Filename, Astring: import Dat; File: import filem; Buffer: import buffm; Text: import textm; error, warning, stralloc, strfree, min: import utils; editerror: import edit; init(mods : ref Dat->Mods) { sys = mods.sys; utils = mods.utils; buffm = mods.bufferm; filem = mods.filem; textm = mods.textm; edit = mods.edit; } Wsequence := "warning: changes out of sequence\n"; warned := FALSE; # # Log of changes made by editing commands. Three reasons for this: # 1) We want addresses in commands to apply to old file, not file-in-change. # 2) It's difficult to track changes correctly as things move, e.g. ,x m$ # 3) This gives an opportunity to optimize by merging adjacent changes. # It's a little bit like the Undo/Redo log in Files, but Point 3) argues for a # separate implementation. To do this well, we use Replace as well as # Insert and Delete # Buflog: adt{ typex: int; # Replace, Filename q0: int; # location of change (unused in f) nd: int; # runes to delete nr: int; # runes in string or file name }; Buflogsize: con 7; SHM : con 16rffff; pack(b: Buflog) : string { a := "0123456"; a[0] = b.typex; a[1] = b.q0&SHM; a[2] = (b.q0>>16)&SHM; a[3] = b.nd&SHM; a[4] = (b.nd>>16)&SHM; a[5] = b.nr&SHM; a[6] = (b.nr>>16)&SHM; return a; } scopy(s1: ref Astring, m: int, s2: string, n: int, o: int) { p := o-n; for(i := 0; i < p; i++) s1.s[m++] = s2[n++]; } # # Minstring shouldn't be very big or we will do lots of I/O for small changes. # Maxstring is BUFSIZE so we can fbufalloc() once and not realloc elog.r. # Minstring: con 16; # distance beneath which we merge changes Maxstring: con BUFSIZE; # maximum length of change we will merge into one eloginit(f: ref File) { if(f.elog.typex != Empty) return; f.elog.typex = Null; if(f.elogbuf == nil) f.elogbuf = buffm->newbuffer(); # f.elogbuf = ref Buffer; if(f.elog.r == nil) f.elog.r = stralloc(BUFSIZE); f.elogbuf.reset(); } elogclose(f: ref File) { if(f.elogbuf != nil){ f.elogbuf.close(); f.elogbuf = nil; } } elogreset(f: ref File) { f.elog.typex = Null; f.elog.nd = 0; f.elog.nr = 0; } elogterm(f: ref File) { elogreset(f); if(f.elogbuf != nil) f.elogbuf.reset(); f.elog.typex = Empty; if(f.elog.r != nil){ strfree(f.elog.r); f.elog.r = nil; } warned = FALSE; } elogflush(f: ref File) { b: Buflog; b.typex = f.elog.typex; b.q0 = f.elog.q0; b.nd = f.elog.nd; b.nr = f.elog.nr; case(f.elog.typex){ * => warning(nil, sprint("unknown elog type 0x%ux\n", f.elog.typex)); break; Null => break; Insert or Replace => if(f.elog.nr > 0) f.elogbuf.insert(f.elogbuf.nc, f.elog.r.s, f.elog.nr); f.elogbuf.insert(f.elogbuf.nc, pack(b), Buflogsize); break; Delete => f.elogbuf.insert(f.elogbuf.nc, pack(b), Buflogsize); break; } elogreset(f); } elogreplace(f: ref File, q0: int, q1: int, r: string, nr: int) { gap: int; if(q0==q1 && nr==0) return; eloginit(f); if(f.elog.typex!=Null && q0 0){ f.buf.read(f.elog.q0+f.elog.nd, f.elog.r, f.elog.nr, gap); f.elog.nr += gap; } f.elog.nd += gap + q1-q0; scopy(f.elog.r, f.elog.nr, r, 0, nr); f.elog.nr += nr; return; } } elogflush(f); f.elog.typex = Replace; f.elog.q0 = q0; f.elog.nd = q1-q0; f.elog.nr = nr; if(nr > BUFSIZE) editerror(sprint("internal error: replacement string too large(%d)", nr)); scopy(f.elog.r, 0, r, 0, nr); } eloginsert(f: ref File, q0: int, r: string, nr: int) { n: int; if(nr == 0) return; eloginit(f); if(f.elog.typex!=Null && q0 0){ elogflush(f); f.elog.typex = Insert; f.elog.q0 = q0; n = nr; if(n > BUFSIZE) n = BUFSIZE; f.elog.nr = n; scopy(f.elog.r, 0, r, 0, n); r = r[n:]; nr -= n; } } elogdelete(f: ref File, q0: int, q1: int) { if(q0 == q1) return; eloginit(f); if(f.elog.typex!=Null && q0q0, t->q1, # but using coordinates relative to the unmodified buffer. As we apply the log, # we have to update the coordinates to be relative to the modified buffer. # Textinsert and textdelete will do this for us; our only work is to apply the # convention that an insertion at t->q0==t->q1 is intended to select the # inserted text. # # We constrain the addresses in here (with textconstrain()) because # overlapping changes will generate bogus addresses. We will warn # about changes out of sequence but proceed anyway; here we must # keep things in range. while(log.nc > 0){ up = log.nc-Buflogsize; log.read(up, a, 0, Buflogsize); b.typex = a.s[0]; b.q0 = a.s[1]|(a.s[2]<<16); b.nd = a.s[3]|(a.s[4]<<16); b.nr = a.s[5]|(a.s[6]<<16); case(b.typex){ * => error(sprint("elogapply: 0x%ux\n", b.typex)); break; Replace => if(!mod){ mod = TRUE; f.mark(); } (tq0, tq1) := t.constrain(b.q0, b.q0+b.nd); t.delete(tq0, tq1, TRUE); up -= b.nr; for(i=0; i BUFSIZE) n = BUFSIZE; log.read(up+i, buf, 0, n); t.insert(tq0+i, buf.s, n, TRUE, 0); } if(t.q0 == b.q0 && t.q1 == b.q0) t.q1 += b.nr; break; Delete => if(!mod){ mod = TRUE; f.mark(); } (tq0, tq1) := t.constrain(b.q0, b.q0+b.nd); t.delete(tq0, tq1, TRUE); break; Insert => if(!mod){ mod = TRUE; f.mark(); } (tq0, nil) := t.constrain(b.q0, b.q0); up -= b.nr; for(i=0; i BUFSIZE) n = BUFSIZE; log.read(up+i, buf, 0, n); t.insert(tq0+i, buf.s, n, TRUE, 0); } if(t.q0 == b.q0 && t.q1 == b.q0) t.q1 += b.nr; break; # Filename => # f.seq = u.seq; # f.unsetname(epsilon); # f.mod = u.mod; # up -= u.n; # if(u.n == 0) # f.name = nil; # else{ # fn0 := stralloc(u.n); # delta.read(up, fn0, 0, u.n); # f.name = fn0.s; # strfree(fn0); # } # break; # } log.delete(up, log.nc); } strfree(buf); strfree(a); elogterm(f); # Bad addresses will cause bufload to crash, so double check. # If changes were out of order, we expect problems so don't complain further. if(t.q0 > f.buf.nc || t.q1 > f.buf.nc || t.q0 > t.q1){ if(!warned) warning(nil, sprint("elogapply: can't happen %d %d %d\n", t.q0, t.q1, f.buf.nc)); t.q1 = min(t.q1, f.buf.nc); t.q0 = min(t.q0, t.q1); } }