code: plan9front

Download patch

ref: 9ced712a2ad70557a8ceb89ff3b969b92c85f68c
parent: ed4c1344c90e6c39614241e9e181c8c73d2a6e57
author: phil9 <telephil9@gmail.com>
date: Mon Feb 5 05:53:32 EST 2024

vdiff: add ability to collapse file diffs

	vdiff now displays file diffs in separate blocks that can be collapsed
	to make reading large diffs easier.
	In addition scrolling now uses "galaxy scrolling" mode.

--- a/sys/man/1/vdiff
+++ b/sys/man/1/vdiff
@@ -12,7 +12,7 @@
 ]
 .SH DESCRIPTION
 .I vdiff
-reads unified diff output from standard input and displays a colored version. Right clicking on a line will send a 
+reads unified diff output from standard input and displays a colored version. Each file diff is displayed in a separate block that can be collapsed by a click on the file name. Right clicking on a line will send a 
 .IR plumb (6)
 message to the
 .B edit
@@ -21,7 +21,7 @@
 or
 .B del
 will exit
-.B vdiff
+.B vdiff.
 .PP
 The
 .B \-b
--- a/sys/src/cmd/vdiff.c
+++ b/sys/src/cmd/vdiff.c
@@ -7,14 +7,26 @@
 #include <keyboard.h>
 #include <bio.h>
 
+enum { Meminc = 32 };
+
+typedef struct Block Block;
 typedef struct Line Line;
 typedef struct Col Col;
 
+struct Block {
+	Image *b;
+	Rectangle r;
+	Rectangle sr;
+	int v;
+	char *f;
+	Line **lines;
+	int nlines;
+};
+
 struct Line {
 	int t;
+	int n;
 	char *s;
-	char *f;
-	int l;
 };
 
 struct Col {
@@ -46,24 +58,49 @@
 Rectangle sr;
 Rectangle scrollr;
 Rectangle scrposr;
-Rectangle listr;
-Rectangle textr;
+Rectangle viewr;
 Col cols[Ncols];
 Col scrlcol;
+Image *bord;
+Image *expander[2];
+int totalh;
+int viewh;
 int scrollsize;
-int lineh;
-int nlines;
 int offset;
+int lineh;
 int scrolling;
 int oldbuttons;
-Line **lines;
-int lsize;
-int lcount;
+Block **blocks;
+int nblocks;
 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;
+}
+
 void
 plumb(char *f, int l)
 {
@@ -83,7 +120,7 @@
 }
 
 void
-drawline(Rectangle r, Line *l)
+renderline(Image *b, Rectangle r, int pad, int lt, char *ls)
 {
 	Point p;
 	Rune  rn;
@@ -90,10 +127,10 @@
 	char *s;
 	int off, tab, nc;
 
-	draw(screen, r, cols[l->t].bg, nil, ZP);
-	p = Pt(r.min.x + Hpadding, r.min.y + (Dy(r)-font->height)/2);
-	off = Δpan / stringwidth(font, " ");
-	for(s = l->s, nc = -1, tab = 0; *s; nc++, tab--, off--){
+	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;
+	for(s = ls, nc = -1, tab = 0; *s; nc++, tab--, off--){
 		if(tab <= 0 && *s == '\t'){
 			tab = 4 - nc % 4;
 			s++;
@@ -100,37 +137,78 @@
 		}
 		if(tab > 0){
 			if(off <= 0)
-				p = runestring(screen, p, cols[l->t].bg, ZP, font, L"█");
-		}else if((p.x+Hpadding+stringwidth(font, " ")+stringwidth(font, ellipsis)>=textr.max.x)){
-			string(screen, p, cols[l->t].fg, ZP, font, ellipsis);
+				p = runestring(b, p, cols[lt].bg, ZP, font, L"█");
+		}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);
 			if(off <= 0)
-				p = runestringn(screen, p, cols[l->t].fg, ZP, font, &rn, 1);
+				p = runestringn(b, p, cols[lt].fg, ZP, font, &rn, 1);
 		}
 	}
 }
 
 void
