diff --git a/grid.c b/grid.c index cd9e8fae5..f86636354 100644 --- a/grid.c +++ b/grid.c @@ -1389,6 +1389,10 @@ grid_reflow_move_osc133(struct grid_line *to, struct grid_line *from, from->flags &= ~flag; *src_col[i] = 0; if (flag == GRID_LINE_END_OUTPUT) { + /* Keep status presence with the D marker. */ + to->flags &= ~GRID_LINE_END_OUTPUT_STATUS; + to->flags |= from->flags & GRID_LINE_END_OUTPUT_STATUS; + from->flags &= ~GRID_LINE_END_OUTPUT_STATUS; dst->exit_status = src->exit_status; src->exit_status = 0; } diff --git a/regress/collapse-output-resize.sh b/regress/collapse-output-resize.sh new file mode 100644 index 000000000..163dde1be --- /dev/null +++ b/regress/collapse-output-resize.sh @@ -0,0 +1,89 @@ +#!/bin/sh + +# Keep output mappings and individual folds across resize and refresh. + +. ./input-common.inc + +first=$(printf '%060d' 0 | tr 0 F) +second=SECOND-OUTPUT +seq="\033]133;A\007P0> \033]133;B\007first\r\n" +seq="${seq}\033]133;C\007${first}\r\n\033]133;D;7\007" +seq="${seq}\033]133;A\007P1> \033]133;B\007second\r\n" +seq="${seq}\033]133;C\007${second}\r\n\033]133;D;0\007" +seq="${seq}\033]133;A\007P2> \033]133;B\007" +commands_seq=$seq + +# Narrow output can create more backing rows than there are source rows. +for hlimit in 2000 1; do + start_pane_hlimit mapping 80 8 "$commands_seq" "$hlimit" + $TMUX copy-mode -t mapping: -U || exit 1 + $TMUX resize-window -t mapping: -x 5 || exit 1 + $TMUX send -t mapping: -X search-backward-text FFFFF || exit 1 + $TMUX set-buffer sentinel || exit 1 + $TMUX send -t mapping: -X copy-output || exit 1 + $TMUX save-buffer "$TMP" || exit 1 + printf '%s\n' "$first" >"$EXP" + cmp "$TMP" "$EXP" || fail "copy-output with history-limit $hlimit" +done + +# Selection endpoints must land on the appropriate wrapped segment. +for keys in emacs vi; do + for length in 60 61; do + payload=$(printf "%0${length}d" 0 | tr 0 F) + selection_seq="\033]133;A\007p> \033]133;B\007first\r\n" + 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" + $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 + $TMUX send -t selection: -X search-backward-text FFF || exit 1 + $TMUX set-buffer sentinel || exit 1 + $TMUX send -t selection: -X select-output || exit 1 + $TMUX send -t selection: -X copy-selection || exit 1 + $TMUX save-buffer "$TMP" || exit 1 + printf '%s' "$payload" >"$EXP" + cmp "$TMP" "$EXP" || \ + fail "$keys $length cells at width $width" + done + done +done + +# Refresh must keep a mixture of collapsed and expanded commands. +for refresh in refresh-now refresh-on; do + start_cmd refresh 80 12 \ + "printf '$commands_seq'; sleep 1; printf LIVE; sleep 1; printf LIVE; $INPUT_HOLD" + $TMUX copy-mode -t refresh: -c || exit 1 + $TMUX send -t refresh: -X search-backward-text second || exit 1 + $TMUX send -t refresh: -X expand-output || exit 1 + for width in 20 100; do + $TMUX resize-window -t refresh: -x "$width" || exit 1 + $TMUX send -t refresh: -X "$refresh" || exit 1 + sleep 1 + $TMUX send -t refresh: -X history-top || exit 1 + $TMUX capture-pane -t refresh: -Mp >"$TMP" || exit 1 + grep -q FFFFF "$TMP" && fail "$refresh expanded the first command" + grep -q "$second" "$TMP" || \ + fail "$refresh collapsed the second command" + done +done + +# Initial output and commands without C use different fold anchors. +seq='initial-control\r\nHIDDEN-INITIAL\r\n' +seq="${seq}\033]133;A\007p> \033]133;B\007silent\r\n" +seq="${seq}\033]133;D;7\007HIDDEN-SEPARATOR\r\n" +seq="${seq}\033]133;A\007p> \033]133;B\007" +start_pane_history initial 80 12 "$seq" +$TMUX copy-mode -t initial: -c || exit 1 +for width in 20 100; do + $TMUX resize-window -t initial: -x "$width" || exit 1 + $TMUX send -t initial: -X refresh-now || exit 1 + $TMUX send -t initial: -X history-top || exit 1 + $TMUX capture-pane -t initial: -Mp >"$TMP" || exit 1 + grep -q HIDDEN "$TMP" && fail "initial folds lost at width $width" + grep -q initial-control "$TMP" || fail 'initial control line lost' +done + +exit "$exit_status" diff --git a/regress/copy-mode-gutter-resize.sh b/regress/copy-mode-gutter-resize.sh new file mode 100644 index 000000000..2d988cdb0 --- /dev/null +++ b/regress/copy-mode-gutter-resize.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +# Exercise clipped gutter drawing through a real client. + +PATH=/bin:/usr/bin +TERM=screen +export TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +INNER="$TEST_TMUX -Li$$ -f/dev/null" +OUTER="$TEST_TMUX -Lo$$ -f/dev/null" +TMP=$(mktemp) +exit_status=0 +trap 'rm -f "$TMP"; $INNER kill-server 2>/dev/null; $OUTER kill-server 2>/dev/null' 0 1 15 + +$INNER new-session -d -x 80 -y 12 \ + "printf '\033]133;A\007p> \033]133;B\007failed\r\n\033]133;C\007012345678901234567890123456789012345678901234567890123456789\033]133;D;7\007\r\n\033]133;A\007p> \033]133;B\007'; exec sleep 100" || exit 1 +$INNER set -g status off || exit 1 +$INNER set -g window-size manual || exit 1 +$INNER set -g copy-mode-position-format '' || exit 1 +$INNER set -g copy-mode-exit-status-format \ + '#{?exit_status_present,RC=#{exit_status},}' || exit 1 +$OUTER new-session -d -x 80 -y 12 "$INNER attach" || exit 1 +$OUTER set -g status off || exit 1 +sleep 1 + +# Reflow must move the exit status presence flag with its numeric value. +for width in 20 100; do + $INNER resize-window -x "$width" || exit 1 + $INNER copy-mode -U || exit 1 + $INNER send -X history-top || exit 1 + sleep 0.3 + $OUTER capture-pane -p >"$TMP" || exit 1 + if ! grep -q RC=7 "$TMP"; then + echo "exit status disappeared at width $width" + exit_status=1 + fi + $INNER send -X cancel || exit 1 +done + +# A gutter wider than the pane must not wrap or scroll the displayed rows. +$INNER resize-window -x 20 || exit 1 +$INNER set -g copy-mode-exit-status-format \ + 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' || exit 1 +$INNER copy-mode -c || exit 1 +$INNER send -X history-top || exit 1 +sleep 0.3 +$OUTER capture-pane -p >"$TMP" || exit 1 +if ! grep -q '^XXXXXXXXXXXXXXX' "$TMP"; then + echo 'wide gutter scrolled the displayed rows' + exit_status=1 +fi +$INNER display -p '#{copy_cursor_y}' | \ + awk '$1 < 12 { found = 1 } END { exit !found }' || exit 1 + +# Line numbers must also fit when the gutter leaves no room for them. +$INNER send -X line-numbers-on || exit 1 +for width in 3 1; do + $INNER resize-window -x "$width" || exit 1 + $INNER send -X history-top || exit 1 + sleep 0.3 + $OUTER capture-pane -p >"$TMP" || exit 1 + if [ "$width" = 3 ]; then + grep -q '^1 p' "$TMP" || exit 1 + else + grep -q '^p' "$TMP" || exit 1 + fi +done + +exit "$exit_status" diff --git a/window-copy.c b/window-copy.c index 1da5fdbcc..8817d4dd4 100644 --- a/window-copy.c +++ b/window-copy.c @@ -364,7 +364,7 @@ struct window_copy_mode_data { u_char *source_flags; /* flags per source line */ u_int source_line_count; struct window_copy_line *lines; /* source of each backing line */ - u_int line_count; + u_int line_count; /* allocated backing metadata rows */ u_int sync_added; /* snapshot of backing grid counters */ u_int sync_collected; @@ -549,6 +549,28 @@ window_copy_clone_screen(struct screen *src, struct screen *hint, u_int *cx, return (dst); } +/* Grow backing metadata to cover rows added by wrapping. */ +static void +window_copy_grow_line_info(struct window_copy_mode_data *data, u_int count) +{ + u_int old_count = data->line_count, new_count, y; + + if (count <= old_count) + return; + new_count = count; + if (new_count < 2 * old_count) + new_count = 2 * old_count; + data->lines = xreallocarray(data->lines, new_count, + sizeof *data->lines); + memset(data->lines + old_count, 0, + (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].output_line = UINT_MAX; + } + data->line_count = new_count; +} + /* Get the source mapping and fold metadata for a backing line. */ static struct window_copy_line * window_copy_get_line_info(struct window_copy_mode_data *data, u_int y) @@ -996,12 +1018,9 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) screen_size_y(&data->screen), screen_hlimit(src)); dst->grid->flags |= GRID_HISTORY; free(data->lines); - data->lines = xcalloc(total, sizeof *data->lines); - data->line_count = total; - for (y = 0; y < total; y++) { - data->lines[y].source_line = UINT_MAX; - data->lines[y].output_line = UINT_MAX; - } + data->lines = NULL; + data->line_count = 0; + window_copy_grow_line_info(data, total); /* Blank lines at the end would only scroll the folded view. */ last = total; while (last > 1) { @@ -1028,6 +1047,9 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) sgl = grid_get_line(sgd, y); osc133 = &sgl->osc133_data; for (x = 0; x <= sgl->cellused; x++) { + /* Wrapped backing rows may outnumber source rows. */ + window_copy_grow_line_info(data, + screen_hsize(dst) + screen_size_y(dst)); dy = dst->grid->hsize + dst->cy; was_hiding = fold.hiding; fold.source_y = y; @@ -4008,6 +4030,101 @@ window_copy_cmd_search_forward_incremental(struct window_copy_cmd_state *cs) return (action); } +/* Copy collapse flags to matching output markers in a refreshed source. */ +static u_char * +window_copy_clone_fold_flags(struct window_mode_entry *wme, struct screen *src) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *old = data->source->grid; + struct grid *gd = src->grid, *rgd; + struct grid_line *gl, *new_gl, *rgl; + struct screen *resized; + u_char *flags; + u_int y, x, new_y, wx = 0, wy = 0; + u_int reflow_y = 0, reflow_col = 0; + u_int reflow_row = 0; + u_int col, flag, collected, first_prompt; + u_int total = gd->hsize + gd->sy; + + flags = xcalloc(total, sizeof *flags); + + /* Expanded views have no old marker positions to preserve. */ + if (!window_copy_any_fold_collapsed(data)) + return (flags); + collected = wme->swp->base.grid->scroll_collected - + data->sync_collected; + + /* Reflow a copy of the old source to locate its surviving markers. */ + resized = window_copy_clone_screen(data->source, src, NULL, NULL, 0); + rgd = resized->grid; + for (y = 0; y < data->source_line_count; y++) { + gl = grid_get_line(old, y); + flag = 0; + col = 0; + if (gl->flags & GRID_LINE_START_OUTPUT) { + flag = GRID_LINE_START_OUTPUT; + col = gl->osc133_data.out_start_col; + } else if (gl->flags & GRID_LINE_END_OUTPUT) { + flag = GRID_LINE_END_OUTPUT; + col = gl->osc133_data.out_end_col; + } + if ((data->source_flags[y] & WINDOW_COPY_FOLD_COLLAPSED) && + flag != 0) { + /* Find this marker's reflowed row and column. */ + while (reflow_y + 1 < rgd->hsize + rgd->sy) { + rgl = grid_get_line(rgd, reflow_y); + if (reflow_row == wy && + ((~rgl->flags & GRID_LINE_WRAPPED) || + wx + col < reflow_col + rgl->cellused)) { + break; + } + if (rgl->flags & GRID_LINE_WRAPPED) + reflow_col += rgl->cellused; + else { + reflow_col = 0; + reflow_row++; + } + reflow_y++; + } + x = wx + col - reflow_col; + new_y = reflow_y; + + /* Skip markers collected from history. */ + if (new_y >= collected && new_y - collected < total) { + new_y -= collected; + new_gl = grid_get_line(gd, new_y); + if (flag == GRID_LINE_START_OUTPUT) + col = new_gl->osc133_data.out_start_col; + else + col = new_gl->osc133_data.out_end_col; + if ((new_gl->flags & flag) && col == x) { + flags[new_y] = + WINDOW_COPY_FOLD_COLLAPSED; + } + } + } + + /* Include marker boundaries after the last cell. */ + if (gl->flags & GRID_LINE_WRAPPED) + wx += gl->cellused; + else { + wx = 0; + wy++; + } + } + screen_free(resized); + free(resized); + + /* Keep the initial fold if both sources retain leading output. */ + first_prompt = window_copy_first_prompt(old); + if (collected == 0 && first_prompt != 0 && first_prompt != UINT_MAX) { + first_prompt = window_copy_first_prompt(gd); + if (first_prompt != 0 && first_prompt != UINT_MAX) + flags[0] = data->source_flags[0]; + } + return (flags); +} + /* * Reconcile the backing screen with the live pane, incrementally if possible * and otherwise by recloning, then reposition the view. When following, jump @@ -4019,23 +4136,34 @@ window_copy_do_refresh(struct window_mode_entry *wme, int follow) { struct window_pane *wp = wme->swp; struct window_copy_mode_data *data = wme->data; + struct screen *source; + u_char *flags; u_int oy_from_top; - int refold; if (data->oy > screen_hsize(data->backing)) data->oy = screen_hsize(data->backing); oy_from_top = screen_hsize(data->backing) - data->oy; if (!window_copy_sync_backing(wme)) { - refold = data->fold_view; - if (refold) - window_copy_destroy_fold_view(wme); - screen_free(data->backing); - free(data->backing); - data->backing = window_copy_clone_screen(&wp->base, - &data->screen, NULL, NULL, wme->swp != wme->wp); - if (refold) - window_copy_create_fold_view(wme); + if (data->fold_view) { + /* Preserve folds before replacing the old source. */ + source = window_copy_clone_screen(&wp->base, + &data->screen, NULL, NULL, wme->swp != wme->wp); + flags = window_copy_clone_fold_flags(wme, source); + screen_free(data->source); + free(data->source); + free(data->source_flags); + data->source = source; + data->source_flags = flags; + data->source_line_count = screen_hsize(source) + + screen_size_y(source); + window_copy_rebuild_backing(wme); + } else { + screen_free(data->backing); + free(data->backing); + data->backing = window_copy_clone_screen(&wp->base, + &data->screen, NULL, NULL, wme->swp != wme->wp); + } } if (follow) { @@ -4363,7 +4491,8 @@ window_copy_find_backing_position(struct window_mode_entry *wme, u_int source_x, { struct window_copy_mode_data *data = wme->data; struct window_copy_line *line; - u_int y, total; + struct grid_line *gl; + u_int x, y, total; if (!data->fold_view) { *backing_x = source_x; @@ -4376,14 +4505,20 @@ window_copy_find_backing_position(struct window_mode_entry *wme, u_int source_x, if (line == NULL || line->source_line != source_y) continue; if (source_x >= line->source_col) { - *backing_x = line->backing_col + source_x - - line->source_col; + x = line->backing_col + source_x - line->source_col; } else { if (line->source_col - source_x > line->backing_col) - return (0); - *backing_x = line->backing_col - (line->source_col - - source_x); + continue; + x = line->backing_col - (line->source_col - source_x); } + + /* A source row may span several backing rows after wrapping. */ + gl = grid_get_line(data->backing->grid, y); + if (x > gl->cellused || + (x == gl->cellused && (gl->flags & GRID_LINE_WRAPPED))) { + continue; + } + *backing_x = x; *backing_y = y; return (1); } @@ -6914,11 +7049,16 @@ static u_int window_copy_left_margin(struct window_mode_entry *wme) { struct window_copy_mode_data *data = wme->data; - u_int width; + u_int width, sx; + sx = screen_size_x(&data->screen); width = window_copy_line_number_width(wme); if (data->fold_view) width += data->output_status_width + 2; + + /* Leave at least one column for text in a narrow pane. */ + if (data->fold_view && width >= sx) + width = sx - 1; return (width); } @@ -7063,11 +7203,17 @@ window_copy_write_gutter(struct window_mode_entry *wme, struct window_copy_mode_data *data = wme->data; struct window_copy_line *line; struct grid_cell control_gc; - u_int backing_y, i, width; + u_int backing_y, i, width, status_width, sx; char control, *status; - /* Reserve the status field, one control column and a trailing space. */ - width = data->output_status_width + 2; + /* Clip the gutter to leave one text column and prevent wrapping. */ + sx = screen_size_x(ctx->s); + if (line_width >= sx - 1) + return; + width = window_copy_left_margin(wme) - line_width; + status_width = 0; + if (width > 2) + status_width = width - 2; /* Find the metadata for the backing row currently displayed at py. */ backing_y = screen_hsize(data->backing) - data->oy + py; @@ -7082,7 +7228,7 @@ window_copy_write_gutter(struct window_mode_entry *wme, /* Draw the control or tree symbol after the reserved status field. */ if (control != ' ') { screen_write_cursormove(ctx, - line_width + data->output_status_width, py, 0); + line_width + status_width, py, 0); memcpy(&control_gc, gc, sizeof control_gc); /* Draw tree symbols with ACS; +/- use ordinary text. */ @@ -7095,11 +7241,13 @@ window_copy_write_gutter(struct window_mode_entry *wme, screen_write_putc(ctx, &control_gc, control); } /* Show a command's exit status only on its fold control row. */ - if (line == NULL || (~line->flags & WINDOW_COPY_LINE_CONTROL)) + if (line == NULL || (~line->flags & WINDOW_COPY_LINE_CONTROL) || + status_width == 0) { return; + } status = window_copy_expand_exit_status(wme, line); screen_write_cursormove(ctx, line_width, py, 0); - format_draw(ctx, gc, data->output_status_width, status, NULL, 0); + format_draw(ctx, gc, status_width, status, NULL, 0); free(status); } @@ -7127,6 +7275,8 @@ window_copy_write_line(struct window_mode_entry *wme, /* Leave room for both line numbers and the fold/status gutter. */ line_width = window_copy_line_number_width(wme); width = window_copy_left_margin(wme); + if (data->fold_view && line_width > width) + line_width = width; if (width >= sx) content_sx = 1; else if (width != 0)