Commit: patch 9.2.0407: tabpanel: A few issues with the tabpanel

2 views
Skip to first unread message

Christian Brabandt

unread,
Apr 27, 2026, 5:30:14 PM (20 hours ago) Apr 27
to vim...@googlegroups.com
patch 9.2.0407: tabpanel: A few issues with the tabpanel

Commit: https://github.com/vim/vim/commit/2d43240659868e91e7c371017793091082bfda74
Author: Hirohito Higashi <h.eas...@gmail.com>
Date: Mon Apr 27 21:14:46 2026 +0000

patch 9.2.0407: tabpanel: A few issues with the tabpanel

Problem: Several issues around the tabpanel scrollbar:
1. :set tabpanelopt= completion did not offer "scroll" and
"scrollbar".
2. gt/gT and other tab switches did not update the scrollbar
thumb; the current tab could move outside the visible
panel range without the view following.
3. When tpl_scroll_offset was at its maximum, the thumb's
bottom did not reach the last screen row due to integer
truncation in thumb_top (e.g. 31 tabs on 24 rows + :tablast
left a one-row gap).
4. For align:right the scrollbar was drawn on the panel's
left edge (adjacent to the buffer area), which breaks the
common convention that a vertical scrollbar sits on the
right.
Solution: - Add "scroll" and "scrollbar" to the 'tabpanelopt' expansion
list. Cover the completion in test_options.vim and extend
util/gen_opt_test.vim with the new valid/invalid values;
drop the now-redundant acceptance test from
test_tabpanel.vim.
- In draw_tabpanel(), remember the last-drawn curtab and,
when it changes, adjust tpl_scroll_offset so curtab_row
falls inside [offset, offset + Rows). Mouse wheel and
drag leave curtab unchanged, so the user's chosen offset
is preserved.
- In draw_tabpanel_scrollbar(), compute thumb_top as
(Rows - thumb_height) * tpl_scroll_offset
/ (tpl_total_rows - Rows), mirroring the mapping already
used by tabpanel_drag_scrollbar(). This guarantees the
thumb's bottom reaches the last row at the maximum offset.
- In draw_tabpanel(), place the scrollbar at the tabpanel's
right edge for both align:left and align:right (previously
align:right put it on the panel's left edge next to the
vertical separator). For align:right this means the
scrollbar now sits at the screen's right edge.
- Update :h tabpanel-scroll to describe the new, align-
independent placement.
- Add Test_tabpanel_scrollbar_follows_curtab() and
Test_tabpanel_scrollbar_reaches_bottom() to exercise the
regressions fixed by items 2 and 3.

closes: #20052

Co-Authored-By: Claude Opus 4.7 (1M context) <nor...@anthropic.com>
Signed-off-by: Hirohito Higashi <h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index d2611db09..b3edc22e3 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -1,4 +1,4 @@
-*options.txt* For Vim version 9.2. Last change: 2026 Apr 21
+*options.txt* For Vim version 9.2. Last change: 2026 Apr 27


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -9062,25 +9062,17 @@ A jump table for the options with a short description can be found at |Q_op|.

columns:{n} Number of columns of the tabpanel.
If this value is 0 or less than 'columns', the
- tab panel will not be displayed.
+ tabpanel will not be displayed.
(default 20)

- scroll Enable mouse wheel scrolling over the tabpanel
- area when the tab list exceeds the visible
- screen height. The scroll step is controlled
- by 'mousescroll'. When disabled (the default),
- the tabpanel shows the page containing the
- current tab, with no way to view tabs outside
- that page.
-
- scrollbar Reserve a one-column scrollbar in the tabpanel
- showing the current scroll position. The
- scrollbar uses the |hl-PmenuSbar| and
- |hl-PmenuThumb| highlight groups for the track
- and thumb respectively. Clicking on the
- scrollbar column jumps the thumb to that
- position; the thumb can also be dragged.
- Implies "scroll".
+ scrollbar Reserve a one-column scrollbar at the right
+ edge of the tabpanel showing the current
+ scroll position. The scrollbar uses the
+ |hl-PmenuSbar| and |hl-PmenuThumb| highlight
+ groups for the track and thumb respectively.
+ Clicking on the scrollbar column jumps the
+ thumb to that position; the thumb can also be
+ dragged. See |tabpanel-scroll|.

