From f9d2115a359c7846056bb2eb31f9eae818c6d510 Mon Sep 17 00:00:00 2001 From: bfredl Date: Fri, 29 Aug 2025 12:22:25 +0200 Subject: [PATCH 1/2] perf(highlight): allow decoration providers to skip ranges without data Continuing the work of #31400 That PR allowed the provider to be invoked multiple times per line. We want only to do that when there actually is more data later on the line. Additionally, we want to skip over lines which contain no new highlight items. The TS query cursor already tells us what the next position with more data is, so there is no need to reinvoke the range callback before that. NB: this removes the double buffering introduced in #32619 which is funtamentally incompatible with this (nvim core is supposed to keep track of long ranges by itself, without requiring a callback reinvoke blitz). Need to adjust the priorities some other way to fix the same issue. --- runtime/doc/api.txt | 6 ++ runtime/lua/vim/_meta/api.lua | 7 ++ runtime/lua/vim/treesitter/highlighter.lua | 86 ++++--------------- src/nvim/api/extmark.c | 7 ++ src/nvim/decoration_defs.h | 8 ++ src/nvim/decoration_provider.c | 76 +++++++++++----- src/nvim/lua/executor.c | 42 ++++++--- src/nvim/lua/executor.h | 5 +- test/functional/treesitter/highlight_spec.lua | 2 +- 9 files changed, 133 insertions(+), 106 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 113f54f06f..1ef4b7533c 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3362,6 +3362,12 @@ nvim_set_decoration_provider({ns_id}, {opts}) included. > ["range", winid, bufnr, begin_row, begin_col, end_row, end_col] < + In addition to returning a boolean, it is also allowed to + return a `skip_row, skip_col` pair of integers. This + implies that this function does not need to be called until + a range which continues beyond the skipped position. A + single integer return value `skip_row` is short for + `skip_row, 0` • on_end: called at the end of a redraw cycle > ["end", tick] < diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua index 61e4a28704..abda4dead0 100644 --- a/runtime/lua/vim/_meta/api.lua +++ b/runtime/lua/vim/_meta/api.lua @@ -2175,6 +2175,13 @@ function vim.api.nvim_set_current_win(window) end --- ``` --- ["range", winid, bufnr, begin_row, begin_col, end_row, end_col] --- ``` +--- +--- In addition to returning a boolean, it is also allowed to +--- return a `skip_row, skip_col` pair of integers. This implies +--- that this function does not need to be called until a range +--- which continues beyond the skipped position. A single integer +--- return value `skip_row` is short for `skip_row, 0` +--- --- - on_end: called at the end of a redraw cycle --- ``` --- ["end", tick] diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index da13f7fe47..9165550132 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -53,15 +53,12 @@ function TSHighlighterQuery:query() return self._query end ----@alias MarkInfo { start_line: integer, start_col: integer, opts: vim.api.keyset.set_extmark } - ---@class (private) vim.treesitter.highlighter.State ---@field tstree TSTree ---@field next_row integer ---@field next_col integer ---@field iter vim.treesitter.highlighter.Iter? ---@field highlighter_query vim.treesitter.highlighter.Query ----@field prev_marks MarkInfo[] ---@nodoc ---@class vim.treesitter.highlighter @@ -238,7 +235,6 @@ function TSHighlighter:prepare_highlight_states(win, srow, erow) next_col = 0, iter = nil, highlighter_query = hl_query, - prev_marks = {}, }) end) end @@ -330,44 +326,6 @@ local function get_spell(capture_name) return nil, 0 end ----Adds the mark to the buffer, clipped by the line. ----Queues the remainder if the mark continues after the line. ----@param m MarkInfo ----@param buf integer ----@param range_start_row integer ----@param range_start_col integer ----@param range_end_row integer ----@param range_end_col integer ----@param next_marks MarkInfo[] -local function add_mark( - m, - buf, - range_start_row, - range_start_col, - range_end_row, - range_end_col, - next_marks -) - local cur_start_l = m.start_line - local cur_start_c = m.start_col - if cmp_lt(cur_start_l, cur_start_c, range_start_row, range_start_col) then - cur_start_l = range_start_row - cur_start_c = range_start_col - end - - local cur_opts = m.opts - if cmp_lt(range_end_row, range_end_col, cur_opts.end_line, cur_opts.end_col) then - cur_opts = vim.deepcopy(cur_opts, true) - cur_opts.end_line = range_end_row - cur_opts.end_col = range_end_col - table.insert(next_marks, m) - end - - if cmp_lt(cur_start_l, cur_start_c, cur_opts.end_line, cur_opts.end_col) then - api.nvim_buf_set_extmark(buf, ns, cur_start_l, cur_start_c, cur_opts) - end -end - ---@param self vim.treesitter.highlighter ---@param win integer ---@param buf integer @@ -398,6 +356,10 @@ local function on_range_impl( for i = range_start_row, range_end_row - 1 do self._conceal_checked[i] = self._conceal_line or nil end + + local MAX_ROW = 2147483647 -- sentinel for skipping to the end of file + local skip_until_row = MAX_ROW + local skip_until_col = 0 self:for_each_highlight_state(win, function(state) local root_node = state.tstree:root() ---@type { [1]: integer, [2]: integer, [3]: integer, [4]: integer } @@ -409,25 +371,15 @@ local function on_range_impl( { range_start_row, range_start_col, range_end_row, range_end_col } ) then + if cmp_lt(root_range[1], root_range[2], skip_until_row, skip_until_col) then + skip_until_row = root_range[1] + skip_until_col = root_range[2] + end return end local tree_region = state.tstree:included_ranges(true) - local next_marks = {} - - for _, mark in ipairs(state.prev_marks) do - add_mark( - mark, - buf, - range_start_row, - range_start_col, - range_end_row, - range_end_col, - next_marks - ) - end - local next_row = state.next_row local next_col = state.next_col @@ -488,7 +440,7 @@ local function on_range_impl( local url = get_url(match, buf, capture, metadata) if hl and not on_conceal and (not on_spell or spell ~= nil) then - local opts = { + api.nvim_buf_set_extmark(buf, ns, start_row, start_col, { end_line = end_row, end_col = end_col, hl_group = hl, @@ -497,17 +449,7 @@ local function on_range_impl( conceal = conceal, spell = spell, url = url, - } - local mark = { start_line = start_row, start_col = start_col, opts = opts } - add_mark( - mark, - buf, - range_start_row, - range_start_col, - range_end_row, - range_end_col, - next_marks - ) + }) end if @@ -525,8 +467,12 @@ local function on_range_impl( state.next_row = next_row state.next_col = next_col - state.prev_marks = next_marks + if cmp_lt(next_row, next_col, skip_until_row, skip_until_col) then + skip_until_row = next_row + skip_until_col = next_col + end end) + return skip_until_row, skip_until_col end ---@private @@ -542,7 +488,7 @@ function TSHighlighter._on_range(_, win, buf, br, bc, er, ec, _) return end - on_range_impl(self, win, buf, br, bc, er, ec, false, false) + return on_range_impl(self, win, buf, br, bc, er, ec, false, false) end ---@private diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index c82e3840ca..9d6ed35e12 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -1051,6 +1051,13 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start, /// ``` /// ["range", winid, bufnr, begin_row, begin_col, end_row, end_col] /// ``` +/// +/// In addition to returning a boolean, it is also allowed to +/// return a `skip_row, skip_col` pair of integers. This implies +/// that this function does not need to be called until a range +/// which continues beyond the skipped position. A single integer +/// return value `skip_row` is short for `skip_row, 0` +/// /// - on_end: called at the end of a redraw cycle /// ``` /// ["end", tick] diff --git a/src/nvim/decoration_defs.h b/src/nvim/decoration_defs.h index 3f66d2cb61..33670d1e3c 100644 --- a/src/nvim/decoration_defs.h +++ b/src/nvim/decoration_defs.h @@ -145,6 +145,9 @@ typedef struct { kDecorProviderDisabled = 4, } state; + int win_skip_row; + int win_skip_col; + LuaRef redraw_start; LuaRef redraw_buf; LuaRef redraw_win; @@ -159,3 +162,8 @@ typedef struct { uint8_t error_count; } DecorProvider; + +#define DECORATION_PROVIDER_INIT(ns_id) (DecorProvider) \ + { ns_id, kDecorProviderDisabled, 0, 0, LUA_NOREF, LUA_NOREF, \ + LUA_NOREF, LUA_NOREF, LUA_NOREF, LUA_NOREF, \ + LUA_NOREF, LUA_NOREF, -1, false, false, 0 } diff --git a/src/nvim/decoration_provider.c b/src/nvim/decoration_provider.c index ce6c2b8442..3019bd4df4 100644 --- a/src/nvim/decoration_provider.c +++ b/src/nvim/decoration_provider.c @@ -23,11 +23,6 @@ static kvec_t(DecorProvider) decor_providers = KV_INITIAL_VALUE; -#define DECORATION_PROVIDER_INIT(ns_id) (DecorProvider) \ - { ns_id, kDecorProviderDisabled, LUA_NOREF, LUA_NOREF, \ - LUA_NOREF, LUA_NOREF, LUA_NOREF, LUA_NOREF, \ - LUA_NOREF, LUA_NOREF, -1, false, false, 0 } - static void decor_provider_error(DecorProvider *provider, const char *name, const char *msg) { const char *ns = describe_ns(provider->ns_id, "(UNKNOWN PLUGIN)"); @@ -38,21 +33,28 @@ static void decor_provider_error(DecorProvider *provider, const char *name, cons // Note we pass in a provider index as this function may cause decor_providers providers to be // reallocated so we need to be careful with DecorProvider pointers static bool decor_provider_invoke(int provider_idx, const char *name, LuaRef ref, Array args, - bool default_true) + bool default_true, Array *res) { Error err = ERROR_INIT; textlock++; - Object ret = nlua_call_ref(ref, name, args, kRetNilBool, NULL, &err); + Object ret = nlua_call_ref(ref, name, args, res ? kRetMulti : kRetNilBool, NULL, &err); textlock--; // We get the provider here via an index in case the above call to nlua_call_ref causes // decor_providers to be reallocated. DecorProvider *provider = &kv_A(decor_providers, provider_idx); - if (!ERROR_SET(&err) - && api_object_to_bool(ret, "provider %s retval", default_true, &err)) { + if (!ERROR_SET(&err)) { provider->error_count = 0; - return true; + if (res) { + assert(ret.type == kObjectTypeArray); + *res = ret.data.array; + return true; + } else { + if (api_object_to_bool(ret, "provider %s retval", default_true, &err)) { + return true; + } + } } if (ERROR_SET(&err) && provider->error_count < CB_MAX_ERROR) { @@ -65,7 +67,7 @@ static bool decor_provider_invoke(int provider_idx, const char *name, LuaRef ref } api_clear_error(&err); - api_free_object(ret); + api_free_object(ret); // TODO(bfredl): wants to be on an arena return false; } @@ -81,7 +83,7 @@ void decor_providers_invoke_spell(win_T *wp, int start_row, int start_col, int e ADD_C(args, INTEGER_OBJ(start_col)); ADD_C(args, INTEGER_OBJ(end_row)); ADD_C(args, INTEGER_OBJ(end_col)); - decor_provider_invoke((int)i, "spell", p->spell_nav, args, true); + decor_provider_invoke((int)i, "spell", p->spell_nav, args, true, NULL); } } } @@ -97,7 +99,7 @@ bool decor_providers_invoke_conceal_line(win_T *wp, int row) ADD_C(args, INTEGER_OBJ(wp->handle)); ADD_C(args, INTEGER_OBJ(wp->w_buffer->handle)); ADD_C(args, INTEGER_OBJ(row)); - decor_provider_invoke((int)i, "conceal_line", p->conceal_line, args, true); + decor_provider_invoke((int)i, "conceal_line", p->conceal_line, args, true, NULL); } } return wp->w_buffer->b_marktree->n_keys > keys; @@ -114,7 +116,7 @@ void decor_providers_start(void) if (p->state != kDecorProviderDisabled && p->redraw_start != LUA_NOREF) { MAXSIZE_TEMP_ARRAY(args, 2); ADD_C(args, INTEGER_OBJ((int)display_tick)); - bool active = decor_provider_invoke((int)i, "start", p->redraw_start, args, true); + bool active = decor_provider_invoke((int)i, "start", p->redraw_start, args, true, NULL); kv_A(decor_providers, i).state = active ? kDecorProviderActive : kDecorProviderRedrawDisabled; } else if (p->state != kDecorProviderDisabled) { kv_A(decor_providers, i).state = kDecorProviderActive; @@ -147,6 +149,9 @@ void decor_providers_invoke_win(win_T *wp) p->state = kDecorProviderActive; } + p->win_skip_row = 0; + p->win_skip_col = 0; + if (p->state == kDecorProviderActive && p->redraw_win != LUA_NOREF) { MAXSIZE_TEMP_ARRAY(args, 4); ADD_C(args, WINDOW_OBJ(wp->handle)); @@ -154,7 +159,8 @@ void decor_providers_invoke_win(win_T *wp) // TODO(bfredl): we are not using this, but should be first drawn line? ADD_C(args, INTEGER_OBJ(wp->w_topline - 1)); ADD_C(args, INTEGER_OBJ(botline - 1)); - if (!decor_provider_invoke((int)i, "win", p->redraw_win, args, true)) { + // TODO(bfredl): could skip a call if retval was interpreted like range? + if (!decor_provider_invoke((int)i, "win", p->redraw_win, args, true, NULL)) { kv_A(decor_providers, i).state = kDecorProviderWinDisabled; } } @@ -178,7 +184,7 @@ void decor_providers_invoke_line(win_T *wp, int row) ADD_C(args, WINDOW_OBJ(wp->handle)); ADD_C(args, BUFFER_OBJ(wp->w_buffer->handle)); ADD_C(args, INTEGER_OBJ(row)); - if (!decor_provider_invoke((int)i, "line", p->redraw_line, args, true)) { + if (!decor_provider_invoke((int)i, "line", p->redraw_line, args, true, NULL)) { // return 'false' or error: skip rest of this window kv_A(decor_providers, i).state = kDecorProviderWinDisabled; } @@ -195,6 +201,10 @@ void decor_providers_invoke_range(win_T *wp, int start_row, int start_col, int e for (size_t i = 0; i < kv_size(decor_providers); i++) { DecorProvider *p = &kv_A(decor_providers, i); if (p->state == kDecorProviderActive && p->redraw_range != LUA_NOREF) { + if (p->win_skip_row > end_row || (p->win_skip_row == end_row && p->win_skip_col >= end_col)) { + continue; + } + MAXSIZE_TEMP_ARRAY(args, 6); ADD_C(args, WINDOW_OBJ(wp->handle)); ADD_C(args, BUFFER_OBJ(wp->w_buffer->handle)); @@ -202,11 +212,35 @@ void decor_providers_invoke_range(win_T *wp, int start_row, int start_col, int e ADD_C(args, INTEGER_OBJ(start_col)); ADD_C(args, INTEGER_OBJ(end_row)); ADD_C(args, INTEGER_OBJ(end_col)); - if (!decor_provider_invoke((int)i, "range", p->redraw_range, args, true)) { - // return 'false' or error: skip rest of this window - kv_A(decor_providers, i).state = kDecorProviderWinDisabled; + Array res = ARRAY_DICT_INIT; + bool status = decor_provider_invoke((int)i, "range", p->redraw_range, args, true, &res); + p = &kv_A(decor_providers, i); // lua call might have reallocated decor_providers + + if (!status) { + // error: skip rest of this window + p->state = kDecorProviderWinDisabled; + } else if (res.size >= 1) { + Object first = res.items[0]; + if (first.type == kObjectTypeBoolean) { + if (first.data.boolean == false) { + p->state = kDecorProviderWinDisabled; + } + } else if (first.type == kObjectTypeInteger) { + Integer row = first.data.integer; + Integer col = 0; + if (res.size >= 2) { + Object second = res.items[1]; + if (second.type == kObjectTypeInteger) { + col = second.data.integer; + } + } + p->win_skip_row = (int)row; + p->win_skip_col = (int)col; + } } + api_free_array(res); + hl_check_ns(); } } @@ -226,7 +260,7 @@ void decor_providers_invoke_buf(buf_T *buf) MAXSIZE_TEMP_ARRAY(args, 2); ADD_C(args, BUFFER_OBJ(buf->handle)); ADD_C(args, INTEGER_OBJ((int64_t)display_tick)); - decor_provider_invoke((int)i, "buf", p->redraw_buf, args, true); + decor_provider_invoke((int)i, "buf", p->redraw_buf, args, true, NULL); } } } @@ -243,7 +277,7 @@ void decor_providers_invoke_end(void) if (p->state != kDecorProviderDisabled && p->redraw_end != LUA_NOREF) { MAXSIZE_TEMP_ARRAY(args, 1); ADD_C(args, INTEGER_OBJ((int)display_tick)); - decor_provider_invoke((int)i, "end", p->redraw_end, args, true); + decor_provider_invoke((int)i, "end", p->redraw_end, args, true, NULL); } } decor_check_to_be_deleted(); diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index e3b7abde46..1cedc73e92 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -181,12 +181,9 @@ int nlua_pcall(lua_State *lstate, int nargs, int nresults) lua_remove(lstate, -2); } else { if (nresults == LUA_MULTRET) { - int new_top = lua_gettop(lstate); - int actual_nres = new_top - pre_top + nargs + 1; - lua_remove(lstate, -1 - actual_nres); - } else { - lua_remove(lstate, -1 - nresults); + nresults = lua_gettop(lstate) - (pre_top - nargs - 1); } + lua_remove(lstate, -1 - nresults); } return status; } @@ -1551,6 +1548,7 @@ Object nlua_exec(const String str, const char *chunkname, const Array args, LuaR { lua_State *const lstate = global_lstate; + int top = lua_gettop(lstate); const char *name = (chunkname && chunkname[0]) ? chunkname : ""; if (luaL_loadbuffer(lstate, str.data, str.size, name)) { size_t len; @@ -1570,7 +1568,7 @@ Object nlua_exec(const String str, const char *chunkname, const Array args, LuaR return NIL; } - return nlua_call_pop_retval(lstate, mode, arena, err); + return nlua_call_pop_retval(lstate, mode, arena, top, err); } bool nlua_ref_is_function(LuaRef ref) @@ -1601,10 +1599,16 @@ Object nlua_call_ref(LuaRef ref, const char *name, Array args, LuaRetMode mode, return nlua_call_ref_ctx(false, ref, name, args, mode, arena, err); } +static int mode_ret(LuaRetMode mode) +{ + return mode == kRetMulti ? LUA_MULTRET : 1; +} + Object nlua_call_ref_ctx(bool fast, LuaRef ref, const char *name, Array args, LuaRetMode mode, Arena *arena, Error *err) { lua_State *const lstate = global_lstate; + int top = lua_gettop(lstate); nlua_pushref(lstate, ref); int nargs = (int)args.size; if (name != NULL) { @@ -1616,12 +1620,12 @@ Object nlua_call_ref_ctx(bool fast, LuaRef ref, const char *name, Array args, Lu } if (fast) { - if (nlua_fast_cfpcall(lstate, nargs, 1, -1) < 0) { + if (nlua_fast_cfpcall(lstate, nargs, mode_ret(mode), -1) < 0) { // error is already scheduled, set anyways to convey failure. api_set_error(err, kErrorTypeException, "fast context failure"); return NIL; } - } else if (nlua_pcall(lstate, nargs, 1)) { + } else if (nlua_pcall(lstate, nargs, mode_ret(mode))) { // if err is passed, the caller will deal with the error. if (err) { size_t len; @@ -1633,16 +1637,18 @@ Object nlua_call_ref_ctx(bool fast, LuaRef ref, const char *name, Array args, Lu return NIL; } - return nlua_call_pop_retval(lstate, mode, arena, err); + return nlua_call_pop_retval(lstate, mode, arena, top, err); } -static Object nlua_call_pop_retval(lua_State *lstate, LuaRetMode mode, Arena *arena, Error *err) +static Object nlua_call_pop_retval(lua_State *lstate, LuaRetMode mode, Arena *arena, int pretop, + Error *err) { - if (lua_isnil(lstate, -1)) { + if (mode != kRetMulti && lua_isnil(lstate, -1)) { lua_pop(lstate, 1); return NIL; } Error dummy = ERROR_INIT; + Error *perr = err ? err : &dummy; switch (mode) { case kRetNilBool: { @@ -1658,7 +1664,19 @@ static Object nlua_call_pop_retval(lua_State *lstate, LuaRetMode mode, Arena *ar return LUAREF_OBJ(ref); } case kRetObject: - return nlua_pop_Object(lstate, false, arena, err ? err : &dummy); + return nlua_pop_Object(lstate, false, arena, perr); + case kRetMulti: + ; + int nres = lua_gettop(lstate) - pretop; + Array res = arena_array(arena, (size_t)nres); + for (int i = 0; i < nres; i++) { + res.items[nres - i - 1] = nlua_pop_Object(lstate, false, arena, perr); + if (ERROR_SET(err)) { + return NIL; + } + } + res.size = (size_t)nres; + return ARRAY_OBJ(res); } UNREACHABLE; } diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h index 9493015d10..c73b704a22 100644 --- a/src/nvim/lua/executor.h +++ b/src/nvim/lua/executor.h @@ -38,10 +38,11 @@ typedef struct { } while (0) typedef enum { - kRetObject, ///< any object, but doesn't preserve nested luarefs + kRetObject, ///< any object, but doesn't preserve nested luarefs kRetNilBool, ///< NIL preserved as such, other values return their booleanness ///< Should also be used when return value is ignored, as it is allocation-free - kRetLuaref, ///< return value becomes a single Luaref, regardless of type (except NIL) + kRetLuaref, ///< return value becomes a single Luaref, regardless of type (except NIL) + kRetMulti, ///< like kRetObject but return muliple return values as an Array } LuaRetMode; /// Maximum number of errors in vim.ui_attach() and decor provider callbacks. diff --git a/test/functional/treesitter/highlight_spec.lua b/test/functional/treesitter/highlight_spec.lua index 40d528a379..2f13793686 100644 --- a/test/functional/treesitter/highlight_spec.lua +++ b/test/functional/treesitter/highlight_spec.lua @@ -548,7 +548,7 @@ describe('treesitter highlighting (C)', function() lua = [[ ; query (string) @string - (comment) @comment + ((comment) @comment (#set! priority 90)) (function_call (identifier) @function.call) [ "(" ")" ] @punctuation.bracket ]], From 5119c03be7a9d440ae1ab027c3879d2ad7e71bba Mon Sep 17 00:00:00 2001 From: bfredl Date: Wed, 3 Sep 2025 13:11:39 +0200 Subject: [PATCH 2/2] fix(treesitter): use subpriorities for tree ordering This partially reverts 0b8a72b73934d33a05e20c255298e88cd921df32, that is unreverts 15e77a56b711102fdc123e15b3f37d49bc0b1df1 "priority" is an internal neovim concept which does not occur in shared queries. Ideally a single priority space should eventually be enough for our needs. But as we don't want to poke at the usages of priorities right now in the wider ecosystem, introduce the "subpriorities" so that treesitter code can distinguish highlights of the same priorities with different tree nesting depth. This mainly affects `injection.combined` as parent-tree nodes might appear in the middle of child-tree nodes which otherwise is not possible. --- runtime/lua/vim/_meta/api_keysets.lua | 1 + runtime/lua/vim/treesitter/highlighter.lua | 4 ++++ src/nvim/api/extmark.c | 12 +++++++++++- src/nvim/api/keysets_defs.h | 2 ++ src/nvim/decoration.c | 16 +++++++++------- src/nvim/decoration.h | 2 +- src/nvim/decoration_defs.h | 1 + test/functional/treesitter/highlight_spec.lua | 2 +- 8 files changed, 30 insertions(+), 10 deletions(-) diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua index a3c6b52409..a09b0dbf65 100644 --- a/runtime/lua/vim/_meta/api_keysets.lua +++ b/runtime/lua/vim/_meta/api_keysets.lua @@ -423,6 +423,7 @@ error('Cannot require a meta file') --- @field undo_restore? boolean --- @field url? string --- @field scoped? boolean +--- @field _subpriority? integer --- @class vim.api.keyset.user_command --- @field addr? any diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 9165550132..c0398bbe18 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -360,7 +360,10 @@ local function on_range_impl( local MAX_ROW = 2147483647 -- sentinel for skipping to the end of file local skip_until_row = MAX_ROW local skip_until_col = 0 + + local subtree_counter = 0 self:for_each_highlight_state(win, function(state) + subtree_counter = subtree_counter + 1 local root_node = state.tstree:root() ---@type { [1]: integer, [2]: integer, [3]: integer, [4]: integer } local root_range = { root_node:range() } @@ -449,6 +452,7 @@ local function on_range_impl( conceal = conceal, spell = spell, url = url, + _subpriority = subtree_counter, }) end diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index 9d6ed35e12..a14cf426f4 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -836,6 +836,15 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer col2 = c; } + DecorPriority subpriority = 0; + if (HAS_KEY(opts, set_extmark, _subpriority)) { + VALIDATE_RANGE((opts->_subpriority >= 0 && opts->_subpriority <= UINT16_MAX), + "_subpriority", { + goto error; + }); + subpriority = (DecorPriority)opts->_subpriority; + } + if (kv_size(virt_text.data.virt_text)) { decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_text, NULL), true); } @@ -845,7 +854,8 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer if (has_hl) { DecorSignHighlight sh = decor_sh_from_inline(hl); sh.url = url; - decor_range_add_sh(&decor_state, r, c, line2, col2, &sh, true, (uint32_t)ns_id, id); + decor_range_add_sh(&decor_state, r, c, line2, col2, &sh, true, (uint32_t)ns_id, id, + subpriority); } } else { if (opts->ephemeral) { diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h index e9a7637427..cb68d09186 100644 --- a/src/nvim/api/keysets_defs.h +++ b/src/nvim/api/keysets_defs.h @@ -62,6 +62,8 @@ typedef struct { Boolean undo_restore; String url; Boolean scoped; + + Integer _subpriority; } Dict(set_extmark); typedef struct { diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index 7776adf185..2c64935b64 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -553,12 +553,12 @@ static void decor_range_add_from_inline(DecorState *state, int start_row, int st uint32_t idx = decor.data.ext.sh_idx; while (idx != DECOR_ID_INVALID) { DecorSignHighlight *sh = &kv_A(decor_items, idx); - decor_range_add_sh(state, start_row, start_col, end_row, end_col, sh, owned, ns, mark_id); + decor_range_add_sh(state, start_row, start_col, end_row, end_col, sh, owned, ns, mark_id, 0); idx = sh->next; } } else { DecorSignHighlight sh = decor_sh_from_inline(decor.data.hl); - decor_range_add_sh(state, start_row, start_col, end_row, end_col, &sh, owned, ns, mark_id); + decor_range_add_sh(state, start_row, start_col, end_row, end_col, &sh, owned, ns, mark_id, 0); } } @@ -619,14 +619,15 @@ void decor_range_add_virt(DecorState *state, int start_row, int start_col, int e .data.vt = vt, .attr_id = 0, .owned = owned, - .priority = vt->priority, + .priority_internal = ((DecorPriorityInternal)vt->priority << 16), .draw_col = -10, }; decor_range_insert(state, &range); } void decor_range_add_sh(DecorState *state, int start_row, int start_col, int end_row, int end_col, - DecorSignHighlight *sh, bool owned, uint32_t ns, uint32_t mark_id) + DecorSignHighlight *sh, bool owned, uint32_t ns, uint32_t mark_id, + DecorPriority subpriority) { if (sh->flags & kSHIsSign) { return; @@ -638,7 +639,7 @@ void decor_range_add_sh(DecorState *state, int start_row, int start_col, int end .data.sh = *sh, .attr_id = 0, .owned = owned, - .priority = sh->priority, + .priority_internal = ((DecorPriorityInternal)sh->priority << 16) + subpriority, .draw_col = -10, }; @@ -731,7 +732,7 @@ next_mark: break; } int const ordering = r->ordering; - DecorPriority const priority = r->priority; + DecorPriorityInternal const priority = r->priority_internal; int begin = 0; int end = cur_end; @@ -739,7 +740,8 @@ next_mark: int mid = begin + ((end - begin) >> 1); int mi = indices[mid]; DecorRange *mr = &slots[mi].range; - if (mr->priority < priority || (mr->priority == priority && mr->ordering < ordering)) { + if (mr->priority_internal < priority + || (mr->priority_internal == priority && mr->ordering < ordering)) { begin = mid + 1; } else { end = mid; diff --git a/src/nvim/decoration.h b/src/nvim/decoration.h index a1b0df21c2..08586eb238 100644 --- a/src/nvim/decoration.h +++ b/src/nvim/decoration.h @@ -37,7 +37,7 @@ typedef struct { int end_row; int end_col; int ordering; ///< range insertion order - DecorPriority priority; + DecorPriorityInternal priority_internal; bool owned; ///< ephemeral decoration, free memory immediately DecorRangeKind kind; // next pointers MUST NOT be used, these are separate ranges diff --git a/src/nvim/decoration_defs.h b/src/nvim/decoration_defs.h index 33670d1e3c..c392880818 100644 --- a/src/nvim/decoration_defs.h +++ b/src/nvim/decoration_defs.h @@ -36,6 +36,7 @@ enum { typedef kvec_t(struct virt_line { VirtText line; int flags; }) VirtLines; typedef uint16_t DecorPriority; +typedef uint32_t DecorPriorityInternal; #define DECOR_PRIORITY_BASE 0x1000 /// Keep in sync with hl_mode_str[] in decoration.h diff --git a/test/functional/treesitter/highlight_spec.lua b/test/functional/treesitter/highlight_spec.lua index 2f13793686..40d528a379 100644 --- a/test/functional/treesitter/highlight_spec.lua +++ b/test/functional/treesitter/highlight_spec.lua @@ -548,7 +548,7 @@ describe('treesitter highlighting (C)', function() lua = [[ ; query (string) @string - ((comment) @comment (#set! priority 90)) + (comment) @comment (function_call (identifier) @function.call) [ "(" ")" ] @punctuation.bracket ]],