+renderblock(Block *b)
+{
+	Rectangle r, lr, br;
+	Line *l;
+	int i, pad;
+
+	pad = 0;
+	r = insetrect(b->r, 1);
+	draw(b->b, b->r, cols[Lnone].bg, nil, ZP);
+	if(b->f != nil){
+		pad = Margin;
+		lr = r;
+		lr.max.y = lineh;
+		br = rectaddpt(expander[0]->r, Pt(lr.min.x+Hpadding, lr.min.y+Vpadding));
+		border(b->b, b->r, 1, bord, ZP);
+		renderline(b->b, lr, Dx(expander[0]->r)+Hpadding, Lfile, b->f);
+		draw(b->b, 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(b->b, lr, pad, l->t, l->s);
+	}
+}
+
+void
 redraw(void)
 {
-	Rectangle lr;
-	int i, h, y;
+	Rectangle clipr;
+	int i, h, y, ye, vmin, vmax;
+	Block *b;
 
 	draw(screen, sr, cols[Lnone].bg, nil, ZP);
 	draw(screen, scrollr, scrlcol.bg, nil, ZP);
-	if(lcount>0){
-		h = ((double)nlines/lcount)*Dy(scrollr);
-		y = ((double)offset/lcount)*Dy(scrollr);
-		scrposr = Rect(scrollr.min.x, scrollr.min.y+y, scrollr.max.x-1, scrollr.min.y+y+h);
+	if(viewh < totalh){
+		h = ((double)viewh/totalh)*Dy(scrollr);
+		y = ((double)offset/totalh)*Dy(scrollr);
+		ye = scrollr.min.y + y + h - 1;
+		if(ye >= scrollr.max.y)
+			ye = scrollr.max.y - 1;
+		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(screen, scrposr, scrlcol.fg, nil, ZP);
-	for(i=0; i<nlines && offset+i<lcount; i++){
-		lr = Rect(textr.min.x, textr.min.y+i*lineh, textr.max.x, textr.min.y+(i+1)*lineh);
-		drawline(lr, lines[offset+i]);
+	vmin = viewr.min.y + offset;
+	vmax = viewr.max.y + offset;
+	clipr = screen->clipr;
+	replclipr(screen, 0, viewr);
+	for(i = 0; i < nblocks; i++){
+		b = blocks[i];
+		if(b->sr.min.y <= vmax && b->sr.max.y >= vmin){
+			renderblock(b);
+			draw(screen, rectaddpt(b->sr, Pt(0, -offset)), b->b, nil, ZP);
+		}
 	}
+	replclipr(screen, 0, clipr);
 	flushimage(display, 1);
 }
 
@@ -139,8 +217,8 @@
 {
 	int max;
 
-	max = Hpadding + maxlength * stringwidth(font, " ") + 2 * stringwidth(font, ellipsis) - Dx(textr);
-	Δpan += off * stringwidth(font, " ");
+	max = Hpadding + Margin + Hpadding + maxlength * spacew + 2 * ellipsisw - Dx(blocks[0]->r);
+	Δpan += off * spacew;
 	if(Δpan < 0 || max <= 0)
 		Δpan = 0;
 	else if(Δpan > max)
@@ -153,8 +231,8 @@
 {
 	if(offset<0)
 		offset = 0;
-	if(offset+nlines>lcount)
-		offset = lcount-nlines+1;
+	if(offset+viewh>totalh)
+		offset = totalh - viewh;
 }
 
 void
@@ -162,7 +240,7 @@
 {
 	if(off<0 && offset<=0)
 		return;
-	if(off>0 && offset+nlines>lcount)
+	if(off>0 && offset+viewh>totalh)
 		return;
 	offset += off;
 	clampoffset();
@@ -169,23 +247,31 @@
 	redraw();
 }
 
-int
-indexat(Point p)
+void
+blockresize(Block *b)
 {
-	int n;
+	int w, h;
 
-	if (!ptinrect(p, textr))
-		return -1;
-	n = (p.y - textr.min.y) / lineh;
-	if ((n+offset) >= lcount)
-		return -1;
-	return n;
+	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);
+	freeimage(b->b);
+	b->b = allocimage(display, b->r, screen->chan, 0, DNofill);
 }
 
 void
-eresize(void)
+eresize(int new)
 {
-	if(getwindow(display, Refnone)<0)
+	Rectangle listr;
+	Block *b;
+	Point p;
+	int i;
+
+	if(new && getwindow(display, Refnone)<0)
 		sysfatal("cannot reattach: %r");
 	sr = screen->r;
 	scrollr = sr;
@@ -192,12 +278,22 @@
 	scrollr.max.x = scrollr.min.x+Scrollwidth+Scrollgap;
 	listr = sr;
 	listr.min.x = scrollr.max.x;
-	textr = insetrect(listr, Margin);
+	viewr = insetrect(listr, Margin);
+	viewh = Dy(viewr);
 	lineh = Vpadding+font->height+Vpadding;
-	nlines = Dy(textr)/lineh;
-	scrollsize = mousescrollsize(nlines);
-	if(offset > 0 && offset+nlines>lcount)
-		offset = lcount-nlines+1;
+	totalh = - Margin + Vpadding + 1;
+	p = addpt(viewr.min, Pt(0, totalh));
+	for(i = 0; i < nblocks; i++){
+		b = 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(offset > 0 && offset+viewh>totalh)
+		offset = totalh - viewh;
 	redraw();
 }
 
@@ -210,22 +306,22 @@
 		threadexitsall(nil);
 		break;
 	case Khome:
-		scroll(-1000000);
+		scroll(-totalh);
 		break;
 	case Kend:
-		scroll(1000000);
+		scroll(totalh);
 		break;
 	case Kpgup:
-		scroll(-nlines);
+		scroll(-viewh);
 		break;
 	case Kpgdown:
-		scroll(nlines);
+		scroll(viewh);
 		break;
 	case Kup:
-		scroll(-1);
+		scroll(-scrollsize);
 		break;
 	case Kdown:
-		scroll(1);
+		scroll(scrollsize);
 		break;
 	case Kleft:
 		pan(-4);
@@ -237,56 +333,78 @@
 }
 
 void
-emouse(Mouse m)
+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);
+	}
+}
+
+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 - scrollr.min.y);
 	if(scrolling){
 		if(m.buttons&1){
-			n = (m.xy.y - scrollr.min.y) / lineh;
-			if(-n<lcount-offset){
-				scroll(-n);
-			} else {
-				scroll(-lcount+offset);
-			}
+			scroll(-n);
 			return;
 		}else if(m.buttons&2){
-			n = (m.xy.y - scrollr.min.y) * lcount / Dy(scrollr);
-			offset = n;
+			offset = (m.xy.y - scrollr.min.y) * totalh/Dy(scrollr);
 			clampoffset();
 			redraw();
 		}else if(m.buttons&4){
-			n = (m.xy.y - scrollr.min.y) / lineh;
-			if(n<lcount-offset){
-				scroll(n);
-			} else {
-				scroll(lcount-offset);
-			}
+			scroll(n);
 			return;
 		}
-	}else{
-		if(m.buttons&4){
-			n = indexat(m.xy);
-			if(n>=0 && lines[n+offset]->f != nil)
-				plumb(lines[n+offset]->f, lines[n+offset]->l);
-		}else if(m.buttons&8)
-			scroll(-scrollsize);
-		else if(m.buttons&16)
-			scroll(scrollsize);
+	}else if(m.buttons&8){
+		scroll(-n);
+	}else if(m.buttons&16){
+		scroll(n);
+	}else if(m.buttons != 0 && ptinrect(m.xy, viewr)){
+		for(i = 0; i < nblocks; i++){
+			b = blocks[i];
+			if(ptinrect(addpt(m.xy, Pt(0, offset)), b->sr)){
+				blockmouse(b, m);
+				break;
+			}
+		}
 	}
 	oldbuttons = m.buttons;
 }
 
