-*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
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
-*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
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*
-*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
- 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 ~
-----------------
#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)
/* 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);
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;
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;
{
p += 9;
new_scrollbar = true;
- new_scroll = true;
- }
- else if (STRNCMP(p, "scroll", 6) == 0)
- {
- p += 6;
- new_scroll = true;
}
if (*p != ',' && *p != NUL)
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.
*/
}
}
+/*
+ * 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.
*/
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);
{
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)
// A user function may reset KeyTyped, restore it.
KeyTyped = saved_KeyTyped;
+ tpl_last_curtab = curtab;
redraw_tabpanel = FALSE;
}
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;
{
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;
// 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;
}
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)
/*
* 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)
int max_offset;
int new_offset;
- if (!tpl_scroll || tabpanel_width() == 0)
+ if (tabpanel_width() == 0)
return false;
max_offset = tpl_total_rows - Rows;
-|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|
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
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!
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
\ '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']],
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 407,
/**/
406,
/**/
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);