vert Use a vertical separator for tabpanel.
The vertical separator character is taken from
diff --git a/runtime/doc/tabpage.txt b/runtime/doc/tabpage.txt
index 7fc09c545..88aeb4ad3 100644
--- a/runtime/doc/tabpage.txt
+++ b/runtime/doc/tabpage.txt
@@ -1,4 +1,4 @@
-*tabpage.txt* For Vim version 9.2. Last change: 2026 Apr 26
+*tabpage.txt* For Vim version 9.2. Last change: 2026 Apr 27


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -485,33 +485,33 @@ groups: |hl-TabPanel| |hl-TabPanelSel| |hl-TabPanelFill|
SCROLLING IN THE TABPANEL: *tabpanel-scroll*

When the total height of the tab page list exceeds the visible screen height,
-the tabpanel by default displays the "page" that contains the current tab page
-and offers no way to view tab pages outside that page.
+mouse wheel events over the tabpanel area scroll the tab page list up or
+down. The scroll step follows the 'mousescroll' setting. Wheel events
+inside the tabpanel area are consumed by the tabpanel and do not trigger
+|<ScrollWheelUp>| or |<ScrollWheelDown>| mappings.

-To make the tabpanel scrollable, add "scroll" to 'tabpanelopt': >
- :set tabpanelopt+=scroll
+The current tab page is always brought into view: when the selected tab
+page changes (by |gt|, |gT|, |:tabnext| etc.), the panel scrolls so the
+current entry is visible.

-With "scroll" enabled, mouse wheel events over the tabpanel area scroll the
-tab page list up or down. The scroll step follows the 'mousescroll' setting.
-Wheel events inside the tabpanel area are consumed by the tabpanel and do not
-trigger |<ScrollWheelUp>| or |<ScrollWheelDown>| mappings.
-
-To additionally show a vertical scrollbar indicating the current scroll
-position, use "scrollbar": >
+To show a vertical scrollbar indicating the current scroll position, add
+"scrollbar" to 'tabpanelopt': >
:set tabpanelopt+=scrollbar

-The "scrollbar" value implies "scroll". A one-column scrollbar is reserved at
-the edge of the tabpanel; clicking on the scrollbar column moves the thumb to
+A one-column scrollbar is always reserved at the right edge of the
+tabpanel, regardless of 'align'. For |'tabpanelopt'|=align:left this is
+the edge adjacent to the buffer windows; for align:right it is the right
+edge of the screen. Clicking on the scrollbar column moves the thumb to
the click position, and the thumb can be dragged to scroll continuously.

-When "vert" is combined with "scrollbar", the scrollbar is drawn next to the
-vertical separator, on the panel side.
+When "vert" is combined with "scrollbar", the vertical separator is drawn
+at the tabpanel's boundary with the buffer area and the scrollbar stays at
+the tabpanel's right edge.

The scrollbar uses the |hl-PmenuSbar| highlight group for the track and
|hl-PmenuThumb| for the thumb.

-The scroll offset is remembered across redraws but is reset when "scroll" or
-"scrollbar" is toggled off and back on.
+The scroll offset is remembered across redraws.

MOUSE CLICKS IN THE TABPANEL: *tabpanel-mouse*

diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index 43106ef30..725ba3326 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt* For Vim version 9.2. Last change: 2026 Apr 26
+*version9.txt* For Vim version 9.2. Last change: 2026 Apr 27


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52616,8 +52616,8 @@ Other ~
- Allow mouse clickable regions in the 'statusline', 'tabline' and the
'tabpanel' using the |stl-%[FuncName]| atom.
- Enable reflow support in the |:terminal|.
-- Added "scroll" and "scrollbar" sub-options to 'tabpanelopt' so the tabpanel
- can scroll when the tab page list exceeds the visible screen height.
+- Added "scrollbar" sub-option to 'tabpanelopt' so the tabpanel can scroll
+ when the tab page list exceeds the visible screen height.

