Do not update selection endpoints when the copy mode view scrolls unless a

mouse drag is active. This keeps an existing selection anchored to the same
text while using scroll commands or the mouse wheel.

Allow dragging from inside an existing selection to extend it from the nearest
endpoint, including the copy mode cursor endpoint, while preserving the old
behavior of starting a new selection when dragging from outside the selection.

Added regression tests to test this behaviour.
This commit is contained in:
Michael Grant
2026-07-09 23:47:29 +01:00
parent ec31f4566d
commit 4434284f30
2 changed files with 259 additions and 7 deletions

View File

@@ -0,0 +1,113 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
}
fail()
{
echo "$1"
cleanup
exit 1
}
expect_buffer()
{
expected=$1
actual=$($TMUX show-buffer)
[ "$actual" = "$expected" ] ||
fail "unexpected buffer: expected [$expected], got [$actual]"
}
wheel()
{
button=$1
col=$2
row=$3
seq=$(printf '\033[<%s;%s;%sM' "$button" "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
trap cleanup 0
trap 'exit 1' 1 2 3 15
$TMUX new -d -x40 -y10 \
'i=0; while [ $i -lt 80 ]; do printf "line %02d xxxxxxxxxx\n" $i; i=$((i + 1)); done; cat' ||
exit 1
$TMUX set -g window-size manual || exit 1
$TMUX set -g mouse on || exit 1
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -N10 -X cursor-down || exit 1
$TMUX send-keys -X start-of-line || exit 1
$TMUX send-keys -X begin-selection || exit 1
$TMUX send-keys -N2 -X cursor-down || exit 1
$TMUX send-keys -X copy-selection-no-clear || exit 1
initial=$(printf 'line 10 xxxxxxxxxx\nline 11 xxxxxxxxxx')
expect_buffer "$initial"
$TMUX send-keys -X stop-selection || exit 1
$TMUX send-keys -N3 -X scroll-down || exit 1
$TMUX send-keys -X copy-selection-no-clear || exit 1
expect_buffer "$initial"
$TMUX send-keys -N2 -X scroll-up || exit 1
$TMUX send-keys -X copy-selection-no-clear || exit 1
expect_buffer "$initial"
$TMUX send-keys -X scroll-middle || exit 1
$TMUX send-keys -X copy-selection-no-clear || exit 1
expect_buffer "$initial"
$TMUX send-keys -X scroll-bottom || exit 1
$TMUX send-keys -X copy-selection-no-clear || exit 1
expect_buffer "$initial"
$TMUX send-keys -X scroll-top || exit 1
$TMUX send-keys -X copy-selection-no-clear || exit 1
expect_buffer "$initial"
$TMUX send-keys -X recentre-top-bottom || exit 1
$TMUX send-keys -X copy-selection-no-clear || exit 1
expect_buffer "$initial"
$TMUX send-keys -X other-end || exit 1
$TMUX send-keys -X cursor-down || exit 1
$TMUX send-keys -X copy-selection-no-clear || exit 1
extended_end=$(printf 'line 10 xxxxxxxxxx\nline 11 xxxxxxxxxx\nline 12 xxxxxxxxxx')
expect_buffer "$extended_end"
$TMUX send-keys -X stop-selection || exit 1
$TMUX send-keys -X other-end || exit 1
$TMUX send-keys -X other-end || exit 1
$TMUX send-keys -X cursor-up || exit 1
$TMUX send-keys -X copy-selection-no-clear || exit 1
extended_start=$(printf 'line 09 xxxxxxxxxx\nline 10 xxxxxxxxxx\nline 11 xxxxxxxxxx\nline 12 xxxxxxxxxx')
expect_buffer "$extended_start"
$TMUX2 new-session -d -x40 -y10 "$TMUX attach" || exit 1
sleep 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "no outer pane"
wheel 65 5 5
$TMUX send-keys -X copy-selection-no-clear || exit 1
expect_buffer "$extended_start"
wheel 64 5 5
$TMUX send-keys -X copy-selection-no-clear || exit 1
expect_buffer "$extended_start"
exit 0

View File

@@ -103,8 +103,12 @@ static void window_copy_goto_line(struct window_mode_entry *, const char *);
static void window_copy_update_cursor(struct window_mode_entry *, u_int,
u_int);
static void window_copy_start_selection(struct window_mode_entry *);
static int window_copy_mouse_in_selection(struct window_mode_entry *,
u_int, u_int, int *, int *);
static int window_copy_adjust_selection(struct window_mode_entry *,
u_int *, u_int *);
static int window_copy_update_selection_view(struct window_mode_entry *,
int, int);
static int window_copy_set_selection(struct window_mode_entry *, int, int);
static int window_copy_update_selection(struct window_mode_entry *, int,
int);
@@ -861,7 +865,7 @@ window_copy_scroll1(struct window_mode_entry *wme, struct window_pane *wp,
if (data->searchmark != NULL && !data->timeout)
window_copy_search_marks(wme, NULL, data->searchregex, 1);
window_copy_update_selection(wme, 1, 0);
window_copy_update_selection_view(wme, 1, 0);
window_pane_scrollbar_show(wp, 1);
window_copy_redraw_screen(wme);
}
@@ -1647,7 +1651,7 @@ window_copy_cmd_scroll_to(struct window_copy_cmd_state *cs, u_int to)
data->cy += delta;
}
window_copy_update_selection(wme, 0, 0);
window_copy_update_selection_view(wme, 0, 0);
return (WINDOW_COPY_CMD_REDRAW);
}
@@ -2349,6 +2353,15 @@ window_copy_cmd_scroll_down(struct window_copy_cmd_state *cs)
struct window_mode_entry *wme = cs->wme;
struct window_copy_mode_data *data = wme->data;
u_int np = wme->prefix;
int dragging;
dragging = (cs->c != NULL && cs->c->tty.mouse_drag_flag != 0);
if (data->screen.sel != NULL && !dragging) {
data->cursordrag = CURSORDRAG_NONE;
data->lineflag = LINE_SEL_NONE;
window_copy_scroll_up(wme, np);
return (WINDOW_COPY_CMD_NOTHING);
}
for (; np != 0; np--)
window_copy_cursor_down(wme, 1);
@@ -2375,7 +2388,17 @@ static enum window_copy_cmd_action
window_copy_cmd_scroll_up(struct window_copy_cmd_state *cs)
{
struct window_mode_entry *wme = cs->wme;
struct window_copy_mode_data *data = wme->data;
u_int np = wme->prefix;
int dragging;
dragging = (cs->c != NULL && cs->c->tty.mouse_drag_flag != 0);
if (data->screen.sel != NULL && !dragging) {
data->cursordrag = CURSORDRAG_NONE;
data->lineflag = LINE_SEL_NONE;
window_copy_scroll_down(wme, np);
return (WINDOW_COPY_CMD_NOTHING);
}
for (; np != 0; np--)
window_copy_cursor_up(wme, 1);
@@ -3099,7 +3122,7 @@ window_copy_cmd_recentre_top_bottom(struct window_copy_cmd_state *cs)
data->cy = cy + (data->oy - oy);
break;
}
window_copy_update_selection(wme, 0, 0);
window_copy_update_selection_view(wme, 0, 0);
return (WINDOW_COPY_CMD_REDRAW);
}
@@ -5486,6 +5509,77 @@ window_copy_start_selection(struct window_mode_entry *wme)
window_copy_set_selection(wme, 1, 0);
}
static int
window_copy_mouse_in_selection(struct window_mode_entry *wme, u_int x, u_int y,
int *on_start, int *on_end)
{
struct window_copy_mode_data *data = wme->data;
u_int hsize, screeny;
u_int mx, my, selx, sely, endselx, endsely;
u_int cursorx, cursory;
long long mpos, spos, epos, dstart, dend;
if (on_start != NULL)
*on_start = 0;
if (on_end != NULL)
*on_end = 0;
if (data->screen.sel == NULL)
return (0);
hsize = screen_hsize(data->backing);
screeny = hsize - data->oy;
selx = window_copy_cursor_offset(wme, data->selx,
screen_size_x(&data->screen));
sely = data->sely - screeny;
if (data->sely >= screeny && sely < screen_size_y(&data->screen) &&
x == selx && y == sely) {
if (on_start != NULL)
*on_start = 1;
return (1);
}
endselx = window_copy_cursor_offset(wme, data->endselx,
screen_size_x(&data->screen));
endsely = data->endsely - screeny;
if (data->endsely >= screeny &&
endsely < screen_size_y(&data->screen) &&
x == endselx && y == endsely) {
if (on_end != NULL)
*on_end = 1;
return (1);
}
cursorx = window_copy_cursor_offset(wme, data->cx,
screen_size_x(&data->screen));
cursory = data->cy;
if (x != cursorx || y != cursory) {
if (!screen_check_selection(&data->screen, x, y))
return (0);
}
if (on_start != NULL || on_end != NULL) {
mx = window_copy_cursor_unoffset(wme, x,
screen_size_x(&data->screen));
my = screeny + y;
mpos = (long long)my * (screen_size_x(&data->screen) + 1) + mx;
spos = (long long)data->sely *
(screen_size_x(&data->screen) + 1) + data->selx;
epos = (long long)data->endsely *
(screen_size_x(&data->screen) + 1) + data->endselx;
dstart = llabs(mpos - spos);
dend = llabs(mpos - epos);
if (dstart <= dend) {
if (on_start != NULL)
*on_start = 1;
} else {
if (on_end != NULL)
*on_end = 1;
}
}
return (1);
}
static int
window_copy_adjust_selection(struct window_mode_entry *wme, u_int *selx,
u_int *sely)
@@ -5531,6 +5625,35 @@ window_copy_update_selection(struct window_mode_entry *wme, int may_redraw,
return (window_copy_set_selection(wme, may_redraw, no_reset));
}
/*
* Update the visible selection after the view has changed. If the mouse is
* not dragging, keep the selection endpoints anchored to the same text.
*/
static int
window_copy_update_selection_view(struct window_mode_entry *wme, int may_redraw,
int no_reset)
{
struct window_copy_mode_data *data = wme->data;
u_int selx, sely, endselx, endsely;
int changed;
if (data->cursordrag != CURSORDRAG_NONE)
return (window_copy_update_selection(wme, may_redraw, no_reset));
selx = data->selx;
sely = data->sely;
endselx = data->endselx;
endsely = data->endsely;
changed = window_copy_update_selection(wme, may_redraw, 1);
data->selx = selx;
data->sely = sely;
data->endselx = endselx;
data->endsely = endsely;
return (changed);
}
static int
window_copy_set_selection(struct window_mode_entry *wme, int may_redraw,
int no_reset)
@@ -6560,7 +6683,7 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny)
if (data->searchmark != NULL && !data->timeout)
window_copy_search_marks(wme, NULL, data->searchregex, 1);
window_copy_update_selection(wme, 0, 0);
window_copy_update_selection_view(wme, 0, 0);
if (window_copy_line_numbers_active(wme)) {
if (window_copy_line_number_mode(wme) !=
WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) {
@@ -6629,7 +6752,7 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny)
if (data->searchmark != NULL && !data->timeout)
window_copy_search_marks(wme, NULL, data->searchregex, 1);
window_copy_update_selection(wme, 0, 0);
window_copy_update_selection_view(wme, 0, 0);
if (window_copy_line_numbers_active(wme)) {
if (window_copy_line_number_mode(wme) !=
WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) {
@@ -6718,6 +6841,7 @@ window_copy_start_drag(struct client *c, struct mouse_event *m)
struct window_mode_entry *wme;
struct window_copy_mode_data *data;
u_int x, y, yg;
int inside_selection, on_start, on_end;
if (c == NULL)
return;
@@ -6738,10 +6862,16 @@ window_copy_start_drag(struct client *c, struct mouse_event *m)
c->tty.mouse_drag_release = window_copy_drag_release;
data = wme->data;
on_start = on_end = 0;
inside_selection = window_copy_mouse_in_selection(wme, x, y,
&on_start, &on_end);
x = window_copy_cursor_unoffset(wme, x, screen_size_x(&data->screen));
yg = screen_hsize(data->backing) + y - data->oy;
if (x < data->selrx || x > data->endselrx || yg != data->selry)
if (on_start || on_end || !inside_selection ||
x < data->selrx || x > data->endselrx || yg != data->selry) {
data->lineflag = LINE_SEL_NONE;
data->selflag = SEL_CHAR;
}
switch (data->selflag) {
case SEL_WORD:
if (data->separators != NULL) {
@@ -6757,7 +6887,15 @@ window_copy_start_drag(struct client *c, struct mouse_event *m)
break;
case SEL_CHAR:
window_copy_update_cursor(wme, x, y);
window_copy_start_selection(wme);
if (!inside_selection)
window_copy_start_selection(wme);
else {
if (on_start)
data->cursordrag = CURSORDRAG_SEL;
else if (on_end)
data->cursordrag = CURSORDRAG_ENDSEL;
window_copy_update_selection(wme, 1, 0);
}
break;
}
@@ -6833,6 +6971,7 @@ window_copy_drag_release(struct client *c, struct mouse_event *m)
data = wme->data;
if (window_copy_line_numbers_active(wme))
window_copy_drag_update(c, m);
data->cursordrag = CURSORDRAG_NONE;
evtimer_del(&data->dragtimer);
}