From 15e77a56b711102fdc123e15b3f37d49bc0b1df1 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:13:58 -0600 Subject: [PATCH] feat(extmarks): subpriorities (relative to declaration order) (#27131) The "priority" field of extmarks can be used to set priorities of extmarks which dictates which highlight group a range will actually have when there are multiple extmarks applied. However, when multiple extmarks have the same priority, the only way to enforce an actual priority is through the order in which the extmarks are set. It is not always possible or desirable to set extmarks in a specific order, however, so we add a new "subpriority" field that explicitly enforces the ordering of extmarks that have the same priority. For now this will be used only to enforce priority of treesitter highlights. A single node in a treesitter tree may match multiple captures, in which case that node will have multiple extmarks set. The order in which captures are returned from the treesitter API is not _necessarily_ in the same order they are defined in a query file, so we use the new subpriority field to force that ordering. For now subpriorites are not documented and are not meant to be used by external code, and it only applies to ephemeral extmarks. We indicate the "private" nature of subpriorities by prefixing the field name with an "_". --- runtime/lua/vim/_meta/api_keysets.lua | 1 + src/nvim/api/extmark.c | 20 +++++++++--- src/nvim/api/keysets_defs.h | 2 ++ src/nvim/decoration.c | 19 +++++++---- src/nvim/decoration.h | 2 ++ test/functional/api/extmark_spec.lua | 30 +++++++++++++++++ test/functional/ui/decorations_spec.lua | 43 ++++++++++++++++++++++++- 7 files changed, 106 insertions(+), 11 deletions(-) diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua index 00d3ff5bb4..c9ef4e9b4f 100644 --- a/runtime/lua/vim/_meta/api_keysets.lua +++ b/runtime/lua/vim/_meta/api_keysets.lua @@ -274,6 +274,7 @@ error('Cannot require a meta file') --- @field ui_watched? boolean --- @field undo_restore? boolean --- @field url? string +--- @field _subpriority? integer --- @class vim.api.keyset.user_command --- @field addr? any diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index 27a4b7854f..4e84b41a02 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -748,20 +748,32 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer col2 = c; } + DecorPriority subpriority = DECOR_PRIORITY_BASE; + 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); + decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_text, NULL), true, + subpriority); } if (kv_size(virt_lines.data.virt_lines)) { - decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_lines, NULL), true); + decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_lines, NULL), true, + subpriority); } if (url != NULL) { DecorSignHighlight sh = DECOR_SIGN_HIGHLIGHT_INIT; sh.url = url; - decor_range_add_sh(&decor_state, r, c, line2, col2, &sh, true, 0, 0); + decor_range_add_sh(&decor_state, r, c, line2, col2, &sh, true, 0, 0, subpriority); } if (has_hl) { DecorSignHighlight sh = decor_sh_from_inline(hl); - 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 811f60f4d6..85896c1fa7 100644 --- a/src/nvim/api/keysets_defs.h +++ b/src/nvim/api/keysets_defs.h @@ -55,6 +55,8 @@ typedef struct { Boolean ui_watched; Boolean undo_restore; String url; + + Integer _subpriority; } Dict(set_extmark); typedef struct { diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index 755655856d..5358241644 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -449,18 +449,21 @@ static void decor_range_add_from_inline(DecorState *state, int start_row, int st if (decor.ext) { DecorVirtText *vt = decor.data.ext.vt; while (vt) { - decor_range_add_virt(state, start_row, start_col, end_row, end_col, vt, owned); + decor_range_add_virt(state, start_row, start_col, end_row, end_col, vt, owned, + DECOR_PRIORITY_BASE); vt = vt->next; } 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, + DECOR_PRIORITY_BASE); 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, + DECOR_PRIORITY_BASE); } } @@ -470,7 +473,8 @@ static void decor_range_insert(DecorState *state, DecorRange range) size_t index; for (index = kv_size(state->active) - 1; index > 0; index--) { DecorRange item = kv_A(state->active, index - 1); - if (item.priority <= range.priority) { + if ((item.priority < range.priority) + || ((item.priority == range.priority) && (item.subpriority <= range.subpriority))) { break; } kv_A(state->active, index) = kv_A(state->active, index - 1); @@ -479,7 +483,7 @@ static void decor_range_insert(DecorState *state, DecorRange range) } void decor_range_add_virt(DecorState *state, int start_row, int start_col, int end_row, int end_col, - DecorVirtText *vt, bool owned) + DecorVirtText *vt, bool owned, DecorPriority subpriority) { bool is_lines = vt->flags & kVTIsLines; DecorRange range = { @@ -489,13 +493,15 @@ void decor_range_add_virt(DecorState *state, int start_row, int start_col, int e .attr_id = 0, .owned = owned, .priority = vt->priority, + .subpriority = subpriority, .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; @@ -508,6 +514,7 @@ void decor_range_add_sh(DecorState *state, int start_row, int start_col, int end .attr_id = 0, .owned = owned, .priority = sh->priority, + .subpriority = subpriority, .draw_col = -10, }; diff --git a/src/nvim/decoration.h b/src/nvim/decoration.h index b3ff737123..e70c588806 100644 --- a/src/nvim/decoration.h +++ b/src/nvim/decoration.h @@ -48,6 +48,8 @@ typedef struct { int attr_id; ///< cached lookup of inl.hl_id if it was a highlight bool owned; ///< ephemeral decoration, free memory immediately DecorPriority priority; + DecorPriority subpriority; ///< Secondary priority value used for ordering (#27131). + ///< Reflects the order of patterns/captures in the query file. DecorRangeKind kind; /// Screen column to draw the virtual text. /// When -1, the virtual text may be drawn after deciding where. diff --git a/test/functional/api/extmark_spec.lua b/test/functional/api/extmark_spec.lua index 0a286965f2..2acfbfc949 100644 --- a/test/functional/api/extmark_spec.lua +++ b/test/functional/api/extmark_spec.lua @@ -1798,6 +1798,36 @@ describe('API/extmarks', function() eq(1, #extmarks) eq('https://example.com', extmarks[1][4].url) end) + + it('respects priority', function() + screen = Screen.new(15, 10) + screen:attach() + + set_extmark(ns, marks[1], 0, 0, { + hl_group = 'Comment', + end_col = 2, + priority = 20, + }) + + -- Extmark defined after first extmark but has lower priority, first extmark "wins" + set_extmark(ns, marks[2], 0, 0, { + hl_group = 'String', + end_col = 2, + priority = 10, + }) + + screen:expect { + grid = [[ + {1:12}34^5 | + {2:~ }|*8 + | + ]], + attr_ids = { + [1] = { foreground = Screen.colors.Blue1 }, + [2] = { foreground = Screen.colors.Blue1, bold = true }, + }, + } + end) end) describe('Extmarks buffer api with many marks', function() diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua index 7b3533454c..cb804f056d 100644 --- a/test/functional/ui/decorations_spec.lua +++ b/test/functional/ui/decorations_spec.lua @@ -687,7 +687,7 @@ describe('decorations providers', function() ]]} end) - it('can add new providers during redraw #26652', function() + it('can add new providers during redraw #26652', function() setup_provider [[ local ns = api.nvim_create_namespace('test_no_add') function on_do(...) @@ -697,6 +697,47 @@ describe('decorations providers', function() helpers.assert_alive() end) + + it('supports subpriorities (order of definitions in a query file #27131)', function() + insert(mulholland) + setup_provider [[ + local test_ns = api.nvim_create_namespace('mulholland') + function on_do(event, ...) + if event == "line" then + local win, buf, line = ... + api.nvim_buf_set_extmark(buf, test_ns, line, 0, { + end_row = line + 1, + hl_eol = true, + hl_group = 'Comment', + ephemeral = true, + priority = 100, + _subpriority = 20, + }) + + -- This extmark is set last but has a lower subpriority, so the first extmark "wins" + api.nvim_buf_set_extmark(buf, test_ns, line, 0, { + end_row = line + 1, + hl_eol = true, + hl_group = 'String', + ephemeral = true, + priority = 100, + _subpriority = 10, + }) + end + end + ]] + + screen:expect{grid=[[ + {4:// just to see if there was an accident }| + {4:// on Mulholland Drive }| + {4:try_start(); }| + {4:bufref_T save_buf; }| + {4:switch_buffer(&save_buf, buf); }| + {4:posp = getmark(mark, false); }| + {4:restore_buffer(&save_buf);^ }| + | + ]]} + end) end) local example_text = [[