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<f.elog.q0){
if(warned++ == 0)
warning(nil, Wsequence);
elogflush(f);
}
# try to merge with previous
gap = q0 - (f.elog.q0+f.elog.nd); # gap between previous and this
if(f.elog.typex==Replace && f.elog.nr+gap+nr<Maxstring){
if(gap < Minstring){
if(gap > 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<f.elog.q0){
if(warned++ == 0)
warning(nil, Wsequence);
elogflush(f);
}
# try to merge with previous
if(f.elog.typex==Insert && q0==f.elog.q0 && f.elog.nr+nr<Maxstring){
ofer := f.elog.r;
f.elog.r = stralloc(f.elog.nr+nr);
scopy(f.elog.r, 0, ofer.s, 0, f.elog.nr);
scopy(f.elog.r, f.elog.nr, r, 0, nr);
f.elog.nr += nr;
strfree(ofer);
return;
}
while(nr > 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 && q0<f.elog.q0+f.elog.nd){
if(warned++ == 0)
warning(nil, Wsequence);
elogflush(f);
}
# try to merge with previous
if(f.elog.typex==Delete && f.elog.q0+f.elog.nd==q0){
f.elog.nd += q1-q0;
return;
}
elogflush(f);
f.elog.typex = Delete;
f.elog.q0 = q0;
f.elog.nd = q1-q0;
}
elogapply(f: ref File)
{
b: Buflog;
buf: ref Astring;
i, n, up, mod : int;
log: ref Buffer;
elogflush(f);
log = f.elogbuf;
t := f.curtext;
a := stralloc(Buflogsize);
buf = stralloc(BUFSIZE);
mod = FALSE;
# The edit commands have already updated the selection in t->q0, 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<b.nr; i+=n){
n = b.nr - i;
if(n > 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<b.nr; i+=n){
n = b.nr - i;
if(n > 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);
}
}
|