From 5f638e4f3fe760af4458675791785006568380ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?= <deso@posteo.net>
Date: Mon, 6 Apr 2026 09:17:52 -0700
Subject: [PATCH 3/3] Improve pane resizing performance

Defer reflow of history lines when a pane's width changes. Instead of
reflowing the entire scrollback buffer immediately, only reflow the
visible portion and bulk-copy history lines unchanged. History is
reflowed on demand when actually accessed, such as entering copy mode or
capture-pane. This makes pane resizing near-instant regardless of
scrollback size.
---
 cmd-capture-pane.c |   7 ++-
 cmd-resize-pane.c  |   1 +
 grid.c             | 139 ++++++++++++++++++++++++++++++++++++++++++++-
 screen.c           |  19 ++++++-
 tmux.h             |   3 +
 window-copy.c      |   4 ++
 6 files changed, 169 insertions(+), 4 deletions(-)

diff --git a/cmd-capture-pane.c b/cmd-capture-pane.c
index 6c78e9..b27bfc 100644
--- a/cmd-capture-pane.c
+++ b/cmd-capture-pane.c
@@ -137,7 +137,10 @@ cmd_capture_pane_grid(struct window_pane *wp, size_t *len)
 	struct osc133_data	*od;
 	char			*buf = xstrdup(""), *line;
 	char			 p[11];
-	u_int			 yy, xx, total = gd->hsize + gd->sy;
+	u_int			 yy, xx, total;
+
+	grid_reflow_history(gd);
+	total = gd->hsize + gd->sy;
 
 	xasprintf(&line, "G %ux%u (%u/%u)\n", gd->sx, gd->sy, gd->hsize,
 	    gd->hlimit);
@@ -266,6 +269,8 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
 	const char			*Sflag, *Eflag;
 	size_t				 linelen;
 
+	grid_reflow_history(wp->base.grid);
+
 	sx = screen_size_x(&wp->base);
 	if (args_has(args, 'a')) {
 		gd = wp->base.saved_grid;
diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c
index c644dd..e1e943 100644
--- a/cmd-resize-pane.c
+++ b/cmd-resize-pane.c
@@ -71,6 +71,7 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item)
 	if (args_has(args, 'T')) {
 		if (!TAILQ_EMPTY(&wp->modes))
 			return (CMD_RETURN_NORMAL);
+		grid_reflow_history(gd); /* lines come into view from history */
 		adjust = screen_size_y(&wp->base) - 1 - wp->base.cy;
 		if (adjust > (int)gd->hsize)
 			adjust = gd->hsize;
diff --git a/grid.c b/grid.c
index 424f5b..7bfd07 100644
--- a/grid.c
+++ b/grid.c
@@ -439,6 +439,11 @@ grid_trim_history(struct grid *gd, u_int ny)
 	memmove(&gd->linedata[0], &gd->linedata[ny],
 	    remaining * (sizeof *gd->linedata));
 	memset(&gd->linedata[remaining], 0, ny * (sizeof *gd->linedata));
+
+	if (gd->reflow_hsize > ny)
+		gd->reflow_hsize -= ny;
+	else
+		gd->reflow_hsize = 0;
 }
 
 /*
@@ -1531,17 +1536,53 @@ grid_reflow(struct grid *gd, u_int sx)
 	struct grid_line	*gl;
 	struct grid_cell	 gc;
 	u_int			 yy, width, i, at;
+	u_int			 boundary;
+
+	/*
+	 * Find the reflow boundary: walk backward from hsize to find the
+	 * latest position where the preceding line is not wrapped. Lines
+	 * before the boundary form complete logical lines and their reflow
+	 * can be deferred until history is actually accessed.
+	 */
+	boundary = gd->hsize;
+	while (boundary > 0 &&
+	    (gd->linedata[boundary - 1].flags & GRID_LINE_WRAPPED))
+		boundary--;
+
+	/*
+	 * If there is already a pending deferred reflow, make sure we do
+	 * not shrink the unreflowed region (those lines still need reflow).
+	 */
+	if (gd->reflow_hsize > boundary)
+		boundary = gd->reflow_hsize;
 
 	/*
 	 * Create a destination grid. This is just used as a container for the
 	 * line data and may not be fully valid.
 	 */
 	target = grid_create(gd->sx, 0, 0);
