]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0338: Cannot handle mouseclicks in the tabline v9.2.0338
authorYasuhiro Matsumoto <mattn.jp@gmail.com>
Sat, 11 Apr 2026 15:22:24 +0000 (15:22 +0000)
committerChristian Brabandt <cb@256bit.org>
Sat, 11 Apr 2026 15:26:14 +0000 (15:26 +0000)
Problem:  Cannot handle mouseclicks in the tabline
Solution: Support %[FuncName] click regions in 'tabline', add "area" key
          to the click info dict (Yasuhiro Matsumoto).

The previous implementation resolved and stored click regions only for
per-window statuslines; the tabline path in win_redr_custom() (wp==NULL)
parsed %[FuncName] but discarded the regions, and tabline clicks were
dispatched via TabPageIdxs[] which didn't know about them.

Add a global tabline_stl_click array populated from the tabline path,
refactor stl_click_handler() to take the regions directly, and dispatch
matching clicks from do_mouse() before falling through to tab selection.
The winid entry in the callback dict is 0 for tabline clicks.

related: #19841
closes:  #19950

Supported by AI.

Signed-off-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
runtime/doc/options.txt
src/globals.h
src/mouse.c
src/screen.c
src/testdir/test_statusline.vim
src/version.c

index 300eebbc221d5da18e72e528cbbea731e89de0ab..f083d6ff10d00581d7dfb891a2a6dac8eb613250 100644 (file)
@@ -1,4 +1,4 @@
-*options.txt*  For Vim version 9.2.  Last change: 2026 Apr 09
+*options.txt*  For Vim version 9.2.  Last change: 2026 Apr 11
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -8626,7 +8626,8 @@ A jump table for the options with a short description can be found at |Q_op|.
 
                                                *stl-%[FuncName]*
        %[ defines clickable regions in the statusline.  When the user clicks
-       on a region with the mouse, the specified function is called.
+       on a region with the mouse, the specified function is called.  The
+       same syntax can also be used in 'tabline'.
 
          %[FuncName]   Start of a clickable region.  "FuncName" is the name
                        of a Vim function to call when the region is clicked.
@@ -8644,11 +8645,17 @@ A jump table for the options with a short description can be found at |Q_op|.
          "button"      Mouse button: "l" (left), "m" (middle), "r" (right).
          "mods"        Modifier keys: combination of "s" (shift), "c" (ctrl),
                        "a" (alt).  Empty string if no modifiers.
-         "winid"       |window-ID| of the window whose statusline was clicked.
+         "winid"       |window-ID| of the window whose statusline was clicked,
+                       or 0 when the click was in 'tabline'.
+         "area"        "statusline" or "tabline".  Indicates which option the
+                       clicked region belongs to.  Useful when a single
+                       callback is shared between 'statusline' and 'tabline'.
 
        If the function returns non-zero, the statusline is redrawn.
        Dragging the statusline to resize the window still works even when
-       click handlers are defined.
+       click handlers are defined.  When used in 'tabline', clicks in
+       %[FuncName] regions are dispatched to the callback instead of the
+       default tab-selection behavior.
 
        Example: >
            func! ClickFile(info)
index b7533c63224447ad7703c242a5ccf61be749abb6..7d873eea4ec696e66cad3b768dad427f5f37cf08 100644 (file)
@@ -104,6 +104,10 @@ EXTERN int redrawing_for_callback INIT(= 0);
  */
 EXTERN short   *TabPageIdxs INIT(= NULL);
 
+// Click regions for 'tabline' (%[FuncName]).
+EXTERN stl_click_region_T *tabline_stl_click INIT(= NULL);
+EXTERN int     tabline_stl_click_count INIT(= 0);
+
 #ifdef FEAT_PROP_POPUP
 // Array with size Rows x Columns containing zindex of popups.
 EXTERN short   *popup_mask INIT(= NULL);
index 6bff3d7c284ea6ca3ec6b137f8e668a50571fdb5..1077c5ee7281259ec65a18e73b4b9bfce040451b 100644 (file)
@@ -21,6 +21,9 @@ static long mouse_hor_step = 6;
 static long mouse_vert_step = 3;
 static win_T *dragwin = NULL;  // window being dragged
 static int stl_click_handler(win_T *wp, int mcol, int which_button, int mods);
+static int stl_click_handler_regions(stl_click_region_T *regions,
+                                   int region_count, int winid, int mcol,
+                                   int which_button, int mods);
 
     void
 mouse_set_vert_scroll_step(long step)
@@ -502,6 +505,16 @@ do_mouse(
     // Check for clicking in the tab page line.
     if (TabPageIdxs != NULL && mouse_row == 0 && firstwin->w_winrow > 0)
     {
+       // Dispatch 'tabline' %[FuncName] click regions before falling through
+       // to tab-page selection.  On drag events fall through to the normal
+       // tab-drag handling.
+       if (is_click && !is_drag
+               && stl_click_handler_regions(tabline_stl_click,
+                                           tabline_stl_click_count,
+                                           0, mouse_col, which_button,
+                                           mod_mask))
+           return FALSE;
+
        tp_label.just_in = true;
        tp_label.nr = TabPageIdxs[mouse_col];
 
@@ -1640,11 +1653,19 @@ mouse_model_popup(void)
 }
 
 /*
- * Call a statusline click handler function.
+ * Call a click-region callback function.
+ * "regions"/"region_count" describe the resolved click regions,
+ * "winid" is stored as the "winid" key in the info dict (0 for tabline).
  * Returns TRUE if the function was called and handled the click.
  */
     static int
-stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
+stl_click_handler_regions(
+       stl_click_region_T  *regions,
+       int                 region_count,
+       int                 winid,
+       int                 mcol,
+       int                 which_button,
+       int                 mods)
 {
 #ifdef FEAT_EVAL
     int                n;
@@ -1658,17 +1679,16 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
     funcexe_T  funcexe;
     int                col = mcol;
 
-    if (wp == NULL || wp->w_stl_click == NULL || wp->w_stl_click_count == 0)
+    if (regions == NULL || region_count == 0)
        return FALSE;
 
     // Find the click region at the given column.
-    for (n = 0; n < wp->w_stl_click_count; n++)
+    for (n = 0; n < region_count; n++)
     {
-       if (col >= wp->w_stl_click[n].col_start
-                                         && col < wp->w_stl_click[n].col_end)
+       if (col >= regions[n].col_start && col < regions[n].col_end)
            break;
     }
-    if (n >= wp->w_stl_click_count || wp->w_stl_click[n].funcname == NULL)
+    if (n >= region_count || regions[n].funcname == NULL)
        return FALSE;
 
     // Build the info dictionary.
@@ -1676,7 +1696,7 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
     if (info == NULL)
        return FALSE;
 
-    dict_add_number(info, "minwid", wp->w_stl_click[n].minwid);
+    dict_add_number(info, "minwid", regions[n].minwid);
 
     // Determine number of clicks.
     // MOD_MASK_2CLICK=0x20, MOD_MASK_3CLICK=0x40, MOD_MASK_4CLICK=0x60
@@ -1705,7 +1725,13 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
     mods_str[mi] = NUL;
     dict_add_string(info, "mods", mods_str);
 
-    dict_add_number(info, "winid", wp->w_id);
+    dict_add_number(info, "winid", winid);
+
+    // "area": which option the clicked region belongs to.  Lets a shared
+    // dispatcher distinguish 'statusline' from 'tabline' (and future areas)
+    // without having to overload winid == 0.
+    dict_add_string(info, "area",
+           winid == 0 ? (char_u *)"tabline" : (char_u *)"statusline");
 
     // Call the function with the info dict as argument.
     argvars[0].v_type = VAR_DICT;
@@ -1718,7 +1744,7 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
 
     CLEAR_FIELD(funcexe);
     funcexe.fe_evaluate = TRUE;
-    (void)call_func(wp->w_stl_click[n].funcname, -1,
+    (void)call_func(regions[n].funcname, -1,
                                            &rettv, 1, argvars, &funcexe);
 
     n = (int)rettv.vval.v_number;
@@ -1726,11 +1752,20 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
     dict_unref(info);
 
     if (n != 0)
+    {
+       // Make sure the tabline gets redrawn too when the callback asks for
+       // a redraw (redraw_statuslines() only redraws the tabline when
+       // redraw_tabline is set).
+       if (winid == 0)
+           redraw_tabline = TRUE;
        redraw_statuslines();
+    }
 
     return TRUE;
 #else
-    (void)wp;
+    (void)regions;
+    (void)region_count;
+    (void)winid;
     (void)mcol;
     (void)which_button;
     (void)mods;
@@ -1738,6 +1773,19 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
 #endif
 }
 
+/*
+ * Call a statusline click handler function for window "wp".
+ * Returns TRUE if the function was called and handled the click.
+ */
+    static int
+stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
+{
+    if (wp == NULL)
+       return FALSE;
+    return stl_click_handler_regions(wp->w_stl_click, wp->w_stl_click_count,
+                                       wp->w_id, mcol, which_button, mods);
+}
+
 // dragwin is declared near the top of the file
 
 /*
index f667e6eb24209d14c6d27e5ed17f756543c201e4..ed28ac91abed50e548947b042beb517de4d3636e 100644 (file)
@@ -1486,23 +1486,41 @@ win_redr_custom(
            TabPageIdxs[col++] = fillchar;
     }
 
-    // Resolve click function regions for statusline.
-    if (wp != NULL && !draw_ruler)
+    // Resolve click function regions for statusline or tabline.
+    if (!draw_ruler)
     {
-       int     click_count = 0;
+       stl_click_region_T  **out_regions;
+       int                 *out_count;
+       int                 base_col;
+       int                 click_count = 0;
+
+       if (wp != NULL)
+       {
+           out_regions = &wp->w_stl_click;
+           out_count = &wp->w_stl_click_count;
+           base_col = wp->w_wincol;
+       }
+       else
+       {
+           // 'tabline': store regions in global state since there is no
+           // associated window.
+           out_regions = &tabline_stl_click;
+           out_count = &tabline_stl_click_count;
+           base_col = firstwin->w_wincol;
+       }
 
        // Count the click regions.
        for (n = 0; clicktab[n].start != NULL; n++)
            click_count++;
 
        // Free old click regions.
-       if (wp->w_stl_click != NULL)
+       if (*out_regions != NULL)
        {
-           for (n = 0; n < wp->w_stl_click_count; n++)
-               vim_free(wp->w_stl_click[n].funcname);
-           VIM_CLEAR(wp->w_stl_click);
+           for (n = 0; n < *out_count; n++)
+               vim_free((*out_regions)[n].funcname);
+           VIM_CLEAR(*out_regions);
        }
-       wp->w_stl_click_count = 0;
+       *out_count = 0;
 
        if (click_count > 0)
        {
@@ -1514,7 +1532,7 @@ win_redr_custom(
            {
                char_u  *cur_funcname = NULL;
                int     cur_minwid = 0;
-               int     region_start = wp->w_wincol;
+               int     region_start = base_col;
 
                // Walk through click records converting buffer positions
                // to screen columns.
@@ -1530,7 +1548,7 @@ win_redr_custom(
                    if (cur_funcname != NULL)
                    {
                        regions[rcount].col_start = region_start;
-                       regions[rcount].col_end = wp->w_wincol + len;
+                       regions[rcount].col_end = base_col + len;
                        regions[rcount].funcname =
                                            vim_strsave(cur_funcname);
                        regions[rcount].minwid = cur_minwid;
@@ -1539,22 +1557,22 @@ win_redr_custom(
 
                    cur_funcname = clicktab[n].funcname;
                    cur_minwid = clicktab[n].minwid;
-                   region_start = wp->w_wincol + len;
+                   region_start = base_col + len;
                }
 
                // Close final region if it extends to the end.
                if (cur_funcname != NULL)
                {
                    regions[rcount].col_start = region_start;
-                   regions[rcount].col_end = wp->w_wincol + maxwidth;
+                   regions[rcount].col_end = base_col + maxwidth;
                    regions[rcount].funcname =
                                        vim_strsave(cur_funcname);
                    regions[rcount].minwid = cur_minwid;
                    rcount++;
                }
 
-               wp->w_stl_click = regions;
-               wp->w_stl_click_count = rcount;
+               *out_regions = regions;
+               *out_count = rcount;
            }
        }
 
index 673a6ed1257a632f48583050772177b69420b67f..27fef946af5030d88f4d4832486bda456b68bb14 100644 (file)
@@ -752,6 +752,7 @@ func Test_statusline_click_handler()
   call assert_equal(1, g:stl_click_info.nclicks)
   call assert_equal(0, g:stl_click_info.minwid)
   call assert_equal(win_getid(), g:stl_click_info.winid)
+  call assert_equal('statusline', g:stl_click_info.area)
   unlet! g:stl_click_info
 
   " Click outside click region (on the filename part)
@@ -873,4 +874,60 @@ func Test_statusline_click_linebreak_still_works()
   let &laststatus = save_ls
 endfunc
 
+func Test_tabline_click_handler()
+  let save_mouse = &mouse
+  let save_tal = &tabline
+  let save_stal = &showtabline
+  if has('gui')
+    let save_go = &guioptions
+    set guioptions-=e
+  endif
+  set mouse=a
+  set showtabline=2
+
+  " Two adjacent click regions in 'tabline' with different minwid.
+  set tabline=%1[StlClickTestFunc][AAA]%[]%2[StlClickTestFunc][BBB]%[]
+  redraw!
+
+  " Click on [AAA] region (tabline is row 1).
+  call test_setmouse(1, 2)
+  call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
+  call assert_true(exists('g:stl_click_info'))
+  call assert_equal('l', g:stl_click_info.button)
+  call assert_equal(1, g:stl_click_info.nclicks)
+  call assert_equal(1, g:stl_click_info.minwid)
+  " winid is 0 for tabline clicks (no associated window).
+  call assert_equal(0, g:stl_click_info.winid)
+  call assert_equal('tabline', g:stl_click_info.area)
+  unlet! g:stl_click_info
+
+  " Click on [BBB] region.
+  call test_setmouse(1, 7)
+  call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
+  call assert_true(exists('g:stl_click_info'))
+  call assert_equal(2, g:stl_click_info.minwid)
+  unlet! g:stl_click_info
+
+  " Middle click on [AAA].
+  call test_setmouse(1, 2)
+  call feedkeys("\<MiddleMouse>\<MiddleRelease>", 'xt')
+  call assert_true(exists('g:stl_click_info'))
+  call assert_equal('m', g:stl_click_info.button)
+  unlet! g:stl_click_info
+
+  " Click outside any %[...] region: no callback, no error.
+  set tabline=xxx%1[StlClickTestFunc][YYY]%[]
+  redraw!
+  call test_setmouse(1, 1)
+  call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
+  call assert_false(exists('g:stl_click_info'))
+
+  let &mouse = save_mouse
+  let &tabline = save_tal
+  let &showtabline = save_stal
+  if has('gui')
+    let &guioptions = save_go
+  endif
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
index f4e5ef7ec273b10c1ca84245a4379bbc3355ce70..15f52077eb73aacccce162f2d89a3b2bf3ac6cfc 100644 (file)
@@ -734,6 +734,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    338,
 /**/
     337,
 /**/