Platform specific ~
-----------------
diff --git a/src/optionstr.c b/src/optionstr.c
index ab7c5ffff..72f5dd3ce 100644
--- a/src/optionstr.c
+++ b/src/optionstr.c
@@ -30,7 +30,7 @@ static char *(p_briopt_values[]) = {"shift:", "min:", "sbr", "list:", "column:",
#endif
#if defined(FEAT_TABPANEL)
// Note: Keep this in sync with tabpanelopt_changed()
-static char *(p_tplo_values[]) = {"align:", "columns:", "vert", NULL};
+static char *(p_tplo_values[]) = {"align:", "columns:", "scrollbar", "vert", NULL};
static char *(p_tplo_align_values[]) = {"left", "right", NULL};
#endif
#if defined(FEAT_DIFF)
diff --git a/src/proto/tabpanel.pro b/src/proto/tabpanel.pro
index 72726d89e..e6328b177 100644
--- a/src/proto/tabpanel.pro
+++ b/src/proto/tabpanel.pro
@@ -1,5 +1,6 @@
/* tabpanel.c */
int tabpanelopt_changed(void);
+void tabpanel_forget_tabpage(const tabpage_T *tp);
int tabpanel_width(void);
int tabpanel_leftcol(void);
void draw_tabpanel(void);
diff --git a/src/tabpanel.c b/src/tabpanel.c
index 07be48e44..34372fe60 100644
--- a/src/tabpanel.c
+++ b/src/tabpanel.c
@@ -43,11 +43,11 @@ static int opt_scope = OPT_LOCAL;
static int tpl_align = ALIGN_LEFT;
static int tpl_columns = 20;
static bool tpl_is_vert = false;
-static bool tpl_scroll = false;
static bool tpl_scrollbar = false;
static int tpl_scroll_offset = 0;
static int tpl_total_rows = 0;
-static int tpl_scrollbar_col = -1; // screen column of scrollbar, -1 if none
+static int tpl_scrollbar_col = -1; // screen column of scrollbar, -1 if none
+static tabpage_T *tpl_last_curtab = NULL; // last curtab seen by draw_tabpanel

typedef struct {
win_T *wp;
@@ -69,7 +69,6 @@ tabpanelopt_changed(void)
int new_align = ALIGN_LEFT;
long new_columns = 20;
bool new_is_vert = false;
- bool new_scroll = false;
bool new_scrollbar = false;

p = p_tplo;
@@ -107,12 +106,6 @@ tabpanelopt_changed(void)
{
p += 9;
new_scrollbar = true;
- new_scroll = true;
- }
- else if (STRNCMP(p, "scroll", 6) == 0)
- {
- p += 6;
- new_scroll = true;
}

if (*p != ',' && *p != NUL)
@@ -124,15 +117,26 @@ tabpanelopt_changed(void)
tpl_align = new_align;
tpl_columns = new_columns;
tpl_is_vert = new_is_vert;
- if (tpl_scroll != new_scroll)
- tpl_scroll_offset = 0;
- tpl_scroll = new_scroll;
tpl_scrollbar = new_scrollbar;

+ // Re-center the current tab on the next redraw.
+ tpl_last_curtab = NULL;
+
shell_new_columns();
return OK;
}

+/*
+ * Drop any internal reference to "tp", so draw_tabpanel() never compares
+ * against a dangling pointer after the tabpage has been freed.
+ */
+ void
+tabpanel_forget_tabpage(const tabpage_T *tp)
+{
+ if (tpl_last_curtab == tp)
+ tpl_last_curtab = NULL;
+}
+
/*
* Return the width of tabpanel.
*/
@@ -264,6 +268,31 @@ tabpanel_append_click_regions(
}
}