+	target->linedata = xcalloc(gd->hsize + gd->sy,
+	    sizeof *target->linedata);
 
 	/*
-	 * Loop over each source line.
+	 * Phase 1: Bulk-copy unreflowed history lines (0..boundary-1).
+	 * These lines are complete logical lines that will be reflowed
+	 * on demand when history is accessed.
 	 */
-	for (yy = 0; yy < gd->hsize + gd->sy; yy++) {
+	if (boundary > 0) {
+		grid_reflow_add(target, boundary);
+		memcpy(target->linedata, gd->linedata,
+		    boundary * sizeof *target->linedata);
+		memset(gd->linedata, 0,
+		    boundary * sizeof *gd->linedata);
+		for (i = 0; i < boundary; i++)
+			gd->linedata[i].flags = GRID_LINE_DEAD;
+	}
+
+	/*
+	 * Phase 2: Reflow from boundary to end.
+	 */
+	for (yy = boundary; yy < gd->hsize + gd->sy; yy++) {
 		gl = &gd->linedata[yy];
 		if (gl->flags & GRID_LINE_DEAD)
 			continue;
@@ -1605,6 +1646,100 @@ grid_reflow(struct grid *gd, u_int sx)
 		gd->hscrolled = gd->hsize;
 	free(gd->linedata);
 	gd->linedata = target->linedata;
+	gd->reflow_hsize = boundary;
+	free(target);
+	gd->scroll_generation++;
+
+	/*
+	 * If joins reduced the line count enough that unreflowed history
+	 * lines ended up in the visible area, reflow them now.
+	 */
+	if (gd->reflow_hsize > gd->hsize)
+		grid_reflow_history(gd);
+}
+
+/* Reflow deferred history lines to the current grid width. */
+void
+grid_reflow_history(struct grid *gd)
+{
+	struct grid		*target;
+	struct grid_line	*gl, *new_linedata;
+	struct grid_cell	 gc;
+	u_int			 yy, width, i, at;
+	u_int			 n, m, remaining, new_total;
+
+	if (gd->reflow_hsize == 0)
+		return;
+
+	n = gd->reflow_hsize;
+
+	/* Build a temporary target for the reflowed history. */
+	target = grid_create(gd->sx, 0, 0);
+	target->linedata = xcalloc(n, sizeof *target->linedata);
+
+	for (yy = 0; yy < n; yy++) {
+		gl = &gd->linedata[yy];
+		if (gl->flags & GRID_LINE_DEAD)
+			continue;
+
+		at = width = 0;
+		if (~gl->flags & GRID_LINE_EXTENDED) {
+			width = gl->cellused;
+			if (width > gd->sx)
+				at = gd->sx;
+			else
+				at = width;
+		} else {
+			for (i = 0; i < gl->cellused; i++) {
+				grid_get_cell1(gl, i, &gc);
+				if (at == 0 && width + gc.data.width > gd->sx)
+					at = i;
+				width += gc.data.width;
+			}
+		}
+
+		if (width == gd->sx)
+			grid_reflow_move(target, gl);
+		else if (width > gd->sx)
+			grid_reflow_split(target, gd, gd->sx, yy, at);
+		else if (gl->flags & GRID_LINE_WRAPPED)
+			grid_reflow_join(target, gd, gd->sx, yy, width, 0);
+		else
+			grid_reflow_move(target, gl);
+	}
+
+	/* Splice: replace lines 0..n-1 with the reflowed result. */
+	m = target->sy;
+	remaining = gd->hsize + gd->sy - n;
+	new_total = m + remaining;
+
+	/*
+	 * If the total after the splice would not be enough to hold the
+	 * visible region, pad with empty lines. Without this, gd->hsize
+	 * would underflow and subsequent linedata accesses would be OOB.
+	 */
+	if (new_total < gd->sy)
+		new_total = gd->sy;
+
+	new_linedata = xreallocarray(NULL, new_total, sizeof *new_linedata);
+	memcpy(new_linedata, target->linedata, m * sizeof *new_linedata);
+	memcpy(new_linedata + m, gd->linedata + n,
+	    remaining * sizeof *new_linedata);
+	if (new_total > m + remaining) {
+		memset(new_linedata + m + remaining, 0,
+		    (new_total - m - remaining) * sizeof *new_linedata);
+	}
+
+	free(gd->linedata);
+	gd->linedata = new_linedata;
+
+	gd->hsize = new_total - gd->sy;
+	if (gd->hscrolled > gd->hsize)
+		gd->hscrolled = gd->hsize;
+
+	gd->reflow_hsize = 0;
+
+	free(target->linedata);
 	free(target);
 	gd->scroll_generation++;
 }
diff --git a/screen.c b/screen.c
index f9ca89..4a75a3 100644
--- a/screen.c
+++ b/screen.c
@@ -410,7 +410,7 @@ static void
 screen_resize_y(struct screen *s, u_int sy, int eat_empty, u_int *cy)
 {
 	struct grid	*gd = s->grid;
-	u_int		 needed, available, oldy, i;
+	u_int		 needed, available, oldy, i, hsize;
 
 	if (sy == 0)
 		fatalx("zero size");
@@ -460,6 +460,23 @@ screen_resize_y(struct screen *s, u_int sy, int eat_empty, u_int *cy)
 		}
 	}
 
