From 2e37e61ff143a7798fdf1e272086b0d89d9c9ec0 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Wed, 14 Oct 2026 15:57:40 +0200 Subject: [PATCH] Fix folded output selection and teardown after resize Map joined source rows in a shared column space so select-output works after widening. Resize the saved source when disabling fold view and preserve the cursor across reflow. Extend resize regressions to cover widening and fold view teardown. --- regress/collapse-output-resize.sh | 54 ++++++++++++++++++-- window-copy.c | 85 ++++++++++++++++++++++++++----- 2 files changed, 123 insertions(+), 16 deletions(-) diff --git a/regress/collapse-output-resize.sh b/regress/collapse-output-resize.sh index 163dde1be..65b0ff617 100644 --- a/regress/collapse-output-resize.sh +++ b/regress/collapse-output-resize.sh @@ -34,8 +34,12 @@ for keys in emacs vi; do selection_seq="${selection_seq}\033]133;C\007${payload}" selection_seq="${selection_seq}\033]133;D;7\007\r\n" selection_seq="${selection_seq}\033]133;A\007p> \033]133;B\007" - for width in 5 20 100; do - start_pane_history selection 80 12 "$selection_seq" + for sizes in '80 5' '80 20' '80 100' '20 5' '20 80' '20 100'; do + set -- $sizes + initial_width=$1 + width=$2 + start_pane_history selection "$initial_width" 12 \ + "$selection_seq" $TMUX set -w -t selection: mode-keys "$keys" || exit 1 $TMUX copy-mode -t selection: -U || exit 1 $TMUX resize-window -t selection: -x "$width" || exit 1 @@ -46,11 +50,55 @@ for keys in emacs vi; do $TMUX save-buffer "$TMP" || exit 1 printf '%s' "$payload" >"$EXP" cmp "$TMP" "$EXP" || \ - fail "$keys $length cells at width $width" + fail "$keys $length cells, $initial_width to $width" done done done +# Turning folding off must use the same wrapping as ordinary copy mode. +for sizes in '20 80' '80 5'; do + set -- $sizes + start_pane_history toggle "$1" 24 "$selection_seq" + $TMUX copy-mode -t toggle: -U || exit 1 + $TMUX resize-window -t toggle: -x "$2" || exit 1 + $TMUX send -t toggle: -X fold-view-toggle || exit 1 + $TMUX send -t toggle: -X history-top || exit 1 + $TMUX capture-pane -t toggle: -Mp >"$TMP" || exit 1 + $TMUX send -t toggle: -X cancel || exit 1 + $TMUX copy-mode -t toggle: || exit 1 + $TMUX send -t toggle: -X history-top || exit 1 + $TMUX capture-pane -t toggle: -Mp >"$EXP" || exit 1 + cmp "$TMP" "$EXP" || fail "fold view off from $1 to $2" +done + +# Cursor mapping must work in both directions across joined source rows. +payload="$(printf '%030d' 0 | tr 0 F)Z$(printf '%029d' 0 | tr 0 F)" +seq="\033]133;A\007p> \033]133;B\007first\r\n" +seq="${seq}\033]133;C\007${payload}\033]133;D;7\007\r\n" +seq="${seq}\033]133;A\007p> \033]133;B\007" +start_pane_history cursor 20 24 "$seq" +for width in 80 10 20 80; do + $TMUX copy-mode -t cursor: -U || exit 1 + $TMUX resize-window -t cursor: -x "$width" || exit 1 + $TMUX send -t cursor: -X search-backward-text Z || exit 1 + $TMUX set-buffer sentinel || exit 1 + $TMUX send -t cursor: -X copy-output || exit 1 + $TMUX save-buffer "$TMP" || exit 1 + printf '%s' "$payload" >"$EXP" + cmp "$TMP" "$EXP" || fail "cursor output at width $width" + before=$($TMUX display -p -t cursor: \ + '#{copy_cursor_x}:#{copy_cursor_line}') + $TMUX send -t cursor: -X fold-view-toggle || exit 1 + after=$($TMUX display -p -t cursor: \ + '#{copy_cursor_x}:#{copy_cursor_line}') + if [ "$before" != "$after" ]; then + printf '%s\n' "$before" >"$EXP" + printf '%s\n' "$after" >"$TMP" + fail "cursor after turning folding off at width $width" + fi + $TMUX send -t cursor: -X cancel || exit 1 +done + # Refresh must keep a mixture of collapsed and expanded commands. for refresh in refresh-now refresh-on; do start_cmd refresh 80 12 \ diff --git a/window-copy.c b/window-copy.c index 8817d4dd4..7af743602 100644 --- a/window-copy.c +++ b/window-copy.c @@ -317,6 +317,8 @@ struct window_copy_fold_request { struct window_copy_fold_ctx { u_int source_y; /* current row in the unfolded source */ u_int source_x; /* current column in the unfolded source */ + u_int wrapped_y; /* first source row of this wrapped line */ + u_int wrapped_x; /* cells in preceding rows of this line */ u_int prompt_dy; /* prompt row in the backing, or UINT_MAX */ u_int output_line; /* fold flag's source row, or UINT_MAX */ int collapsed; /* current fold is collapsed */ @@ -331,7 +333,8 @@ struct window_copy_fold_ctx { /* Source mapping and gutter metadata for one backing row. */ struct window_copy_line { u_int source_line; /* source row; UINT_MAX if unmapped */ - u_int source_col; /* source column at mapped boundary */ + u_int wrapped_line; /* wrap start row, or UINT_MAX */ + u_int source_col; /* column across wrapped source rows */ u_int backing_col; /* backing column at same boundary */ u_int output_line; /* source fold flag row, or UINT_MAX */ u_char flags; /* WINDOW_COPY_LINE_* bits */ @@ -566,6 +569,7 @@ window_copy_grow_line_info(struct window_copy_mode_data *data, u_int count) (new_count - old_count) * sizeof *data->lines); for (y = old_count; y < new_count; y++) { data->lines[y].source_line = UINT_MAX; + data->lines[y].wrapped_line = UINT_MAX; data->lines[y].output_line = UINT_MAX; } data->line_count = new_count; @@ -606,8 +610,11 @@ window_copy_map_line(struct window_copy_mode_data *data, line = window_copy_get_line_info(data, y); if (line == NULL) return; + + /* Use one column space for all rows of a wrapped source line. */ line->source_line = fold->source_y; - line->source_col = fold->source_x; + line->wrapped_line = fold->wrapped_y; + line->source_col = fold->wrapped_x + fold->source_x; line->backing_col = dst->cx; line->output_line = fold->output_line; } @@ -983,6 +990,10 @@ window_copy_destroy_fold_view(struct window_mode_entry *wme) free(data->backing); data->backing = data->source; data->source = NULL; + + /* The saved source may still be wrapped to an earlier pane size. */ + screen_resize_cursor(data->backing, screen_size_x(&data->screen), + screen_size_y(&data->screen), 1, 0, 0); free(data->source_flags); data->source_flags = NULL; data->source_line_count = 0; @@ -1118,6 +1129,14 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) fold.hiding = data->source_flags[0] & WINDOW_COPY_FOLD_COLLAPSED; } + + /* Keep joined source rows in one mapping coordinate space. */ + if (sgl->flags & GRID_LINE_WRAPPED) + fold.wrapped_x += sgl->cellused; + else { + fold.wrapped_y = y + 1; + fold.wrapped_x = 0; + } } screen_write_stop(&ctx); if (data->backing != NULL) { @@ -4387,16 +4406,21 @@ window_copy_cmd_fold_view_toggle(struct window_copy_cmd_state *cs) { struct window_mode_entry *wme = cs->wme; struct window_copy_mode_data *data = wme->data; - u_int source_x, source_y; + u_int source_x, source_y, wx, wy; if (data->viewmode) return (WINDOW_COPY_CMD_NOTHING); window_copy_get_cursor_source(wme, &source_x, &source_y); window_copy_clear_selection(wme); window_copy_clear_marks(wme); - if (data->fold_view) + if (data->fold_view) { + /* Reflowing the source also changes the cursor coordinates. */ + grid_wrap_position(data->source->grid, source_x, source_y, + &wx, &wy); window_copy_destroy_fold_view(wme); - else + grid_unwrap_position(data->backing->grid, &source_x, &source_y, + wx, wy); + } else window_copy_create_fold_view(wme); if (window_copy_restore_cursor(wme, source_x, source_y)) return (WINDOW_COPY_CMD_REDRAW); @@ -4451,6 +4475,40 @@ window_copy_find_fold(struct window_mode_entry *wme, u_int target, return (1); } +/* Express a source position relative to its first wrapped row. */ +static void +window_copy_wrap_source_position(struct window_copy_mode_data *data, + u_int *x, u_int *y) +{ + struct grid *gd = data->source->grid; + struct grid_line *gl; + + while (*y > 0) { + gl = grid_get_line(gd, *y - 1); + if (~gl->flags & GRID_LINE_WRAPPED) + break; + *x += gl->cellused; + (*y)--; + } +} + +/* Restore a source position to the row containing its column. */ +static void +window_copy_unwrap_source_position(struct window_copy_mode_data *data, + u_int *x, u_int *y) +{ + struct grid *gd = data->source->grid; + struct grid_line *gl; + + while (*y + 1 < gd->hsize + gd->sy) { + gl = grid_get_line(gd, *y); + if (~gl->flags & GRID_LINE_WRAPPED || *x < gl->cellused) + break; + *x -= gl->cellused; + (*y)++; + } +} + /* Map the cursor to its position in the unfolded source. */ static void window_copy_get_cursor_source(struct window_mode_entry *wme, u_int *source_x, @@ -4472,16 +4530,14 @@ window_copy_get_cursor_source(struct window_mode_entry *wme, u_int *source_x, *source_y = data->source_line_count - 1; return; } - *source_y = line->source_line; + *source_y = line->wrapped_line; if (data->cx >= line->backing_col) { *source_x = line->source_col + data->cx - line->backing_col; - return; - } - if (line->backing_col - data->cx <= line->source_col) { + } else if (line->backing_col - data->cx <= line->source_col) { *source_x = line->source_col - (line->backing_col - data->cx); - return; - } - *source_x = 0; + } else + *source_x = 0; + window_copy_unwrap_source_position(data, source_x, source_y); } /* Find the backing position corresponding to a source position. */ @@ -4499,10 +4555,13 @@ window_copy_find_backing_position(struct window_mode_entry *wme, u_int source_x, *backing_y = source_y; return (1); } + + /* Joined source rows share a row and column space in the mappings. */ + window_copy_wrap_source_position(data, &source_x, &source_y); total = screen_hsize(data->backing) + screen_size_y(data->backing); for (y = 0; y < total; y++) { line = window_copy_get_line_info(data, y); - if (line == NULL || line->source_line != source_y) + if (line == NULL || line->wrapped_line != source_y) continue; if (source_x >= line->source_col) { x = line->backing_col + source_x - line->source_col;