+Image*
+ecolor(ulong n)
+{
+	Image *i;
+
+	i = allocimage(display, Rect(0,0,1,1), screen->chan, 1, n);
+	if(i == nil)
+		sysfatal("allocimage: %r");
+	return i;
+}
+
 void
 initcol(Col *c, ulong fg, ulong bg)
 {
-	c->fg = allocimage(display, Rect(0,0,1,1), screen->chan, 1, fg);
-	c->bg = allocimage(display, Rect(0,0,1,1), screen->chan, 1, bg);
+	c->fg = ecolor(fg);
+	c->bg = ecolor(bg);
 }
 
 void
@@ -293,6 +411,7 @@
 initcols(int black)
 {
 	if(black){
+		bord = ecolor(0x888888FF^(~0xFF));
 		initcol(&scrlcol,     DBlack, 0x999999FF^(~0xFF));
 		initcol(&cols[Lfile], DWhite, 0x333333FF);
 		initcol(&cols[Lsep],  DBlack, DPurpleblue);
@@ -300,6 +419,7 @@
 		initcol(&cols[Ldel],  DWhite, 0x3F0000FF);
 		initcol(&cols[Lnone], DWhite, DBlack);
 	}else{
+		bord = ecolor(0x888888FF);
 		initcol(&scrlcol,     DWhite, 0x999999FF);
 		initcol(&cols[Lfile], DBlack, 0xEFEFEFFF);
 		initcol(&cols[Lsep],  DBlack, 0xEAFFFFFF);
@@ -309,6 +429,62 @@
 	}
 }
 
