shithub: vdiff

Download patch

ref: 50a3064b6ae10e6c26e552c7cd33aa4ca98e1527
parent: a0b6286313c067b883ce1330499cf02f5cca6c0f
author: phil9 <telephil9@gmail.com>
date: Sun Feb 4 08:16:00 EST 2024

make file blocks collapsible

	this required an almost complete rewrite as the initial logic was
	line oriented and lead to too many hacks to make collapsing work.
	While at it, changed the UI a bit to make blocks more distinctive.

--- a/vdiff.c
+++ b/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,10 +333,28 @@
 }
 
 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)
@@ -247,46 +361,50 @@
 		scrolling = 0;
 
 	if(scrolling){
+		n = 5*(m.xy.y - scrollr.min.y);
 		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(-scrollsize);
+	}else if(m.buttons&16){
+		scroll(scrollsize);
+	}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);