]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0407: tabpanel: A few issues with the tabpanel v9.2.0407
authorHirohito Higashi <h.east.727@gmail.com>
Mon, 27 Apr 2026 21:14:46 +0000 (21:14 +0000)
committerChristian Brabandt <cb@256bit.org>
Mon, 27 Apr 2026 21:16:56 +0000 (21:16 +0000)
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) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
12 files changed:
runtime/doc/options.txt
runtime/doc/tabpage.txt
runtime/doc/version9.txt
src/optionstr.c
src/proto/tabpanel.pro
src/tabpanel.c
src/testdir/dumps/Test_tabpanel_many_tabpages_4.dump
src/testdir/test_options.vim
src/testdir/test_tabpanel.vim
src/testdir/util/gen_opt_test.vim
src/version.c
src/window.c

index d2611db0959ec5fc0e1124544da7af6b416e9caf..b3edc22e35a10d4a356dc06d0e117771bd03940a 100644 (file)
@@ -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
index 7fc09c54518ccf6ccaadda89f664fc69b69fcd6b..88aeb4ad34b1bb69a273eb9adb4a6e011a4bd679 100644 (file)
@@ -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*
 
index 43106ef30a66dae1c861f42a79ad6b0b953368b1..725ba3326d1e4be82ce9ad0cea1641418dad2b65 100644 (file)
@@ -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 ~
 -----------------
index ab7c5ffff16ddc6335ecf3acf0d5abc4a10a9657..72f5dd3ced848ed7e09453ebd9bd9e2898bf7695 100644 (file)
@@ -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)
index 72726d89ea4fbd9c09ac093b797512b40cb19c5d..e6328b17743f0465d90453f5293081b1b35122bc 100644 (file)
@@ -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);
index 07be48e44c7c02d071fb5536a0d54386b29e0bf4..34372fe60c4115ce696da98ad9bdc2ebb2a87cbe 100644 (file)
@@ -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;
index aa1ae3e18ba027978633aa516cc82472d02c3e37..3f462354c42e450144d5c794979bdb4511b37343 100644 (file)
@@ -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| 
index b1be22446773fef71a7c2c00c1fb3ba7f2b3eedb..91610e12892367bf72090554b731132270fb0fc9 100644 (file)
@@ -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
index c510d078fdbed2d342d7982c09bafdea2ac5f50b..f7e6bdb082e9cac4541f051d12dbc6678dab4249 100644 (file)
@@ -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
index bc54d272d8db4ab1b857d095c9bade547da84589..02f7fdf34ee9a570fd5f3b2d11a3aad17a9443e6 100644 (file)
@@ -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']],
index 0d4d8eac2f6e88e596ae8b0f8c3fa867f78adce6..b6cbb51aa8b8e1dfc8d655a15a7b738c87e4bdc9 100644 (file)
@@ -729,6 +729,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    407,
 /**/
     406,
 /**/
index 478ec03ae1c0adf49e184319d5db8806a7174051..c3acd7af3359983f0ee39fc5175c9b7721bc4273 100644 (file)
@@ -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);