+void
+initicons(void)
+{
+	int w, h;
+	Point p[4];
+
+	w = font->height;
+	h = font->height;
+	expander[0] = allocimage(display, Rect(0, 0, w, h), screen->chan, 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] = allocimage(display, Rect(0, 0, w, h), screen->chan, 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->b = nil;
+	b->v = 1;
+	b->f = nil;
+	b->lines = nil;
+	b->nlines = 0;
+	if(nblocks%Meminc == 0)
+		blocks = erealloc(blocks, (nblocks+Meminc)*sizeof *blocks);
+	blocks[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)
 {
@@ -329,28 +505,6 @@
 	return type;
 }
 
-Line*
-parseline(char *f, int n, char *s)
-{
-	Line *l;
-	int len;
-
-	l = malloc(sizeof *l);
-	if(l==nil)
-		sysfatal("malloc: %r");
-	l->t = linetype(s);
-	l->s = s;
-	l->l = n;
-	if(l->t != Lfile && l->t != Lsep)
-		l->f = f;
-	else
-		l->f = nil;
-	len = strlen(s);
-	if(len > maxlength)
-		maxlength = len;
-	return l;
-}
-
 int
 lineno(char *s)
 {
@@ -370,48 +524,55 @@
 parse(int fd)
 {
 	Biobuf *bp;
-	Line *l;
-	char *s, *f, *t;
-	int n, ab;
+	Block *b;
+	char *s, *f, *tab;
+	int t, n, ab, len;
 
+	blocks = nil;
+	nblocks = 0;
 	ab = 0;
 	n = 0;
-	f = nil;
-	lsize = 64;
-	lcount = 0;
-	lines = malloc(lsize * sizeof *lines);
-	if(lines==nil)
-		sysfatal("malloc: %r");
 	bp = Bfdopen(fd, OREAD);
 	if(bp==nil)
 		sysfatal("Bfdopen: %r");
+	b = addblock();
 	for(;;){
 		s = Brdstr(bp, '\n', 1);
 		if(s==nil)
 			break;
-		l = parseline(f, n, s);
-		if(l->t == Lfile && l->s[0] == '-' && strncmp(l->s+4, "a/", 2)==0)
-			ab = 1;
-		if(l->t == Lfile && l->s[0] == '+'){
-			f = l->s+4;
-			if(ab && strncmp(f, "b/", 2)==0){
-				f += 1;
-				if(access(f, AEXIST) < 0)
+		t = linetype(s);
+		switch(t){
+		case Lfile:
+			if(s[0] == '-'){
+				b = addblock();
+				if(strncmp(s+4, "a/", 2) == 0)
+					ab = 1;
+			}else if(s[0] == '+'){
+				f = s+4;
+				if(ab && strncmp(f, "b/", 2) == 0){
 					f += 1;
+					if(access(f, AEXIST) < 0)
+						f += 1;
+				}
+				tab = strchr(f, '\t');
+				if(tab != nil)
+					*tab = 0;
+				b->f = f;
 			}
-			t = strchr(f, '\t');
-			if(t!=nil)
-				*t = 0;
-		}else if(l->t == Lsep)
-			n = lineno(l->s);
-		else if(l->t == Ladd || l->t == Lnone)
+			break;
+		case Lsep:
+			n = lineno(s) - 1; /* -1 as the separator is not an actual line */
+			if(0){
+		case Ladd:
+		case Lnone:
 			++n;
-		lines[lcount++] = l;
-		if(lcount>=lsize){
-			lsize *= 2;
-			lines = realloc(lines, lsize*sizeof *lines);
-			if(lines==nil)
-				sysfatal("realloc: %r");
+			}
+		default:
+			addline(b, t, n, s);
+			len = strlen(s);
+			if(len > maxlength)
+				maxlength = len;
+			break;
 		}
 	}
 }
@@ -453,7 +614,7 @@
 	}ARGEND;
 
 	parse(0);
-	if(lcount==0){
+	if(nblocks==0){
 		fprint(2, "no diff\n");
 		exits(nil);
 	}
@@ -468,7 +629,10 @@
 	a[Eresize].c = mctl->resizec;
 	a[Ekeyboard].c = kctl->c;
 	initcols(b);
-	eresize();
+	initicons();
+	spacew = stringwidth(font, " ");
+	ellipsisw = stringwidth(font, ellipsis);
+	eresize(0);
 	for(;;){
 		switch(alt(a)){
 		case Emouse:
@@ -475,7 +639,7 @@
 			emouse(m);
 			break;
 		case Eresize:
-			eresize();
+			eresize(1);
 			break;
 		case Ekeyboard:
 			ekeyboard(k);