+/*
+ * Ensure the current tab is visible by adjusting tpl_scroll_offset when
+ * the selected tab has changed since the previous redraw. Mouse wheel or
+ * scrollbar drag operations leave curtab unchanged, so the user's chosen
+ * offset is preserved in those cases.
+ */
+ static void
+follow_curtab_if_needed(int curtab_row)
+{
+ if (Rows <= 0 || curtab == tpl_last_curtab)
+ return;
+
+ if (curtab_row < tpl_scroll_offset)
+ tpl_scroll_offset = curtab_row;
+ else if (curtab_row >= tpl_scroll_offset + Rows)
+ tpl_scroll_offset = curtab_row - Rows + 1;
+
+ int max_offset = tpl_total_rows > Rows ? tpl_total_rows - Rows : 0;
+
+ if (tpl_scroll_offset < 0)
+ tpl_scroll_offset = 0;
+ else if (tpl_scroll_offset > max_offset)
+ tpl_scroll_offset = max_offset;
+}
+
/*
* draw the tabpanel.
*/
@@ -293,30 +322,36 @@ draw_tabpanel(void)
int sb_len = tpl_scrollbar ? SCROLL_LEN : 0;
int sb_screen_col = -1;

+ // The scrollbar is always placed at the right edge of the tabpanel,
+ // regardless of 'align'. The vertical separator sits at the panel's
+ // boundary with the buffer area (left edge for align:right, right edge
+ // for align:left).
if (tpl_is_vert)
{
if (is_right)
{
- // draw main contents in tabpanel
- do_by_tplmode(TPLMODE_GET_CURTAB_ROW, VERT_LEN + sb_len,
- maxwidth - VERT_LEN, &curtab_row, NULL);
- do_by_tplmode(TPLMODE_REDRAW, VERT_LEN + sb_len, maxwidth,
+ // Panel on the right: vert at panel's left edge, scrollbar at
+ // panel's right edge (= screen's right edge).
+ do_by_tplmode(TPLMODE_GET_CURTAB_ROW, VERT_LEN,
+ maxwidth - sb_len, &curtab_row, NULL);
+ follow_curtab_if_needed(curtab_row);
+ do_by_tplmode(TPLMODE_REDRAW, VERT_LEN, maxwidth - sb_len,
&curtab_row, NULL);
- // draw vert separator in tabpanel
for (vsrow = 0; vsrow < Rows; vsrow++)
screen_putchar(curwin->w_fill_chars.tpl_vert, vsrow,
topframe->fr_width, vs_attr);
if (tpl_scrollbar)
- sb_screen_col = topframe->fr_width + VERT_LEN;
+ sb_screen_col = topframe->fr_width + maxwidth - SCROLL_LEN;
}
else
{
- // draw main contents in tabpanel
+ // Panel on the left: scrollbar just left of vert, vert at
+ // panel's right edge (boundary with buffer).
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0,
maxwidth - VERT_LEN - sb_len, &curtab_row, NULL);
+ follow_curtab_if_needed(curtab_row);
do_by_tplmode(TPLMODE_REDRAW, 0, maxwidth - VERT_LEN - sb_len,
&curtab_row, NULL);
- // draw vert separator in tabpanel
for (vsrow = 0; vsrow < Rows; vsrow++)
screen_putchar(curwin->w_fill_chars.tpl_vert, vsrow,
maxwidth - VERT_LEN, vs_attr);
@@ -328,16 +363,20 @@ draw_tabpanel(void)
{
if (is_right)
{
- do_by_tplmode(TPLMODE_GET_CURTAB_ROW, sb_len, maxwidth,
+ // Panel on the right, no vert: scrollbar at screen's right edge.
+ do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0, maxwidth - sb_len,
+ &curtab_row, NULL);
+ follow_curtab_if_needed(curtab_row);
+ do_by_tplmode(TPLMODE_REDRAW, 0, maxwidth - sb_len,
&curtab_row, NULL);
- do_by_tplmode(TPLMODE_REDRAW, sb_len, maxwidth, &curtab_row, NULL);
if (tpl_scrollbar)
- sb_screen_col = topframe->fr_width;
+ sb_screen_col = topframe->fr_width + maxwidth - SCROLL_LEN;
}
else
{
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0, maxwidth - sb_len,
&curtab_row, NULL);
+ follow_curtab_if_needed(curtab_row);
do_by_tplmode(TPLMODE_REDRAW, 0, maxwidth - sb_len,
&curtab_row, NULL);
if (tpl_scrollbar)
@@ -354,6 +393,7 @@ draw_tabpanel(void)
// A user function may reset KeyTyped, restore it.
KeyTyped = saved_KeyTyped;

+ tpl_last_curtab = curtab;
redraw_tabpanel = FALSE;
}

@@ -606,13 +646,7 @@ do_by_tplmode(
args.col_end = col_end;

if (tplmode != TPLMODE_GET_CURTAB_ROW && args.maxrow > 0)
- {
- if (tpl_scroll)
- args.offsetrow = tpl_scroll_offset;
- else
- while (args.offsetrow + args.maxrow <= *pcurtab_row)
- args.offsetrow += args.maxrow;
- }
+ args.offsetrow = tpl_scroll_offset;

tp = first_tabpage;

@@ -632,16 +666,9 @@ do_by_tplmode(
{
args.attr = attr_tpls;
if (tplmode == TPLMODE_GET_CURTAB_ROW)
- {
+ // Capture the row of the current tab and keep iterating so
+ // tpl_total_rows receives the true content height below.
*pcurtab_row = row;
- // When scroll mode is active keep iterating so tpl_total_rows
- // receives the true content height; otherwise bail out early.
- if (!tpl_scroll)
- {
- do_unlet((char_u *)"g:actual_curtabpage", TRUE);
- break;
- }
- }
}
else
args.attr = attr_tpl;
@@ -742,7 +769,7 @@ do_by_tplmode(
// Capture the true content height during the GET_CURTAB_ROW pass, which
// ignores maxrow and therefore walks every tab. REDRAW stops at the
// visible edge so its "row" is clamped and unusable here.
- if (tplmode == TPLMODE_GET_CURTAB_ROW && tpl_scroll)
+ if (tplmode == TPLMODE_GET_CURTAB_ROW)
tpl_total_rows = row;
}

@@ -761,10 +788,21 @@ draw_tabpanel_scrollbar(int screen_col)

if (tpl_total_rows > Rows && Rows > 0)
{
+ int max_offset = tpl_total_rows - Rows;
+ int track_range;
+
thumb_height = Rows * Rows / tpl_total_rows;
if (thumb_height < 1)
thumb_height = 1;
- thumb_top = Rows * tpl_scroll_offset / tpl_total_rows;
+
+ // Map tpl_scroll_offset onto the track: at offset 0 the thumb's top
+ // is at row 0, at the maximum offset its bottom reaches the last
+ // row. This is the exact inverse of tabpanel_drag_scrollbar().
+ track_range = Rows - thumb_height;
+ if (track_range > 0 && max_offset > 0)
+ thumb_top = track_range * tpl_scroll_offset / max_offset;
+ else
+ thumb_top = 0;
if (thumb_top + thumb_height > Rows)
thumb_top = Rows - thumb_height;
if (thumb_top < 0)
@@ -848,7 +886,6 @@ tabpanel_drag_scrollbar(int screen_row)
/*
* Scroll the tabpanel by 'count' rows in direction 'dir' (1 = down, -1 = up).
* Returns true if the offset changed and a redraw was scheduled.
- * Has no effect unless 'tabpanelopt' contains "scroll".
*/
bool
tabpanel_scroll(int dir, int count)
@@ -856,7 +893,7 @@ tabpanel_scroll(int dir, int count)
int max_offset;
int new_offset;

- if (!tpl_scroll || tabpanel_width() == 0)
+ if (tabpanel_width() == 0)
return false;

max_offset = tpl_total_rows - Rows;
diff --git a/src/testdir/dumps/Test_tabpanel_many_tabpages_4.dump b/src/testdir/dumps/Test_tabpanel_many_tabpages_4.dump
index aa1ae3e18..3f462354c 100644
--- a/src/testdir/dumps/Test_tabpanel_many_tabpages_4.dump
+++ b/src/testdir/dumps/Test_tabpanel_many_tabpages_4.dump
@@ -1,10 +1,10 @@
-|1+2&#ffffff0@1|:|t|a|b| @3> +0&&@34
+|5+8#0000001#e0e0e08|:|t|a|b| @4> +0#0000000#ffffff0@34
+|6+8#0000001#e0e0e08|:|t|a|b| @4|~+0#4040ff13#ffffff0| @33
+|7+8#0000001#e0e0e08|:|t|a|b| @4|~+0#4040ff13#ffffff0| @33
+|8+8#0000001#e0e0e08|:|t|a|b| @4|~+0#4040ff13#ffffff0| @33
+|9+8#0000001#e0e0e08|:|t|a|b| @4|~+0#4040ff13#ffffff0| @33
+|1+8#0000001#e0e0e08|0|:|t|a|b| @3|~+0#4040ff13#ffffff0| @33
+|1+2#0000000&@1|:|t|a|b| @3|~+0#4040ff13&| @33
|1+8#0000001#e0e0e08|2|:|t|a|b| @3|~+0#4040ff13#ffffff0| @33
|1+8#0000001#e0e0e08|3|:|t|a|b| @3|~+0#4040ff13#ffffff0| @33
-|1+8#0000001#e0e0e08|4|:|t|a|b| @3|~+0#4040ff13#ffffff0| @33
-|1+8#0000001#e0e0e08|5|:|t|a|b| @3|~+0#4040ff13#ffffff0| @33
-|1+8#0000001#e0e0e08|6|:|t|a|b| @3|~+0#4040ff13#ffffff0| @33
-|1+8#0000001#e0e0e08|7|:|t|a|b| @3|~+0#4040ff13#ffffff0| @33
-|1+8#0000001#e0e0e08|8|:|t|a|b| @3|~+0#4040ff13#ffffff0| @33
-|1+8#0000001#e0e0e08|9|:|t|a|b| @3|~+0#4040ff13#ffffff0| @33
-|2+8#0000001#e0e0e08|0|:|t|a|b| @3|:+0#0000000#ffffff0|t|a|b|n|e|x|t| |-|3| @6|0|,|0|-|1| @7|A|l@1|
+|1+8#0000001#e0e0e08|4|:|t|a|b| @3|:+0#0000000#ffffff0|t|a|b|n|e|x|t| |-|3| @6|0|,|0|-|1| @7|A|l@1|
diff --git a/src/testdir/test_options.vim b/src/testdir/test_options.vim
index b1be22446..91610e128 100644
--- a/src/testdir/test_options.vim
+++ b/src/testdir/test_options.vim
@@ -593,6 +593,12 @@ func Test_set_completion_string_values()
if exists('+tabclose')
call assert_equal('left uselast', join(sort(getcompletion('set tabclose=', 'cmdline'))), ' ')
endif
+ if has('tabpanel')
+ call assert_equal(['align:', 'columns:', 'scrollbar', 'vert'],
+ \ getcompletion('set tabpanelopt=', 'cmdline'))
+ call assert_equal(['left', 'right'],
+ \ getcompletion('set tabpanelopt=align:', 'cmdline'))
+ endif
if exists('+termwintype')
call assert_equal('conpty', getcompletion('set termwintype=', 'cmdline')[1])
endif
diff --git a/src/testdir/test_tabpanel.vim b/src/testdir/test_tabpanel.vim
index c510d078f..f7e6bdb08 100644
--- a/src/testdir/test_tabpanel.vim
+++ b/src/testdir/test_tabpanel.vim
@@ -963,49 +963,26 @@ func Test_tabpanel_large_columns()
call assert_fails(':set tabpanelopt=columns:-1', 'E474:')
endfunc

-func Test_tabpanel_scrollopt_accepted()
- " 'scroll' / 'scrollbar' must be accepted in 'tabpanelopt'.
- set tabpanelopt=scroll
- call assert_equal('scroll', &tabpanelopt)
- set tabpanelopt=scrollbar
- call assert_equal('scrollbar', &tabpanelopt)
-
- " Combination with other values.
- set tabpanelopt=align:right,scroll
- call assert_equal('align:right,scroll', &tabpanelopt)
- set tabpanelopt=columns:15,vert,scrollbar
- call assert_equal('columns:15,vert,scrollbar', &tabpanelopt)
- set tabpanelopt=align:right,columns:12,vert,scrollbar
- call assert_equal('align:right,columns:12,vert,scrollbar', &tabpanelopt)
-
- " Unknown values must still fail.
- call assert_fails(':set tabpanelopt=scrol', 'E474:')
- call assert_fails(':set tabpanelopt=scrollbarx', 'E474:')
-
- call s:reset()
-endfunc
-
func Test_tabpanel_scroll_many_tabs()
let save_lines = &lines
let save_showtabpanel = &showtabpanel
let save_tabpanelopt = &tabpanelopt

- " Make the screen short so the tab list exceeds the visible height.
+ " Make the screen short so the tab page list exceeds the visible height.
set lines=8
set showtabpanel=2
- set tabpanelopt=scroll
+ set tabpanelopt=
for i in range(20)
tabnew
endfor

- " Should not crash with many tabs and scroll enabled.
+ " Should not crash with many tabs (scroll behaviour is always on).
redraw!

- " Switching to scrollbar resets the offset but must also not crash.
+ " Toggling scrollbar must also not crash.
set tabpanelopt=scrollbar
redraw!

- " Disabling scroll returns to normal behavior.
set tabpanelopt=
redraw!

@@ -1024,6 +1001,94 @@ func Test_tabpanel_scroll_many_tabs()
let &lines = save_lines
endfunc

+" The scrollbar thumb must follow the current tab when it is moved by
+" gt/gT/:tabnext/:tablast, so that the selected tab is always visible.
+func Test_tabpanel_scrollbar_follows_curtab()
+ let save_lines = &lines
+ let save_columns = &columns
+ let save_showtabpanel = &showtabpanel
+ let save_tabpanelopt = &tabpanelopt
+
+ set lines=10 columns=40
+ set showtabpanel=2 tabpanelopt=scrollbar,columns:8
+ for i in range(49)
+ tabnew
+ endfor
+ let sb_col = 8
+
+ " With curtab at the top of the list, row 1 shows the thumb and the
+ " last row shows the track. Record the two attrs for comparison.
+ tabfirst
+ redraw
+ let attr_thumb = screenattr(1, sb_col)
+ let attr_track = screenattr(&lines, sb_col)
+ call assert_notequal(attr_thumb, attr_track)
+
+ " Jump to a tab far outside the visible range: thumb must leave the top.
+ 30tabnext
+ redraw
+ call assert_equal(attr_track, screenattr(1, sb_col))
+
+ " Back to the first tab: thumb returns to the top.
+ tabfirst
+ redraw
+ call assert_equal(attr_thumb, screenattr(1, sb_col))
+ call assert_equal(attr_track, screenattr(&lines, sb_col))
+
+ " gT from the first tab wraps to the last: thumb moves to the bottom.
+ normal! gT
+ redraw
+ call assert_equal(attr_track, screenattr(1, sb_col))
+ call assert_equal(attr_thumb, screenattr(&lines, sb_col))
+
+ " gt from the last tab wraps to the first: thumb returns to the top.
+ normal! gt
+ redraw
+ call assert_equal(attr_thumb, screenattr(1, sb_col))
+ call assert_equal(attr_track, screenattr(&lines, sb_col))
+
+ %bwipeout!
+ let &tabpanelopt = save_tabpanelopt
+ let &showtabpanel = save_showtabpanel
+ let &lines = save_lines
+ let &columns = save_columns
+endfunc
+
+" With 31 tabs on 24 rows, :tablast must place the scrollbar thumb's
+" bottom at the last screen row. Before the fix, integer truncation in
+" thumb_top left a one-row gap at the bottom.
+func Test_tabpanel_scrollbar_reaches_bottom()
+ let save_lines = &lines
+ let save_columns = &columns
+ let save_showtabpanel = &showtabpanel
+ let save_tabpanelopt = &tabpanelopt
+
+ set lines=24 columns=40
+ set showtabpanel=2 tabpanelopt=scrollbar,columns:8
+ for i in range(30)
+ tabnew
+ endfor
+ let sb_col = 8
+
+ " Identify the thumb attr while the thumb is at the top.
+ tabfirst
+ redraw
+ let attr_thumb = screenattr(1, sb_col)
+ let attr_track = screenattr(&lines, sb_col)
+ call assert_notequal(attr_thumb, attr_track)
+
+ " :tablast must push the thumb all the way to the bottom.
+ tablast
+ redraw
+ call assert_equal(attr_thumb, screenattr(&lines, sb_col))
+
+ %bwipeout!
+ let &tabpanelopt = save_tabpanelopt
+ let &showtabpanel = save_showtabpanel
+ let &lines = save_lines
+ let &columns = save_columns
+endfunc
+
func Test_tabpanel_variable_height()

let save_lines = &lines
diff --git a/src/testdir/util/gen_opt_test.vim b/src/testdir/util/gen_opt_test.vim
index bc54d272d..02f7fdf34 100644
--- a/src/testdir/util/gen_opt_test.vim
+++ b/src/testdir/util/gen_opt_test.vim
@@ -330,9 +330,11 @@ let test_values = {
\ 'tabline': [['', 'xxx'], ['%$', '%{', '%{%', '%{%}', '%(', '%)']],
\ 'tabpanel': [['', 'aaa', 'bbb'], []],
\ 'tabpanelopt': [['', 'align:left', 'align:right', 'vert', 'columns:0',
- \ 'columns:20', 'columns:999'],
+ \ 'columns:20', 'columns:999', 'scrollbar',
+ \ 'columns:15,vert,scrollbar',
+ \ 'align:right,columns:12,vert,scrollbar'],
\ ['xxx', 'align:', 'align:middle', 'colomns:', 'cols:10',
- \ 'cols:-1']],
+ \ 'cols:-1', 'scroll', 'scrol', 'scrollbarx']],
\ 'tagcase': [['followic', 'followscs', 'ignore', 'match', 'smart'],
\ ['', 'xxx', 'smart,match']],
\ 'termencoding': [has('gui_gtk') ? [] : ['', 'utf-8'], ['xxx']],
diff --git a/src/version.c b/src/version.c
index 0d4d8eac2..b6cbb51aa 100644
--- a/src/version.c
+++ b/src/version.c
@@ -729,6 +729,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 407,
/**/
406,
/**/
diff --git a/src/window.c b/src/window.c
index 478ec03ae..c3acd7af3 100644
--- a/src/window.c
+++ b/src/window.c
@@ -4784,6 +4784,9 @@ free_tabpage(tabpage_T *tp)

if (tp == lastused_tabpage)
lastused_tabpage = NULL;
+#ifdef FEAT_TABPANEL
+ tabpanel_forget_tabpage(tp);
+#endif

vim_free(tp->tp_localdir);
vim_free(tp->tp_prevdir);
Reply all
Reply to author
Forward
0 new messages