+	/*
+	 * If the height is increasing, lines will be pulled from the history
+	 * into view below. If any of them are still waiting for a deferred
+	 * reflow, reflow the history now, before resizing the line array. The
+	 * cursor line moves with the rest of the grid.
+	 */
+	if (sy > oldy && gd->reflow_hsize != 0 && (gd->flags & GRID_HISTORY)) {
+		available = sy - oldy;
+		if (available > gd->hscrolled)
+			available = gd->hscrolled;
+		if (gd->reflow_hsize + available > gd->hsize) {
+			hsize = gd->hsize;
+			grid_reflow_history(gd);
+			*cy = *cy - hsize + gd->hsize;
+		}
+	}
+
 	/* Resize line array. */
 	grid_adjust_lines(gd, gd->hsize + sy);
 
diff --git a/tmux.h b/tmux.h
index b53dae..77ddab 100644
--- a/tmux.h
+++ b/tmux.h
@@ -933,6 +933,8 @@ struct grid {
 	u_int			 scroll_collected;
 	u_int			 scroll_generation;
 
+	u_int			 reflow_hsize;
+
 	struct grid_line	*linedata;
 };
 
@@ -3527,6 +3529,7 @@ char	*grid_string_cells(struct grid *, u_int, u_int, u_int,
 void	 grid_duplicate_lines(struct grid *, u_int, struct grid *, u_int,
 	     u_int);
 void	 grid_reflow(struct grid *, u_int);
+void	 grid_reflow_history(struct grid *);
 void	 grid_wrap_position(struct grid *, u_int, u_int, u_int *, u_int *);
 void	 grid_unwrap_position(struct grid *, u_int *, u_int *, u_int, u_int);
 u_int	 grid_line_length(struct grid *, u_int);
diff --git a/window-copy.c b/window-copy.c
index 8cf2d8..4947d7 100644
--- a/window-copy.c
+++ b/window-copy.c
@@ -397,6 +397,8 @@ window_copy_clone_screen(struct screen *src, struct screen *hint, u_int *cx,
 	u_int			 sy, wx, wy;
 	int			 reflow;
 
+	grid_reflow_history(src->grid);
+
 	dst = xcalloc(1, sizeof *dst);
 
 	sy = screen_hsize(src) + screen_size_y(src);
@@ -442,6 +444,7 @@ window_copy_clone_screen(struct screen *src, struct screen *hint, u_int *cx,
 		grid_wrap_position(dst->grid, *cx, *cy, &wx, &wy);
 	screen_resize_cursor(dst, screen_size_x(hint), screen_size_y(hint), 1,
 	    0, 0);
+	grid_reflow_history(dst->grid);
 	if (reflow)
 		grid_unwrap_position(dst->grid, cx, cy, wx, wy);
 
@@ -1226,6 +1229,7 @@ window_copy_resize(struct window_mode_entry *wme, u_int sx, u_int sy)
 	if (reflow)
 		grid_wrap_position(gd, cx, cy, &wx, &wy);
 	screen_resize_cursor(data->backing, sx, sy, 1, 0, 0);
+	grid_reflow_history(gd);
 	if (reflow)
 		grid_unwrap_position(gd, &cx, &cy, wx, wy);
 
-- 
2.55.0

