ref: 5b8ab1a707f43bb50a0223fd710b7e17cd9a802c
parent: c88121e55c8631e608f62ac41b746065f0df54dc
author: qwx <qwx@sciops.net>
date: Wed Jan 7 05:30:01 EST 2026
vdiff: better heuristic for autostripping patch file paths alternative: don't autostrip at all and use -p
--- /dev/null
+++ b/sys/src/cmd/vdiff.c
@@ -1,0 +1,805 @@
+#include <u.h>
+#include <libc.h>
+#include <plumb.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <bio.h>
+
+enum { Meminc = 32 };+
+typedef struct Block Block;
+typedef struct Line Line;
+typedef struct Col Col;
+typedef struct Patch Patch;
+
+struct Block {+ Rectangle r;
+ Rectangle sr;
+ int v;
+ char *f;
+ Line **lines;
+ int nlines;
+};
+
+struct Line {+ int t;
+ int n;
+ char *s;
+};
+
+struct Col {+ Image *bg;
+ Image *fg;
+};
+
+struct Patch {+ char *name;
+ Block **blocks;
+ int nblocks;
+};
+
+enum
+{+ Lfile = 0,
+ Lsep,
+ Ladd,
+ Ldel,
+ Lnone,
+ Ncols,
+
+ Lterm = -1,
+ Lhash = -2,
+};
+
+enum
+{+ Mcollapse,
+ Mexpand,
+ Nmenu,
+};
+
+enum
+{+ Scrollwidth = 12,
+ Scrollgap = 2,
+ Margin = 8,
+ Hpadding = 4,
+ Vpadding = 2,
+};
+
+Mousectl *mctl;
+Keyboardctl *kctl;
+Rectangle sr;
+Rectangle scrollr;
+Rectangle scrposr;
+Rectangle viewr;
+Col cols[Ncols];
+Col scrlcol;
+Image *trlcol;
+Image *bord;
+Image *expander[2];
+Image *fb;
+int totalh;
+int viewh;
+int scrollsize;
+int offset;
+int lineh;
+int scrolling;
+int oldbuttons;
+Patch *patches;
+int npatches;
+Patch *cur;
+int maxlength;
+int Δpan;
+int nstrip;
+const char ellipsis[] = "...";
+int ellipsisw;
+int spacew;
+
+void*
+emalloc(ulong n)
+{+ void *p;
+
+ p = malloc(n);
+ if(p == nil)
+ sysfatal("malloc: %r");+ return p;
+}
+
+void*
+erealloc(void *p, ulong n)
+{+ void *q;
+
+ q = realloc(p, n);
+ if(q == nil)
+ sysfatal("realloc: %r");+ return q;
+}
+
+Image*
+eallocimage(Rectangle r, int repl, ulong n)
+{+ Image *i;
+
+ i = allocimage(display, r, screen->chan, repl, n);
+ if(i == nil)
+ sysfatal("allocimage: %r");+ return i;
+}
+
+void
+plumb(char *f, int l)
+{+ int fd, i;
+ char *p, wd[256], addr[300]={0};+
+ fd = plumbopen("send", OWRITE);+ if(fd<0)
+ return;
+ for(i = 0; i < nstrip; i++)
+ if((p = strchr(f, '/')) != nil)
+ f = p+1;
+ getwd(wd, sizeof wd);
+ snprint(addr, sizeof addr, "%s:%d", f, l);
+ plumbsendtext(fd, "vdiff", "edit", wd, addr);
+ close(fd);
+}
+
+void
+renderline(Image *b, Rectangle r, int pad, int lt, char *ls)
+{+ Point p0, p;
+ Rectangle trlr;
+ Rune rn;
+ char *s;
+ int off, tab, nc, hastrl;
+
+ draw(b, r, cols[lt].bg, nil, ZP);
+ p = Pt(r.min.x + pad + Hpadding, r.min.y + (Dy(r)-font->height)/2);
+ off = Δpan / spacew;
+ hastrl = 0;
+ for(s = ls, nc = -1, tab = 0; *s || tab > 0; nc++, tab--, off--){+ if(tab <= 0 && *s && *s == '\t'){+ tab = 4 - nc % 4;
+ s++;
+ }
+ if(tab > 0){+ p0 = p;
+ if(off <= 0)
+ p = runestring(b, p, cols[lt].bg, ZP, font, L"█");
+ if(hastrl)
+ trlr.max = addpt(p, Pt(0, font->height));
+ else{+ trlr.min = p0;
+ hastrl = 1;
+ }
+ }else if((p.x+Hpadding+spacew+ellipsisw>=b->r.max.x)){+ string(b, p, cols[lt].fg, ZP, font, ellipsis);
+ break;
+ }else{+ s += chartorune(&rn, s);
+ p0 = p;
+ if(off <= 0)
+ p = runestringn(b, p, cols[lt].fg, ZP, font, &rn, 1);
+ if(isspacerune(rn)){+ if(hastrl)
+ trlr.max = addpt(p, Pt(0, font->height));
+ else{+ trlr.min = p0;
+ hastrl = 1;
+ }
+ }else if(hastrl)
+ hastrl = 0;
+ }
+ }
+
+ if(hastrl)
+ draw(b, trlr, trlcol, nil, ZP);
+}
+
+void
+renderblock(Block *b, Rectangle sr)
+{+ Rectangle r, lr, br;
+ Line *l;
+ int i, pad;
+
+ pad = 0;
+ r = insetrect(sr, 1);
+ if(b->f != nil){+ pad = Margin;
+ lr = r;
+ lr.max.y = lr.min.y + lineh;
+ br = rectaddpt(expander[0]->r, Pt(lr.min.x+Hpadding, lr.min.y+Vpadding));
+ border(fb, sr, 1, bord, ZP);
+ renderline(fb, lr, Dx(expander[0]->r)+Hpadding, Lfile, b->f);
+ draw(fb, br, expander[b->v], nil, ZP);
+ r.min.y += lineh;
+ }
+ if(b->v == 0)
+ return;
+ for(i = 0; i < b->nlines; i++){+ l = b->lines[i];
+ lr = Rect(r.min.x, r.min.y+i*lineh, r.max.x, r.min.y+(i+1)*lineh);
+ renderline(fb, lr, pad, l->t, l->s);
+ }
+}
+
+void
+redraw(void)
+{+ Rectangle clipr;
+ int i, h, y, ye, vmin, vmax;
+ Block *b;
+
+ draw(fb, fb->r, cols[Lnone].bg, nil, ZP);
+ draw(fb, scrollr, scrlcol.bg, nil, ZP);
+ if(viewh < totalh){+ h = ((double)viewh/totalh)*Dy(scrollr);
+ y = ((double)offset/totalh)*Dy(scrollr);
+ ye = scrollr.min.y + y + h;
+ if(ye >= scrollr.max.y)
+ ye = scrollr.max.y;
+ scrposr = Rect(scrollr.min.x, scrollr.min.y+y+1, scrollr.max.x-1, ye);
+ }else
+ scrposr = Rect(scrollr.min.x, scrollr.min.y, scrollr.max.x-1, scrollr.max.y);
+ draw(fb, scrposr, scrlcol.fg, nil, ZP);
+ vmin = viewr.min.y + offset;
+ vmax = viewr.max.y + offset;
+ clipr = screen->clipr;
+ replclipr(fb, 0, viewr);
+ for(i = 0; i < cur->nblocks; i++){+ b = cur->blocks[i];
+ if(b->sr.min.y <= vmax && b->sr.max.y >= vmin)
+ renderblock(b, rectaddpt(b->sr, Pt(0, -offset)));
+ }
+ replclipr(fb, 0, clipr);
+ draw(screen, screen->r, fb, nil, fb->r.min);
+ flushimage(display, 1);
+}
+
+void
+pan(int off)
+{+ int max;
+
+ max = Dx(scrollr) + Margin + Hpadding + maxlength*spacew + 2*ellipsisw + Hpadding + Margin - Dx(cur->blocks[0]->r)/2;
+ Δpan += off * spacew;
+ if(Δpan < 0 || max <= 0)
+ Δpan = 0;
+ else if(Δpan > max)
+ Δpan = max;
+ redraw();
+}
+
+void
+clampoffset(int off)
+{+ if(offset<0)
+ offset = 0;
+ if(offset+viewh>totalh){+ if(off > 0)
+ offset = totalh - viewh;
+ else
+ offset = 0;
+ }
+}
+
+void
+scroll(int off)
+{+ if(off<0 && offset<=0)
+ return;
+ if(off>0 && offset+viewh>totalh)
+ return;
+ offset += off;
+ clampoffset(off);
+ redraw();
+}
+
+void
+blockresize(Block *b)
+{+ int w, h;
+
+ w = Dx(viewr) - 2; /* add 2 for border */
+ h = 0 + 2;
+ if(b->f != nil)
+ h += lineh;
+ if(b->v)
+ h += b->nlines*lineh;
+ b->r = Rect(0, 0, w, h);
+}
+
+void
+eresize(int new)
+{+ Rectangle listr;
+ Block *b;
+ Point p;
+ int i;
+
+ if(new && getwindow(display, Refnone)<0)
+ sysfatal("cannot reattach: %r");+ sr = screen->r;
+ scrollr = sr;
+ scrollr.max.x = scrollr.min.x+Scrollwidth+Scrollgap;
+ listr = sr;
+ listr.min.x = scrollr.max.x;
+ viewr = insetrect(listr, Margin);
+ viewh = Dy(viewr);
+ lineh = Vpadding+font->height+Vpadding;
+ totalh = - Margin + Vpadding + 1;
+ p = addpt(viewr.min, Pt(0, totalh));
+ for(i = 0; i < cur->nblocks; i++){+ b = cur->blocks[i];
+ blockresize(b);
+ b->sr = rectaddpt(b->r, p);
+ p.y += Margin + Dy(b->r);
+ totalh += Margin + Dy(b->r);
+ }
+ totalh = totalh - Margin + Vpadding;
+ scrollsize = viewh / 2.0;
+ if(viewh <= totalh)
+ clampoffset(1);
+ else
+ clampoffset(0);
+ freeimage(fb);
+ fb = eallocimage(screen->r, 0, DBlack);
+ redraw();
+}
+
+void
+ekeyboard(Rune k)
+{+ switch(k){+ case 'q':
+ case Kdel:
+ threadexitsall(nil);
+ break;
+ case Khome:
+ scroll(-totalh);
+ break;
+ case Kend:
+ scroll(totalh);
+ break;
+ case Kpgup:
+ scroll(-viewh);
+ break;
+ case Kpgdown:
+ scroll(viewh);
+ break;
+ case Kup:
+ scroll(-scrollsize);
+ break;
+ case Kdown:
+ scroll(scrollsize);
+ break;
+ case Kleft:
+ pan(-4);
+ break;
+ case Kright:
+ pan(4);
+ break;
+ }
+}
+
+char*
+genmenu(int i)
+{+ switch(i){+ case Mcollapse:
+ return "collapse";
+ case Mexpand:
+ return "expand";
+ default:
+ i -= Nmenu;
+ if(i >= npatches || npatches == 1)
+ return nil;
+ if(patches[i].name == nil)
+ if((patches[i].name = smprint("%d", i)) == nil)+ sysfatal("smprint: %r");+ return patches[i].name;
+ }
+}
+
+void
+blockmouse(Block *b, Mouse m)
+{+ Line *l;
+ int n;
+
+ n = (m.xy.y + offset - b->sr.min.y) / lineh;
+ if(n == 0 && b->f != nil && m.buttons&1){+ b->v = !b->v;
+ eresize(0);
+ }else if(n > 0 && m.buttons&4){+ l = b->lines[n-1];
+ if(l->t != Lsep)
+ plumb(b->f, l->n);
+ }
+}
+
+Menu menu = {+ nil,
+ genmenu,
+};
+
+void
+collapse(int v)
+{+ int i;
+
+ for(i = 0; i < cur->nblocks; i++)
+ if(cur->blocks[i]->f != nil)
+ cur->blocks[i]->v = v;
+ eresize(0);
+}
+
+void
+emouse(Mouse m)
+{+ Block *b;
+ int n, i;
+
+ if(oldbuttons == 0 && m.buttons != 0 && ptinrect(m.xy, scrollr))
+ scrolling = 1;
+ else if(m.buttons == 0)
+ scrolling = 0;
+
+ n = (m.xy.y - viewr.min.y - Margin)/lineh * lineh;
+ if(scrolling){+ if(m.buttons&1){+ scroll(-n);
+ return;
+ }else if(m.buttons&2){+ offset = (m.xy.y - scrollr.min.y) * totalh/Dy(scrollr);
+ offset = offset/lineh * lineh;
+ if(viewh <= totalh)
+ clampoffset(1);
+ else
+ clampoffset(0);
+ redraw();
+ }else if(m.buttons&4){+ scroll(n);
+ return;
+ }
+ }else if(!scrolling && m.buttons&2){+ n = menuhit(2, mctl, &menu, nil);
+ switch(n){+ case -1:
+ break;
+ case Mexpand: case Mcollapse:
+ collapse(n);
+ break;
+ default:
+ n -= Nmenu;
+ if(cur == patches+n)
+ break;
+ cur = patches+n;
+ eresize(0);
+ }
+ }else if(m.buttons&8){+ scroll(-n);
+ }else if(m.buttons&16){+ scroll(n);
+ }else if((oldbuttons^m.buttons) != 0 && ptinrect(m.xy, viewr)){+ for(i = 0; i < cur->nblocks; i++){+ b = cur->blocks[i];
+ if(ptinrect(addpt(m.xy, Pt(0, offset)), b->sr)){+ blockmouse(b, m);
+ break;
+ }
+ }
+ }
+ oldbuttons = m.buttons;
+}
+
+void
+initcol(Col *c, ulong fg, ulong bg)
+{+ Rectangle r;
+
+ r = Rect(0, 0, 1, 1);
+ c->fg = eallocimage(r, 1, fg);
+ c->bg = eallocimage(r, 1, bg);
+}
+
+void
+initcols(int black)
+{+ Rectangle r;
+
+ r = Rect(0, 0, 1, 1);
+ if(black){+ bord = eallocimage(r, 1, 0x888888FF^(~0xFF));
+ initcol(&scrlcol, DBlack, 0x999999FF^(~0xFF));
+ initcol(&cols[Lfile], DWhite, 0x333333FF);
+ initcol(&cols[Lsep], DBlack, DPurpleblue);
+ initcol(&cols[Ladd], DWhite, 0x002800FF);
+ initcol(&cols[Ldel], DWhite, 0x3F0000FF);
+ initcol(&cols[Lnone], DWhite, DBlack);
+ trlcol = eallocimage(r, 1, 0x9F0000FF);
+ }else{+ bord = eallocimage(r, 1, 0x888888FF);
+ initcol(&scrlcol, DWhite, 0x999999FF);
+ initcol(&cols[Lfile], DBlack, 0xEFEFEFFF);
+ initcol(&cols[Lsep], DBlack, 0xEAFFFFFF);
+ initcol(&cols[Ladd], DBlack, 0xE6FFEDFF);
+ initcol(&cols[Ldel], DBlack, 0xFFEEF0FF);
+ initcol(&cols[Lnone], DBlack, DWhite);
+ trlcol = eallocimage(r, 1, 0xFF8890FF);
+ }
+}
+
+void
+initicons(void)
+{+ int w, h;
+ Point p[4];
+
+ w = font->height;
+ h = font->height;
+ expander[0] = eallocimage(Rect(0, 0, w, h), 0, DNofill);
+ draw(expander[0], expander[0]->r, cols[Lfile].bg, nil, ZP);
+ p[0] = Pt(0.25*w, 0.25*h);
+ p[1] = Pt(0.25*w, 0.75*h);
+ p[2] = Pt(0.75*w, 0.5*h);
+ p[3] = p[0];
+ fillpoly(expander[0], p, 4, 0, bord, ZP);
+ expander[1] = eallocimage(Rect(0, 0, w, h), 0, DNofill);
+ draw(expander[1], expander[1]->r, cols[Lfile].bg, nil, ZP);
+ p[0] = Pt(0.25*w, 0.25*h);
+ p[1] = Pt(0.75*w, 0.25*h);
+ p[2] = Pt(0.5*w, 0.75*h);
+ p[3] = p[0];
+ fillpoly(expander[1], p, 4, 0, bord, ZP);
+ flushimage(display, 0);
+}
+
+Block*
+addblock(void)
+{+ Block *b;
+
+ b = emalloc(sizeof *b);
+ b->v = 1;
+ b->f = nil;
+ b->lines = nil;
+ b->nlines = 0;
+ if(cur->nblocks%Meminc == 0)
+ cur->blocks = erealloc(cur->blocks, (cur->nblocks+Meminc)*sizeof *cur->blocks);
+ cur->blocks[cur->nblocks++] = b;
+ return b;
+}
+
+void
+addline(Block *b, int t, int n, char *s)
+{+ Line *l;
+
+ l = emalloc(sizeof *l);
+ l->t = t;
+ l->n = n;
+ l->s = s;
+ if(b->nlines%Meminc == 0)
+ b->lines = erealloc(b->lines, (b->nlines+Meminc)*sizeof(Line*));
+ b->lines[b->nlines++] = l;
+}
+
+int
+linetype(char *text)
+{+ int type;
+
+ type = Lnone;
+ if(utfncmp(text, "⑨", 1) == 0)
+ type = Lterm;
+ else if(strncmp(text, "diff", 4)==0)
+ type = Lhash;
+ else if(strncmp(text, "+++", 3)==0)
+ type = Lfile;
+ else if(strncmp(text, "---", 3)==0){+ if(strlen(text) > 4)
+ type = Lfile;
+ }else if(strncmp(text, "@@", 2)==0)
+ type = Lsep;
+ else if(strncmp(text, "+", 1)==0)
+ type = Ladd;
+ else if(strncmp(text, "-", 1)==0)
+ type = Ldel;
+ return type;
+}
+
+int
+lineno(char *s)
+{+ char *p, *t[5];
+ int n, l;
+
+ p = strdup(s);
+ if(p == nil)
+ sysfatal("strdup: %r");+ n = tokenize(p, t, 5);
+ if(n<=0)
+ return -1;
+ l = atoi(t[2]);
+ free(p);
+ return l;
+}
+
+void
+parse(int fd, char *name)
+{+ Biobuf *bp;
+ Block *b;
+ char *s, *f, *tab;
+ int t, n, ab, len;
+ int gotterm;
+
+ bp = Bfdopen(fd, OREAD);
+ if(bp==nil)
+ sysfatal("Bfdopen: %r");+ gotterm = 0;
+ goto New;
+ for(;;){+ s = Brdstr(bp, '\n', 1);
+ if(s==nil)
+ break;
+ t = linetype(s);
+ switch(t){+ case Lterm:
+ gotterm = 1;
+ /* remove '--' and extra newline */
+ b->nlines--;
+ free(s);
+ free(Brdstr(bp, '\n', 1));
+ New:
+ npatches++;
+ patches = erealloc(patches, sizeof *patches * npatches);
+ cur = patches+npatches-1;
+ cur->blocks = nil;
+ cur->nblocks = 0;
+ cur->name = name;
+ b = addblock();
+ n = 0;
+ ab = 0;
+ break;
+ case Lfile:
+ if(s[0] == '-'){+ b = addblock();
+ b->f = s+4;
+ if(strncmp(b->f, "a/", 2) == 0){+ ab = 1;
+ b->f++;
+ }
+ }else if(s[0] == '+'){+ f = s+4;
+ if(ab && strncmp(f, "b/", 2) == 0){+ f++;
+ if(access(f+1, AEXIST) == 0)
+ f++;
+ }
+ tab = strchr(f, '\t');
+ if(tab != nil)
+ *tab = 0;
+ if(strcmp(f, "/dev/null") != 0)
+ b->f = f;
+ else
+ free(s);
+ }else
+ free(s);
+ break;
+ case Lsep:
+ n = lineno(s) - 1; /* -1 as the separator is not an actual line */
+ if(0){+ case Lhash:
+ f = strchr(s, ' ');
+ if(f != nil && (f = strchr(f+1, ' '))){+ f++;
+ if(strcmp(f, "uncommitted") != 0){+ if(name != nil)
+ cur->name = smprint("%s %.*s", name, 9, f);+ else
+ cur->name = smprint("%.*s", 9, f);+ if(cur->name == nil)
+ sysfatal("smprint: %r");+ }
+ }
+ t = Lnone;
+ case Ladd:
+ case Lnone:
+ ++n;
+ }
+ default:
+ addline(b, t, n, s);
+ len = strlen(s);
+ if(len > maxlength)
+ maxlength = len;
+ break;
+ }
+ }
+ if(gotterm)
+ npatches--;
+ Bterm(bp);
+}
+
+void
+usage(void)
+{+ fprint(2, "usage: %s [-b] [-p nstrip] [patches...]\n", argv0);
+ exits("usage");+}
+
+void
+threadmain(int argc, char *argv[])
+{+ enum { Emouse, Eresize, Ekeyboard, };+ Mouse m;
+ Rune k;
+ Alt a[] = {+ { nil, &m, CHANRCV },+ { nil, nil, CHANRCV },+ { nil, &k, CHANRCV },+ { nil, nil, CHANEND },+ };
+ int i, b, fd;
+
+ b = 0;
+ ARGBEGIN{+ case 'b':
+ b = 1;
+ break;
+ case 'p':
+ nstrip = atoi(EARGF(usage()));
+ break;
+ default:
+ usage();
+ break;
+ }ARGEND;
+ if(argc == 0)
+ parse(0, nil);
+ else for(i = 0; i < argc; i++){+ fd = open(argv[i], OREAD);
+ if(fd < 0)
+ sysfatal("open: %r");+ parse(fd, argv[i]);
+ close(fd);
+ }
+ cur = patches;
+ if(cur->nblocks==1 && cur->blocks[0]->nlines==0){+ fprint(2, "no diff\n");
+ exits(nil);
+ }
+ if(initdraw(nil, nil, "vdiff")<0)
+ sysfatal("initdraw: %r");+ if((mctl = initmouse(nil, screen)) == nil)
+ sysfatal("initmouse: %r");+ if((kctl = initkeyboard(nil)) == nil)
+ sysfatal("initkeyboard: %r");+ a[Emouse].c = mctl->c;
+ a[Eresize].c = mctl->resizec;
+ a[Ekeyboard].c = kctl->c;
+ initcols(b);
+ initicons();
+ spacew = stringwidth(font, " ");
+ ellipsisw = stringwidth(font, ellipsis);
+ eresize(0);
+ for(;;){+ switch(alt(a)){+ case Emouse:
+ emouse(m);
+ break;
+ case Eresize:
+ eresize(1);
+ break;
+ case Ekeyboard:
+ ekeyboard(k);
+ break;
+ }
+ }
+}
--
⑨