patch 9.2.0328: Cannot handle mouseclicks in the statusline
Commit:
https://github.com/vim/vim/commit/d42b047f78e743bcfab8e76c00c357c0471d3308
Author: Yasuhiro Matsumoto <
matt...@gmail.com>
Date: Thu Apr 9 21:15:30 2026 +0000
patch 9.2.0328: Cannot handle mouseclicks in the statusline
Problem: Cannot handle mouseclicks in the statusline
Solution: Add the %[FuncName] statusline item to define clickable
regions with a callback function. (Yasuhiro Matsumoto)
closes: #19841
Signed-off-by: Yasuhiro Matsumoto <
matt...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index 03f77c519..300eebbc2 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 07
+*options.txt* For Vim version 9.2. Last change: 2026 Apr 09
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -8624,6 +8624,51 @@ A jump table for the options with a short description can be found at |Q_op|.
@ - Inserts a newline. This only takes effect when the "maxheight"
value of 'statuslineopt' is greater than 1, or for |tabpanel|.
+ *stl-%[FuncName]*
+ %[ defines clickable regions in the statusline. When the user clicks
+ on a region with the mouse, the specified function is called.
+
+ %[FuncName] Start of a clickable region. "FuncName" is the name
+ of a Vim function to call when the region is clicked.
+ %[] End of the clickable region. If omitted, the region
+ extends to the end of the statusline or to the start
+ of the next clickable region.
+
+ A {minwid} value can be used to pass an identifier to the callback:
+ %3[FuncName] Starts a clickable region with minwid 3.
+
+ The function receives a single |Dictionary| argument with these
+ entries:
+ "minwid" The minwid value from %N[Func] (0 if not specified).
+ "nclicks" Number of clicks: 1, 2, or 3.
+ "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.
+
+ 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.
+
+ Example: >
+ func! ClickFile(info)
+ if a:info.button ==# 'l' && a:info.nclicks == 2
+ browse edit
+ endif
+ return 0
+ endfunc
+ set statusline=%[ClickFile]%f%[]\ %l:%c
+< This makes the filename in the statusline clickable. Double-clicking
+ it opens the file browser.
+
+ Use `has('statusline_click')` to check if this feature is available.
+ This is useful for backward compatibility: >
+ if has('statusline_click')
+ set statusline=%[ClickFile]%f%[]\ %l:%c
+ else
+ set statusline=%f\ %l:%c
+ endif
+<
When displaying a flag, Vim removes the leading comma, if any, when
that flag comes right after plaintext. This will make a nice display
when flags are used like in the examples below.
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 3da46d2fd..6505d1b88 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -1524,6 +1524,7 @@ $quote eval.txt /*$quote*
+spell various.txt /*+spell*
+startuptime various.txt /*+startuptime*
+statusline various.txt /*+statusline*
++statusline_click various.txt /*+statusline_click*
+sun_workshop various.txt /*+sun_workshop*
+syntax various.txt /*+syntax*
+system() various.txt /*+system()*
@@ -10547,6 +10548,7 @@ status-line windows.txt /*status-line*
statusmsg-variable eval.txt /*statusmsg-variable*
stl-%! options.txt /*stl-%!*
stl-%@ options.txt /*stl-%@*
+stl-%[FuncName] options.txt /*stl-%[FuncName]*
stl-%{ options.txt /*stl-%{*
str2blob() builtin.txt /*str2blob()*
str2float() builtin.txt /*str2float()*
diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt
index 518f6caab..9de661452 100644
--- a/runtime/doc/various.txt
+++ b/runtime/doc/various.txt
@@ -1,4 +1,4 @@
-*various.txt* For Vim version 9.2. Last change: 2026 Feb 14
+*various.txt* For Vim version 9.2. Last change: 2026 Apr 06
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -496,6 +496,7 @@ N *+spell* spell checking support, see |spell|
N *+startuptime* |--startuptime| argument
N *+statusline* Options 'statusline', 'rulerformat' and special
formats of 'titlestring' and 'iconstring'
+N *+statusline_click* Click handlers in 'statusline' |stl-%[FuncName]|
- *+sun_workshop* Removed: |workshop|
N *+syntax* Syntax highlighting |syntax|
*+system()* Unix only: opposite of |+fork|
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index acce37226..e0976453d 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 07
+*version9.txt* For Vim version 9.2. Last change: 2026 Apr 09
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52613,6 +52613,8 @@ Other ~
pairs individually (e.g. 'listchars', 'fillchars', 'diffopt').
- |system()| and |systemlist()| functions accept a list as first argument,
bypassing the shell completely.
+- Allow mouse clickable regions in the |status-line| using the
+ |stl-%[FuncName]| atom.
Platform specific ~
-----------------
diff --git a/src/buffer.c b/src/buffer.c
index 6c99acd0a..cbee49e1a 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -49,7 +49,8 @@ static int value_changed(char_u *str, char_u **last);
static int build_stl_str_hl_local(stl_mode_T mode, win_T *wp,
char_u *out, size_t outlen, char_u **fmt_arg,
char_u *opt_name, int opt_scope, int fillchar, int maxwidth,
- stl_hlrec_T **hltab, stl_hlrec_T **tabtab, int *lbreaks);
+ stl_hlrec_T **hltab, stl_hlrec_T **tabtab,
+ stl_clickrec_T **clicktab, int *lbreaks);
#endif
static int append_arg_number(win_T *wp, char_u *buf, size_t buflen, int add_file);
static void free_buffer(buf_T *);
@@ -4080,7 +4081,7 @@ maketitle(void)
if (stl_syntax & STL_IN_TITLE)
build_stl_str_hl(curwin, title_str, sizeof(buf), p_titlestring,
(char_u *)"titlestring", 0,
- 0, maxlen, NULL, NULL);
+ 0, maxlen, NULL, NULL, NULL);
else
#endif
title_str = p_titlestring;
@@ -4251,7 +4252,8 @@ maketitle(void)
#ifdef FEAT_STL_OPT
if (stl_syntax & STL_IN_ICON)
build_stl_str_hl(curwin, icon_str, sizeof(buf), p_iconstring,
- (char_u *)"iconstring", 0, 0, 0, NULL, NULL);
+ (char_u *)"iconstring", 0, 0, 0, NULL, NULL,
+ NULL);
else
#endif
icon_str = p_iconstring;
@@ -4347,8 +4349,10 @@ typedef struct
Separate,
Highlight,
TabPage,
+ ClickFunc,
Trunc
} stl_type;
+ char_u *stl_clickfunc; // function name for ClickFunc items
} stl_item_T;
static size_t stl_items_len = 20; // Initial value, grows as needed.
@@ -4356,6 +4360,7 @@ static stl_item_T *stl_items = NULL;
static int *stl_groupitem = NULL;
static stl_hlrec_T *stl_hltab = NULL;
static stl_hlrec_T *stl_tabtab = NULL;
+static stl_clickrec_T *stl_clicktab = NULL;
static int *stl_separator_locations = NULL;
/*
@@ -4383,10 +4388,12 @@ build_stl_str_hl(
int fillchar,
int maxwidth,
stl_hlrec_T **hltab, // return: HL attributes (can be NULL)
- stl_hlrec_T **tabtab) // return: tab page nrs (can be NULL)
+ stl_hlrec_T **tabtab, // return: tab page nrs (can be NULL)
+ stl_clickrec_T **clicktab) // return: click func regions (can be NULL)
{
return build_stl_str_hl_local(STL_MODE_SINGLE, wp, out, outlen, &fmt,
- opt_name, opt_scope, fillchar, maxwidth, hltab, tabtab, NULL);
+ opt_name, opt_scope, fillchar, maxwidth, hltab, tabtab, clicktab,
+ NULL);
}
int
@@ -4400,10 +4407,12 @@ build_stl_str_hl_mline(
int fillchar,
int maxwidth,
stl_hlrec_T **hltab, // return: HL attributes (can be NULL)
- stl_hlrec_T **tabtab) // return: tab page nrs (can be NULL)
+ stl_hlrec_T **tabtab, // return: tab page nrs (can be NULL)
+ stl_clickrec_T **clicktab) // return: click func regions (can be NULL)
{
return build_stl_str_hl_local(STL_MODE_MULTI, wp, out, outlen, fmt,
- opt_name, opt_scope, fillchar, maxwidth, hltab, tabtab, NULL);
+ opt_name, opt_scope, fillchar, maxwidth, hltab, tabtab, clicktab,
+ NULL);
}
# ifdef ENABLE_STL_MODE_MULTI_NL
@@ -4418,10 +4427,12 @@ build_stl_str_hl_mline_nl(
int fillchar,
int maxwidth,
stl_hlrec_T **hltab, // return: HL attributes (can be NULL)
- stl_hlrec_T **tabtab) // return: tab page nrs (can be NULL)
+ stl_hlrec_T **tabtab, // return: tab page nrs (can be NULL)
+ stl_clickrec_T **clicktab) // return: click func regions (can be NULL)
{
return build_stl_str_hl_local(STL_MODE_MULTI_NL, wp, out, outlen, fmt,
- opt_name, opt_scope, fillchar, maxwidth, hltab, tabtab, NULL);
+ opt_name, opt_scope, fillchar, maxwidth, hltab, tabtab, clicktab,
+ NULL);
}
# endif
@@ -4442,7 +4453,7 @@ get_stl_rendered_height(
++emsg_off;
(void)build_stl_str_hl_local(STL_MODE_GET_RENDERED_HEIGHT,
wp, buf, sizeof(buf), &fmt,
- opt_name, opt_scope, 0, 0, NULL, NULL, &rendered_height);
+ opt_name, opt_scope, 0, 0, NULL, NULL, NULL, &rendered_height);
--emsg_off;
return rendered_height;
}
@@ -4477,6 +4488,7 @@ build_stl_str_hl_local(
int maxwidth,
stl_hlrec_T **hltab, // return: HL attributes (can be NULL)
stl_hlrec_T **tabtab, // return: tab page nrs (can be NULL)
+ stl_clickrec_T **clicktab, // return: click func regions (can be NULL)
int *rendered_height) // return: stl rendered height (can be NULL)
{
linenr_T lnum;
@@ -4538,6 +4550,7 @@ build_stl_str_hl_local(
// end of the list.
stl_hltab = ALLOC_MULT(stl_hlrec_T, stl_items_len + 1);
stl_tabtab = ALLOC_MULT(stl_hlrec_T, stl_items_len + 1);
+ stl_clicktab = ALLOC_MULT(stl_clickrec_T, stl_items_len + 1);
stl_separator_locations = ALLOC_MULT(int, stl_items_len);
}
@@ -4632,6 +4645,12 @@ build_stl_str_hl_local(
break;
stl_tabtab = new_hlrec;
+ stl_clickrec_T *new_clickrec = vim_realloc(stl_clicktab,
+ sizeof(stl_clickrec_T) * (new_len + 1));
+ if (new_clickrec == NULL)
+ break;
+ stl_clicktab = new_clickrec;
+
int *new_separator_locs = vim_realloc(stl_separator_locations,
sizeof(int) * new_len);
if (new_separator_locs == NULL)
@@ -4641,6 +4660,8 @@ build_stl_str_hl_local(
stl_items_len = new_len;
}
+ stl_items[curitem].stl_clickfunc = NULL;
+
if (*s != '%')
prevchar_isflag = prevchar_isitem = FALSE;
@@ -4675,8 +4696,39 @@ build_stl_str_hl_local(
if (*s == NUL) // ignore trailing %
break;
+ if (*s == STL_CLICKFUNC)
+ {
+ // %[] - end click region
+ if (s[1] == ']')
+ {
+ stl_items[curitem].stl_type = ClickFunc;
+ stl_items[curitem].stl_start = p;
+ stl_items[curitem].stl_minwid = 0;
+ stl_items[curitem].stl_clickfunc = NULL;
+ s += 2;
+ curitem++;
+ continue;
+ }
+ // %[FuncName] - start click region
+ if (ASCII_ISALPHA(s[1]) || s[1] == '_')
+ {
+ char_u *rb = vim_strchr(s + 1, ']');
+ if (rb != NULL)
+ {
+ stl_items[curitem].stl_type = ClickFunc;
+ stl_items[curitem].stl_start = p;
+ stl_items[curitem].stl_minwid = 0;
+ stl_items[curitem].stl_clickfunc =
+ vim_strnsave(s + 1, rb - s - 1);
+ s = rb + 1;
+ curitem++;
+ continue;
+ }
+ }
+ }
if (*s == STL_LINEBREAK)
{
+ // Plain %@ - line break
if (mode == STL_MODE_MULTI
# ifdef ENABLE_STL_MODE_MULTI_NL
|| mode == STL_MODE_MULTI_NL
@@ -5233,6 +5285,40 @@ build_stl_str_hl_local(
++s;
continue;
}
+
+ case STL_CLICKFUNC:
+ // %N[] - end click region (with minwid, minwid is ignored)
+ if (*s == ']')
+ {
+ stl_items[curitem].stl_type = ClickFunc;
+ stl_items[curitem].stl_start = p;
+ stl_items[curitem].stl_minwid = 0;
+ stl_items[curitem].stl_clickfunc = NULL;
+ s++;
+ curitem++;
+ continue;
+ }
+ // %N[FuncName] with minwid
+ if (ASCII_ISALPHA(*s) || *s == '_')
+ {
+ char_u *rb = vim_strchr(s, ']');
+ if (rb != NULL)
+ {
+ stl_items[curitem].stl_type = ClickFunc;
+ stl_items[curitem].stl_start = p;
+ stl_items[curitem].stl_minwid = minwid;
+ stl_items[curitem].stl_clickfunc =
+ vim_strnsave(s, rb - s);
+ s = rb + 1;
+ curitem++;
+ continue;
+ }
+ }
+ continue;
+
+ case STL_LINEBREAK:
+ // %N@ - line break (already handled above, fallback)
+ continue;
}
stl_items[curitem].stl_start = p;
@@ -5388,6 +5474,10 @@ find_linebreak:
if (mode == STL_MODE_GET_RENDERED_HEIGHT)
{
+ // Free click function names that were allocated during parsing.
+ for (l = 0; l < itemcnt; l++)
+ if (stl_items[l].stl_type == ClickFunc)
+ vim_free(stl_items[l].stl_clickfunc);
if (rendered_height != NULL)
*rendered_height = rheight;
return 0;
@@ -5561,6 +5651,34 @@ find_linebreak:
sp->userhl = 0;
}
+ // Store the info about click function regions.
+ if (clicktab != NULL)
+ {
+ stl_clickrec_T *cp;
+
+ *clicktab = stl_clicktab;
+ cp = stl_clicktab;
+ for (l = 0; l < itemcnt; l++)
+ {
+ if (stl_items[l].stl_type == ClickFunc)
+ {
+ cp->start = stl_items[l].stl_start;
+ cp->funcname = stl_items[l].stl_clickfunc;
+ cp->minwid = stl_items[l].stl_minwid;
+ cp++;
+ }
+ }
+ cp->start = NULL;
+ cp->funcname = NULL;
+ }
+ else
+ {
+ // Free click function names when caller doesn't need them.
+ for (l = 0; l < itemcnt; l++)
+ if (stl_items[l].stl_type == ClickFunc)
+ vim_free(stl_items[l].stl_clickfunc);
+ }
+
redraw_not_allowed = save_redraw_not_allowed;
// A user function may reset KeyTyped, restore it.
diff --git a/src/evalfunc.c b/src/evalfunc.c
index b4ea12c3c..f61135f77 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -7509,6 +7509,13 @@ f_has(typval_T *argvars, typval_T *rettv)
1
#else
0
+#endif
+ },
+ {"statusline_click",
+#ifdef FEAT_STL_OPT
+ 1
+#else
+ 0
#endif
},
{"netbeans_intg",
diff --git a/src/gui.c b/src/gui.c
index 4238b8b73..1d60cd7f3 100644
--- a/src/gui.c
+++ b/src/gui.c
@@ -3841,7 +3841,7 @@ get_tabline_label(
// Can't use NameBuff directly, build_stl_str_hl() uses it.
build_stl_str_hl(curwin, res, MAXPATHL, *opt, opt_name, 0,
- 0, (int)Columns, NULL, NULL);
+ 0, (int)Columns, NULL, NULL, NULL);
STRCPY(NameBuff, res);
// Back to the original curtab.
diff --git a/src/hardcopy.c b/src/hardcopy.c
index 55d438418..e87caf302 100644
--- a/src/hardcopy.c
+++ b/src/hardcopy.c
@@ -489,7 +489,8 @@ prt_header(
printer_page_num = pagenum;
build_stl_str_hl(curwin, tbuf, (size_t)(width + IOSIZE), p_header,
- (char_u *)"printheader", 0, ' ', width, NULL, NULL);
+ (char_u *)"printheader", 0, ' ', width, NULL, NULL,
+ NULL);
// Reset line numbers
curwin->w_cursor.lnum = tmp_lnum;
diff --git a/src/mouse.c b/src/mouse.c
index 3f2f86dc8..6bff3d7c2 100644
--- a/src/mouse.c
+++ b/src/mouse.c
@@ -19,6 +19,8 @@
*/
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);
void
mouse_set_vert_scroll_step(long step)
@@ -760,6 +762,22 @@ do_mouse(
in_status_line = (jump_flags & IN_STATUS_LINE);
in_sep_line = (jump_flags & IN_SEP_LINE);
+ // Check for statusline click handler early, before visual mode or
+ // other button-specific handling can interfere.
+ if (in_status_line && is_click && !is_drag
+ && stl_click_handler(dragwin, mouse_col,
+ which_button, mod_mask))
+ {
+#ifdef FEAT_MOUSESHAPE
+ if (!drag_status_line)
+ {
+ drag_status_line = TRUE;
+ update_mouseshape(-1);
+ }
+#endif
+ return FALSE;
+ }
+
#ifdef FEAT_NETBEANS_INTG
if (isNetbeansBuffer(curbuf)
&& !(jump_flags & (IN_STATUS_LINE | IN_SEP_LINE)))
@@ -1621,7 +1639,106 @@ mouse_model_popup(void)
return (p_mousem[0] == 'p');
}
-static win_T *dragwin = NULL; // window being dragged
+/*
+ * Call a statusline click handler function.
+ * 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)
+{
+#ifdef FEAT_EVAL
+ int n;
+ int nclicks;
+ char_u button_str[2];
+ char_u mods_str[4];
+ int mi = 0;
+ dict_T *info;
+ typval_T argvars[2];
+ typval_T rettv;
+ funcexe_T funcexe;
+ int col = mcol;
+
+ if (wp == NULL || wp->w_stl_click == NULL || wp->w_stl_click_count == 0)
+ return FALSE;
+
+ // Find the click region at the given column.
+ for (n = 0; n < wp->w_stl_click_count; n++)
+ {
+ if (col >= wp->w_stl_click[n].col_start
+ && col < wp->w_stl_click[n].col_end)
+ break;
+ }
+ if (n >= wp->w_stl_click_count || wp->w_stl_click[n].funcname == NULL)
+ return FALSE;
+
+ // Build the info dictionary.
+ info = dict_alloc();
+ if (info == NULL)
+ return FALSE;
+
+ dict_add_number(info, "minwid", wp->w_stl_click[n].minwid);
+
+ // Determine number of clicks.
+ // MOD_MASK_2CLICK=0x20, MOD_MASK_3CLICK=0x40, MOD_MASK_4CLICK=0x60
+ nclicks = ((mods & MOD_MASK_MULTI_CLICK) >> 5) + 1;
+ if (nclicks > 3)
+ nclicks = 3;
+ dict_add_number(info, "nclicks", nclicks);
+
+ // Button.
+ if (which_button == MOUSE_LEFT)
+ button_str[0] = 'l';
+ else if (which_button == MOUSE_RIGHT)
+ button_str[0] = 'r';
+ else
+ button_str[0] = 'm';
+ button_str[1] = NUL;
+ dict_add_string(info, "button", button_str);
+
+ // Modifiers.
+ if (mods & MOD_MASK_SHIFT)
+ mods_str[mi++] = 's';
+ if (mods & MOD_MASK_CTRL)
+ mods_str[mi++] = 'c';
+ if (mods & MOD_MASK_ALT)
+ mods_str[mi++] = 'a';
+ mods_str[mi] = NUL;
+ dict_add_string(info, "mods", mods_str);
+
+ dict_add_number(info, "winid", wp->w_id);
+
+ // Call the function with the info dict as argument.
+ argvars[0].v_type = VAR_DICT;
+ argvars[0].vval.v_dict = info;
+ ++info->dv_refcount;
+ argvars[1].v_type = VAR_UNKNOWN;
+
+ rettv.v_type = VAR_NUMBER;
+ rettv.vval.v_number = 0;
+
+ CLEAR_FIELD(funcexe);
+ funcexe.fe_evaluate = TRUE;
+ (void)call_func(wp->w_stl_click[n].funcname, -1,
+ &rettv, 1, argvars, &funcexe);
+
+ n = (int)rettv.vval.v_number;
+ clear_tv(&rettv);
+ dict_unref(info);
+
+ if (n != 0)
+ redraw_statuslines();
+
+ return TRUE;
+#else
+ (void)wp;
+ (void)mcol;
+ (void)which_button;
+ (void)mods;
+ return FALSE;
+#endif
+}
+
+// dragwin is declared near the top of the file
/*
* Reset the window being dragged. To be called when switching tab page.
diff --git a/src/option.h b/src/option.h
index adafbd8d3..7165ea80c 100644
--- a/src/option.h
+++ b/src/option.h
@@ -364,9 +364,10 @@ typedef enum {
#define STL_USER_HL '*' // highlight from (User)1..9 or 0
#define STL_HIGHLIGHT '#' // highlight name
#define STL_LINEBREAK '@' // insert a line break
+#define STL_CLICKFUNC '[' // click handler region
#define STL_TABPAGENR 'T' // tab page label nr
#define STL_TABCLOSENR 'X' // tab page close nr
-#define STL_ALL ((char_u *) "fFtcvVlLknoObBrRhHmYyWwMqpPaNS{#@")
+#define STL_ALL ((char_u *) "fFtcvVlLknoObBrRhHmYyWwMqpPaNS{#@[")
// flags used for parsed 'wildmode'
#define WIM_FULL 0x01
diff --git a/src/optionstr.c b/src/optionstr.c
index 9647f7c01..e82c2b78c 100644
--- a/src/optionstr.c
+++ b/src/optionstr.c
@@ -671,8 +671,30 @@ check_stl_option(char_u *s)
if (!*s)
break;
s++;
+ if (*s == STL_CLICKFUNC)
+ {
+ if (s[1] == ']')
+ {
+ // %[] - end click region
+ s += 2;
+ continue;
+ }
+ if (ASCII_ISALPHA(s[1]) || s[1] == '_')
+ {
+ // %[FuncName] - start click region
+ char_u *rb = vim_strchr(s + 2, ']');
+ if (rb != NULL)
+ {
+ s = rb + 1;
+ continue;
+ }
+ }
+ // Bare %[ is invalid
+ return illegal_char(errbuf, errbuflen, *s);
+ }
if (*s == STL_LINEBREAK)
{
+ // Plain %@ - line break
s++;
continue;
}
@@ -694,6 +716,32 @@ check_stl_option(char_u *s)
s++;
if (*s == STL_USER_HL)
continue;
+ if (*s == STL_CLICKFUNC)
+ {
+ // %N[FuncName] or %N[]
+ if (s[1] == ']')
+ {
+ s += 2;
+ continue;
+ }
+ if (ASCII_ISALPHA(s[1]) || s[1] == '_')
+ {
+ char_u *rb = vim_strchr(s + 2, ']');
+ if (rb != NULL)
+ {
+ s = rb + 1;
+ continue;
+ }
+ }
+ // Bare %N[ is invalid
+ return illegal_char(errbuf, errbuflen, *s);
+ }
+ if (*s == STL_LINEBREAK)
+ {
+ // %N@ - line break
+ s++;
+ continue;
+ }
if (*s == '.')
{
s++;
diff --git a/src/proto/
buffer.pro b/src/proto/
buffer.pro
index 438ddb1a8..13c273d6a 100644
--- a/src/proto/
buffer.pro
+++ b/src/proto/
buffer.pro
@@ -49,9 +49,9 @@ int col_print(char_u *buf, size_t buflen, int col, int vcol);
void maketitle(void);
void resettitle(void);
void free_titles(void);
-int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab);
-int build_stl_str_hl_mline(win_T *wp, char_u *out, size_t outlen, char_u **fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab);
-int build_stl_str_hl_mline_nl(win_T *wp, char_u *out, size_t outlen, char_u **fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab);
+int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab, stl_clickrec_T **clicktab);
+int build_stl_str_hl_mline(win_T *wp, char_u *out, size_t outlen, char_u **fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab, stl_clickrec_T **clicktab);
+int build_stl_str_hl_mline_nl(win_T *wp, char_u *out, size_t outlen, char_u **fmt, char_u *opt_name, int opt_scope, int fillchar, int maxwidth, stl_hlrec_T **hltab, stl_hlrec_T **tabtab, stl_clickrec_T **clicktab);
int get_stl_rendered_height(win_T *wp, char_u *fmt, char_u *opt_name, int opt_scope);
int get_rel_pos(win_T *wp, char_u *buf, int buflen);
char_u *fix_fname(char_u *fname);
diff --git a/src/screen.c b/src/screen.c
index 04c1f237a..311735d82 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -1296,6 +1296,7 @@ win_redr_custom(
int opt_scope = 0;
stl_hlrec_T *hltab;
stl_hlrec_T *tabtab;
+ stl_clickrec_T *clicktab;
win_T *ewp;
int p_crb_save;
bool override_success = false;
@@ -1396,7 +1397,8 @@ win_redr_custom(
width = build_stl_str_hl_mline(ewp, buf, sizeof(buf),
&stl_tmp,
opt_name, opt_scope,
- fillchar, maxwidth, &hltab, &tabtab);
+ fillchar, maxwidth, &hltab, &tabtab,
+ &clicktab);
// Make all characters printable.
p = transstr(buf);
@@ -1475,6 +1477,83 @@ win_redr_custom(
TabPageIdxs[col++] = fillchar;
}
+ // Resolve click function regions for statusline.
+ if (wp != NULL && !draw_ruler)
+ {
+ int click_count = 0;
+
+ // Count the click regions.
+ for (n = 0; clicktab[n].start != NULL; n++)
+ click_count++;
+
+ // Free old click regions.
+ if (wp->w_stl_click != 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);
+ }
+ wp->w_stl_click_count = 0;
+
+ if (click_count > 0)
+ {
+ stl_click_region_T *regions;
+ int rcount = 0;
+
+ regions = ALLOC_MULT(stl_click_region_T, click_count);
+ if (regions != NULL)
+ {
+ char_u *cur_funcname = NULL;
+ int cur_minwid = 0;
+ int region_start = wp->w_wincol;
+
+ // Walk through click records converting buffer positions
+ // to screen columns.
+ len = 0;
+ p = buf;
+ for (n = 0; clicktab[n].start != NULL; n++)
+ {
+ len += vim_strnsize(p,
+ (int)(clicktab[n].start - p));
+ p = clicktab[n].start;
+
+ // Close previous region if there was one.
+ if (cur_funcname != NULL)
+ {
+ regions[rcount].col_start = region_start;
+ regions[rcount].col_end = wp->w_wincol + len;
+ regions[rcount].funcname =
+ vim_strsave(cur_funcname);
+ regions[rcount].minwid = cur_minwid;
+ rcount++;
+ }
+
+ cur_funcname = clicktab[n].funcname;
+ cur_minwid = clicktab[n].minwid;
+ region_start = wp->w_wincol + 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].funcname =
+ vim_strsave(cur_funcname);
+ regions[rcount].minwid = cur_minwid;
+ rcount++;
+ }
+
+ wp->w_stl_click = regions;
+ wp->w_stl_click_count = rcount;
+ }
+ }
+
+ // Free the funcname strings allocated by build_stl_str_hl_local().
+ for (n = 0; clicktab[n].start != NULL; n++)
+ vim_free(clicktab[n].funcname);
+ }
+
theend:
if (override_success)
pop_highlight_overrides();
diff --git a/src/structs.h b/src/structs.h
index 5bb51dd54..955b9f100 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -1436,6 +1436,25 @@ typedef struct
int userhl; // 0: no HL, 1-9: User HL, < 0 for syn ID
} stl_hlrec_T;
+/*
+ * Used for statusline click function regions.
+ */
+typedef struct {
+ char_u *start; // position in output buffer where region starts
+ char_u *funcname; // function name (NULL = end/close marker)
+ int minwid; // minwid value from %N@Func@
+} stl_clickrec_T;
+
+/*
+ * Per-window resolved click regions (screen column based).
+ */
+typedef struct {
+ int col_start; // screen column where region starts
+ int col_end; // screen column where region ends
+ char_u *funcname; // function name (allocated copy)
+ int minwid; // minwid value
+} stl_click_region_T;
+
/*
* Syntax items - usually buffer-specific.
@@ -4117,6 +4136,8 @@ struct window_S
int w_prev_height; // previous height used for 'splitkeep'
int w_stl_rendered_height; // rendered height of window-local 'stl'
// (number of "%@" + 1)
+ stl_click_region_T *w_stl_click; // statusline click regions
+ int w_stl_click_count; // number of click regions
int w_status_height; // number of status lines.
// If 'statuslineopt' was changed, this
// member holds the previous value until
diff --git a/src/tabpanel.c b/src/tabpanel.c
index 2a6dbe831..51929a8b1 100644
--- a/src/tabpanel.c
+++ b/src/tabpanel.c
@@ -507,7 +507,8 @@ do_by_tplmode(
#endif
(args.cwp, buf, sizeof(buf),
&usefmt, opt_name, opt_scope, TPL_FILLCHAR,
- args.col_end - args.col_start, &hltab, &tabtab);
+ args.col_end - args.col_start, &hltab, &tabtab,
+ NULL);
args.prow = &row;
args.pcol = &col;
diff --git a/src/testdir/test_statusline.vim b/src/testdir/test_statusline.vim
index 174c1a475..673a6ed12 100644
--- a/src/testdir/test_statusline.vim
+++ b/src/testdir/test_statusline.vim
@@ -721,4 +721,156 @@ func Test_statusline_singlebyte_negative()
let [&columns, &ls, &stl, &enc] = [_columns, _ls, _stl, _enc]
endfunc
+func g:StlClickTestFunc(info)
+ let g:stl_click_info = a:info
+ return 0
+endfunc
+
+func g:StlClickReturn1(info)
+ let g:stl_click_info = a:info
+ return 1
+endfunc
+
+func Test_statusline_click_handler()
+ let save_mouse = &mouse
+ let save_stl = &statusline
+ let save_ls = &laststatus
+ set mouse=a
+ set laststatus=2
+
+ " Basic click handler
+ set statusline=%[StlClickTestFunc][Click]%[]\ %f
+ redraw!
+
+ " Click on the [Click] region
+ let stl_row = win_screenpos(0)[0] + winheight(0)
+ call test_setmouse(stl_row, 2)
+ call feedkeys("\<LeftMouse>", 'xt')
+ call feedkeys("\<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(0, g:stl_click_info.minwid)
+ call assert_equal(win_getid(), g:stl_click_info.winid)
+ unlet! g:stl_click_info
+
+ " Click outside click region (on the filename part)
+ call test_setmouse(stl_row, 20)
+ call feedkeys("\<LeftMouse>", 'xt')
+ call feedkeys("\<LeftRelease>", 'xt')
+ call assert_false(exists('g:stl_click_info'))
+
+ " Test with minwid
+ set statusline=%42[StlClickTestFunc][Click]%[]\ %f
+ redraw!
+ call test_setmouse(stl_row, 2)
+ call feedkeys("\<LeftMouse>", 'xt')
+ call feedkeys("\<LeftRelease>", 'xt')
+ call assert_true(exists('g:stl_click_info'))
+ call assert_equal(42, g:stl_click_info.minwid)
+ unlet! g:stl_click_info
+
+ " Test middle click
+ call test_setmouse(stl_row, 2)
+ call feedkeys("\<MiddleMouse>", 'xt')
+ call feedkeys("\<MiddleRelease>", 'xt')
+ call assert_true(exists('g:stl_click_info'))
+ call assert_equal('m', g:stl_click_info.button)
+ unlet! g:stl_click_info
+ let &mouse = save_mouse
+ let &statusline = save_stl
+ let &laststatus = save_ls
+endfunc
+
+func Test_statusline_click_multiple_regions()
+ let save_mouse = &mouse
+ let save_stl = &statusline
+ let save_ls = &laststatus
+ set mouse=a
+ set laststatus=2
+
+ " Two adjacent click regions with different minwid
+ set statusline=%1[StlClickTestFunc][AAA]%[]%2[StlClickTestFunc][BBB]%[]
+ redraw!
+
+ let stl_row = win_screenpos(0)[0] + winheight(0)
+
+ " Click on [AAA] region (col 2)
+ call test_setmouse(stl_row, 2)
+ call feedkeys("\<LeftMouse>", 'xt')
+ call feedkeys("\<LeftRelease>", 'xt')
+ call assert_true(exists('g:stl_click_info'))
+ call assert_equal(1, g:stl_click_info.minwid)
+ unlet! g:stl_click_info
+
+ " Click on [BBB] region (col 7)
+ call test_setmouse(stl_row, 7)
+ call feedkeys("\<LeftMouse>", 'xt')
+ call feedkeys("\<LeftRelease>", 'xt')
+ call assert_true(exists('g:stl_click_info'))
+ call assert_equal(2, g:stl_click_info.minwid)
+ unlet! g:stl_click_info
+
+ let &mouse = save_mouse
+ let &statusline = save_stl
+ let &laststatus = save_ls
+endfunc
+
+func Test_statusline_click_region_extends_to_end()
+ let save_mouse = &mouse
+ let save_stl = &statusline
+ let save_ls = &laststatus
+ set mouse=a
+ set laststatus=2
+
+ " Click region without %[] extends to end of statusline
+ set statusline=xxx%[StlClickTestFunc]Clickable
+ redraw!
+
+ let stl_row = win_screenpos(0)[0] + winheight(0)
+
+ " Click near the end of the statusline
+ call test_setmouse(stl_row, 15)
+ call feedkeys("\<LeftMouse>", 'xt')
+ call feedkeys("\<LeftRelease>", 'xt')
+ call assert_true(exists('g:stl_click_info'))
+ unlet! g:stl_click_info
+
+ " Click on "xxx" (before the click region)
+ call test_setmouse(stl_row, 1)
+ call feedkeys("\<LeftMouse>", 'xt')
+ call feedkeys("\<LeftRelease>", 'xt')
+ call assert_false(exists('g:stl_click_info'))
+
+ let &mouse = save_mouse
+ let &statusline = save_stl
+ let &laststatus = save_ls
+endfunc
+
+func Test_statusline_click_option_validation()
+ " Valid formats should not produce errors
+ let save_stl = &statusline
+ set statusline=%[Func]text%[]
+ set statusline=%3[Func]text%[]
+ set statusline=%[Func]text
+ set statusline=%[Func_Name]text%[]
+ " %@ alone is still valid (line break)
+ set statusline=%@
+ let &statusline = save_stl
+endfunc
+
+func Test_statusline_click_linebreak_still_works()
+ " Ensure %@ without FuncName still works as line break
+ let save_stl = &statusline
+ let save_ls = &laststatus
+ set laststatus=2
+
+ " This should not error - %@ is line break
+ set statusline=line1%@line2
+ redraw!
+
+ let &statusline = save_stl
+ let &laststatus = save_ls
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index 483f55d9f..14387f650 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 328,
/**/
327,
/**/
diff --git a/src/window.c b/src/window.c
index 38bc4677e..b42845ecf 100644
--- a/src/window.c
+++ b/src/window.c
@@ -6045,6 +6045,14 @@ win_free(
remove_highlight_overrides(wp->w_hl);
vim_free(wp->w_hl);
+ // Free statusline click regions.
+ if (wp->w_stl_click != NULL)
+ {
+ for (i = 0; i < wp->w_stl_click_count; i++)
+ vim_free(wp->w_stl_click[i].funcname);
+ vim_free(wp->w_stl_click);
+ }
+
clear_winopt(&wp->w_onebuf_opt);
clear_winopt(&wp->w_allbuf_opt);