feat(treesitter): vertical conceal support for highlighter

TSHighlighter now places marks for conceal_lines metadata. A new
internal decor provider callback _on_conceal_line was added that
instructs the highlighter to place conceal_lines marks whenever the
editor needs to know whether a line is concealed. The bundled markdown
queries use conceal_lines metadata to conceal code block fence lines.
This commit is contained in:
Luuk van Baal
2024-11-24 14:46:20 +01:00
committed by luukvbaal
parent f58e7d5fac
commit 8ba047e33f
13 changed files with 280 additions and 67 deletions

View File

@ -406,6 +406,8 @@ TREESITTER
• |LanguageTree:is_valid()| now accepts a range parameter to narrow the scope • |LanguageTree:is_valid()| now accepts a range parameter to narrow the scope
of the validity check. of the validity check.
• |:InspectTree| now shows which nodes are missing. • |:InspectTree| now shows which nodes are missing.
• Bundled markdown highlight queries use `conceal_lines` metadata to conceal
code block fence lines vertically.
TUI TUI

View File

@ -492,10 +492,10 @@ capture marks comments as to be checked: >query
There is also `@nospell` which disables spellchecking regions with `@spell`. There is also `@nospell` which disables spellchecking regions with `@spell`.
*treesitter-highlight-conceal* *treesitter-highlight-conceal*
Treesitter highlighting supports |conceal| via the `conceal` metadata. By Treesitter highlighting supports |conceal| via the `conceal` and `conceal_lines`
convention, nodes to be concealed are captured as `@conceal`, but any capture metadata. By convention, nodes to be concealed are captured as `@conceal`, but
can be used. For example, the following query can be used to hide code block any capture can be used. For example, the following query can be used to hide
delimiters in Markdown: >query code block delimiters in Markdown: >query
((fenced_code_block_delimiter) @conceal (#set! conceal "")) ((fenced_code_block_delimiter) @conceal (#set! conceal ""))
< <
@ -506,7 +506,13 @@ still highlighted the same as other operators: >query
"!=" @operator (#set! conceal "≠") "!=" @operator (#set! conceal "≠")
< <
Conceals specified in this way respect 'conceallevel'. To conceal an entire line (do not draw it at all), a query with `conceal_lines`
metadata can be used: >query
((comment) @comment @spell
(#set! conceal_lines ""))
<
Conceals specified in this way respect 'conceallevel' and 'concealcursor'.
Note that although you can use any string for `conceal`, only the first Note that although you can use any string for `conceal`, only the first
character will be used: >query character will be used: >query

View File

@ -236,6 +236,7 @@ error('Cannot require a meta file')
--- @field on_end? fun(_: "end", tick: integer) --- @field on_end? fun(_: "end", tick: integer)
--- @field _on_hl_def? fun(_: "hl_def") --- @field _on_hl_def? fun(_: "hl_def")
--- @field _on_spell_nav? fun(_: "spell_nav") --- @field _on_spell_nav? fun(_: "spell_nav")
--- @field _on_conceal_line? fun(_: "conceal_line")
--- @class vim.api.keyset.set_extmark --- @class vim.api.keyset.set_extmark
--- @field id? integer --- @field id? integer

View File

@ -67,6 +67,8 @@ end
--- This state is kept during rendering across each line update. --- This state is kept during rendering across each line update.
---@field private _highlight_states vim.treesitter.highlighter.State[] ---@field private _highlight_states vim.treesitter.highlighter.State[]
---@field private _queries table<string,vim.treesitter.highlighter.Query> ---@field private _queries table<string,vim.treesitter.highlighter.Query>
---@field _conceal_line boolean?
---@field _conceal_checked table<integer, boolean>
---@field tree vim.treesitter.LanguageTree ---@field tree vim.treesitter.LanguageTree
---@field private redraw_count integer ---@field private redraw_count integer
---@field parsing boolean true if we are parsing asynchronously ---@field parsing boolean true if we are parsing asynchronously
@ -108,6 +110,11 @@ function TSHighlighter.new(tree, opts)
self:on_changedtree(t:included_ranges(true)) self:on_changedtree(t:included_ranges(true))
end) end)
end, end,
on_child_added = function(child)
child:for_each_tree(function(t)
self._conceal_line = self._conceal_line or self:get_query(t:lang()):query().has_conceal_line
end)
end,
}, true) }, true)
local source = tree:source() local source = tree:source()
@ -115,7 +122,7 @@ function TSHighlighter.new(tree, opts)
self.bufnr = source self.bufnr = source
self.redraw_count = 0 self.redraw_count = 0
self._highlight_states = {} self._conceal_checked = {}
self._queries = {} self._queries = {}
-- Queries for a specific language can be overridden by a custom -- Queries for a specific language can be overridden by a custom
@ -123,8 +130,10 @@ function TSHighlighter.new(tree, opts)
if opts.queries then if opts.queries then
for lang, query_string in pairs(opts.queries) do for lang, query_string in pairs(opts.queries) do
self._queries[lang] = TSHighlighterQuery.new(lang, query_string) self._queries[lang] = TSHighlighterQuery.new(lang, query_string)
self._conceal_line = self._conceal_line or self._queries[lang]:query().has_conceal_line
end end
end end
self._conceal_line = self._conceal_line or self:get_query(tree:lang()):query().has_conceal_line
self.orig_spelloptions = vim.bo[self.bufnr].spelloptions self.orig_spelloptions = vim.bo[self.bufnr].spelloptions
@ -141,7 +150,7 @@ function TSHighlighter.new(tree, opts)
-- immediately afterwards will not error. -- immediately afterwards will not error.
if vim.g.syntax_on ~= 1 then if vim.g.syntax_on ~= 1 then
vim.cmd.runtime({ 'syntax/synload.vim', bang = true }) vim.cmd.runtime({ 'syntax/synload.vim', bang = true })
vim.api.nvim_create_augroup('syntaxset', { clear = false }) api.nvim_create_augroup('syntaxset', { clear = false })
end end
vim._with({ buf = self.bufnr }, function() vim._with({ buf = self.bufnr }, function()
@ -159,6 +168,7 @@ function TSHighlighter:destroy()
if api.nvim_buf_is_loaded(self.bufnr) then if api.nvim_buf_is_loaded(self.bufnr) then
vim.bo[self.bufnr].spelloptions = self.orig_spelloptions vim.bo[self.bufnr].spelloptions = self.orig_spelloptions
vim.b[self.bufnr].ts_highlight = nil vim.b[self.bufnr].ts_highlight = nil
api.nvim_buf_clear_namespace(self.bufnr, ns, 0, -1)
if vim.g.syntax_on == 1 then if vim.g.syntax_on == 1 then
api.nvim_exec_autocmds( api.nvim_exec_autocmds(
'FileType', 'FileType',
@ -187,10 +197,9 @@ function TSHighlighter:prepare_highlight_states(srow, erow)
return return
end end
local highlighter_query = self:get_query(tree:lang()) local hl_query = self:get_query(tree:lang())
-- Some injected languages may not have highlight queries. -- Some injected languages may not have highlight queries.
if not highlighter_query:query() then if not hl_query:query() then
return return
end end
@ -200,7 +209,7 @@ function TSHighlighter:prepare_highlight_states(srow, erow)
tstree = tstree, tstree = tstree,
next_row = 0, next_row = 0,
iter = nil, iter = nil,
highlighter_query = highlighter_query, highlighter_query = hl_query,
}) })
end) end)
end end
@ -222,7 +231,15 @@ end
---@param changes Range6[] ---@param changes Range6[]
function TSHighlighter:on_changedtree(changes) function TSHighlighter:on_changedtree(changes)
for _, ch in ipairs(changes) do for _, ch in ipairs(changes) do
api.nvim__redraw({ buf = self.bufnr, range = { ch[1], ch[4] + 1 }, flush = false }) api.nvim__redraw({ buf = self.bufnr, range = { ch[1], ch[4] }, flush = false })
-- Only invalidate the _conceal_checked range if _conceal_line is set and
-- ch[4] is not UINT32_MAX (empty range on first changedtree).
if ch[4] == 2 ^ 32 - 1 then
self._conceal_checked = {}
end
for i = ch[1], self._conceal_line and ch[4] ~= 2 ^ 32 - 1 and ch[4] or 0 do
self._conceal_checked[i] = false
end
end end
end end
@ -286,8 +303,10 @@ end
---@param self vim.treesitter.highlighter ---@param self vim.treesitter.highlighter
---@param buf integer ---@param buf integer
---@param line integer ---@param line integer
---@param is_spell_nav boolean ---@param on_spell boolean
local function on_line_impl(self, buf, line, is_spell_nav) ---@param on_conceal boolean
local function on_line_impl(self, buf, line, on_spell, on_conceal)
self._conceal_checked[line] = true
self:for_each_highlight_state(function(state) self:for_each_highlight_state(function(state)
local root_node = state.tstree:root() local root_node = state.tstree:root()
local root_start_row, _, root_end_row, _ = root_node:range() local root_start_row, _, root_end_row, _ = root_node:range()
@ -335,7 +354,7 @@ local function on_line_impl(self, buf, line, is_spell_nav)
local url = get_url(match, buf, capture, metadata) local url = get_url(match, buf, capture, metadata)
if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then if hl and end_row >= line and not on_conceal and (not on_spell or spell ~= nil) then
api.nvim_buf_set_extmark(buf, ns, start_row, start_col, { api.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row, end_line = end_row,
end_col = end_col, end_col = end_col,
@ -347,6 +366,16 @@ local function on_line_impl(self, buf, line, is_spell_nav)
url = url, url = url,
}) })
end end
if
(metadata.conceal_lines or metadata[capture] and metadata[capture].conceal_lines)
and #api.nvim_buf_get_extmarks(buf, ns, { start_row, 0 }, { start_row, 0 }, {}) == 0
then
api.nvim_buf_set_extmark(buf, ns, start_row, 0, {
end_line = end_row,
conceal_lines = '',
})
end
end end
if start_row > line then if start_row > line then
@ -366,7 +395,7 @@ function TSHighlighter._on_line(_, _win, buf, line, _)
return return
end end
on_line_impl(self, buf, line, false) on_line_impl(self, buf, line, false, false)
end end
---@private ---@private
@ -385,11 +414,42 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
self:prepare_highlight_states(srow, erow) self:prepare_highlight_states(srow, erow)
for row = srow, erow do for row = srow, erow do
on_line_impl(self, buf, row, true) on_line_impl(self, buf, row, true, false)
end end
self._highlight_states = highlight_states self._highlight_states = highlight_states
end end
---@private
---@param buf integer
---@param row integer
function TSHighlighter._on_conceal_line(_, _, buf, row)
local self = TSHighlighter.active[buf]
if not self or not self._conceal_line or self._conceal_checked[row] then
return self and self._conceal_line
end
-- Do not affect potentially populated highlight state.
local highlight_states = self._highlight_states
self.tree:parse({ row, row })
self:prepare_highlight_states(row, row)
on_line_impl(self, buf, row, false, true)
self._highlight_states = highlight_states
return true
end
---@private
--- Clear conceal_lines marks whenever we redraw for a buffer change. Marks are
--- added back as either the _conceal_line or on_win callback comes across them.
function TSHighlighter._on_buf(_, buf)
local self = TSHighlighter.active[buf]
if not self or not self._conceal_line then
return
end
api.nvim_buf_clear_namespace(buf, ns, 0, -1)
self._conceal_checked = {}
end
---@private ---@private
---@param buf integer ---@param buf integer
---@param topline integer ---@param topline integer
@ -413,7 +473,9 @@ end
api.nvim_set_decoration_provider(ns, { api.nvim_set_decoration_provider(ns, {
on_win = TSHighlighter._on_win, on_win = TSHighlighter._on_win,
on_line = TSHighlighter._on_line, on_line = TSHighlighter._on_line,
on_buf = TSHighlighter._on_buf,
_on_spell_nav = TSHighlighter._on_spell_nav, _on_spell_nav = TSHighlighter._on_spell_nav,
_on_conceal_line = TSHighlighter._on_conceal_line,
}) })
return TSHighlighter return TSHighlighter

View File

@ -10,6 +10,20 @@ local EXTENDS_FORMAT = '^;+%s*extends%s*$'
local M = {} local M = {}
---@nodoc
---Parsed query, see |vim.treesitter.query.parse()|
---
---@class vim.treesitter.Query
---@field lang string parser language name
---@field captures string[] list of (unique) capture names defined in query
---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives)
---@field has_conceal_line boolean whether the query sets conceal_lines metadata
---@field has_combined_injections boolean whether the query contains combined injections
---@field query TSQuery userdata query object
---@field private _processed_patterns table<integer, vim.treesitter.query.ProcessedPattern>
local Query = {}
Query.__index = Query
local function is_directive(name) local function is_directive(name)
return string.sub(name, -1) == '!' return string.sub(name, -1) == '!'
end end
@ -28,15 +42,10 @@ end
---@field directives vim.treesitter.query.ProcessedDirective[] ---@field directives vim.treesitter.query.ProcessedDirective[]
--- Splits the query patterns into predicates and directives. --- Splits the query patterns into predicates and directives.
---@param patterns table<integer, (integer|string)[][]> function Query:_process_patterns()
---@return table<integer, vim.treesitter.query.ProcessedPattern> self._processed_patterns = {}
---@return boolean
local function process_patterns(patterns)
---@type table<integer, vim.treesitter.query.ProcessedPattern>
local processed_patterns = {}
local has_combined = false
for k, pattern_list in pairs(patterns) do for k, pattern_list in pairs(self.info.patterns) do
---@type vim.treesitter.query.ProcessedPredicate[] ---@type vim.treesitter.query.ProcessedPredicate[]
local predicates = {} local predicates = {}
---@type vim.treesitter.query.ProcessedDirective[] ---@type vim.treesitter.query.ProcessedDirective[]
@ -50,7 +59,10 @@ local function process_patterns(patterns)
if is_directive(pred_name) then if is_directive(pred_name) then
table.insert(directives, pattern) table.insert(directives, pattern)
if vim.deep_equal(pattern, { 'set!', 'injection.combined' }) then if vim.deep_equal(pattern, { 'set!', 'injection.combined' }) then
has_combined = true self._has_combined_injections = true
end
if vim.deep_equal(pattern, { 'set!', 'conceal_lines', '' }) then
self.has_conceal_line = true
end end
else else
local should_match = true local should_match = true
@ -62,25 +74,10 @@ local function process_patterns(patterns)
end end
end end
processed_patterns[k] = { predicates = predicates, directives = directives } self._processed_patterns[k] = { predicates = predicates, directives = directives }
end end
return processed_patterns, has_combined
end end
---@nodoc
---Parsed query, see |vim.treesitter.query.parse()|
---
---@class vim.treesitter.Query
---@field lang string parser language name
---@field captures string[] list of (unique) capture names defined in query
---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives)
---@field query TSQuery userdata query object
---@field has_combined_injections boolean whether the query contains combined injections
---@field private _processed_patterns table<integer, vim.treesitter.query.ProcessedPattern>
local Query = {}
Query.__index = Query
---@package ---@package
---@see vim.treesitter.query.parse ---@see vim.treesitter.query.parse
---@param lang string ---@param lang string
@ -96,7 +93,7 @@ function Query.new(lang, ts_query)
patterns = query_info.patterns, patterns = query_info.patterns,
} }
self.captures = self.info.captures self.captures = self.info.captures
self._processed_patterns, self.has_combined_injections = process_patterns(self.info.patterns) self:_process_patterns()
return self return self
end end

View File

@ -49,12 +49,14 @@
(fenced_code_block (fenced_code_block
(fenced_code_block_delimiter) @markup.raw.block (fenced_code_block_delimiter) @markup.raw.block
(#set! conceal "")) (#set! conceal "")
(#set! conceal_lines ""))
(fenced_code_block (fenced_code_block
(info_string (info_string
(language) @label (language) @label
(#set! conceal ""))) (#set! conceal "")
(#set! conceal_lines "")))
(link_destination) @markup.link.url (link_destination) @markup.link.url

View File

@ -1049,6 +1049,7 @@ void nvim_set_decoration_provider(Integer ns_id, Dict(set_decoration_provider) *
{ "on_end", &opts->on_end, &p->redraw_end }, { "on_end", &opts->on_end, &p->redraw_end },
{ "_on_hl_def", &opts->_on_hl_def, &p->hl_def }, { "_on_hl_def", &opts->_on_hl_def, &p->hl_def },
{ "_on_spell_nav", &opts->_on_spell_nav, &p->spell_nav }, { "_on_spell_nav", &opts->_on_spell_nav, &p->spell_nav },
{ "_on_conceal_line", &opts->_on_conceal_line, &p->conceal_line },
{ NULL, NULL, NULL }, { NULL, NULL, NULL },
}; };

View File

@ -21,6 +21,7 @@ typedef struct {
LuaRefOf(("end" _, Integer tick)) on_end; LuaRefOf(("end" _, Integer tick)) on_end;
LuaRefOf(("hl_def" _)) _on_hl_def; LuaRefOf(("hl_def" _)) _on_hl_def;
LuaRefOf(("spell_nav" _)) _on_spell_nav; LuaRefOf(("spell_nav" _)) _on_spell_nav;
LuaRefOf(("conceal_line" _)) _on_conceal_line;
} Dict(set_decoration_provider); } Dict(set_decoration_provider);
typedef struct { typedef struct {

View File

@ -1313,23 +1313,18 @@ struct window_S {
linenr_T w_statuscol_line_count; // line count when 'statuscolumn' width was computed. linenr_T w_statuscol_line_count; // line count when 'statuscolumn' width was computed.
int w_nrwidth_width; // nr of chars to print line count. int w_nrwidth_width; // nr of chars to print line count.
qf_info_T *w_llist; // Location list for this window qf_info_T *w_llist; // Location list for this window
// Location list reference used in the location list window. // Location list reference used in the location list window.
// In a non-location list window, w_llist_ref is NULL. // In a non-location list window, w_llist_ref is NULL.
qf_info_T *w_llist_ref; qf_info_T *w_llist_ref;
// Status line click definitions StlClickDefinition *w_status_click_defs; // Status line click definitions
StlClickDefinition *w_status_click_defs; size_t w_status_click_defs_size; // Size of the w_status_click_defs array
// Size of the w_status_click_defs array StlClickDefinition *w_winbar_click_defs; // Window bar click definitions
size_t w_status_click_defs_size; size_t w_winbar_click_defs_size; // Size of the w_winbar_click_defs array
StlClickDefinition *w_statuscol_click_defs; // Status column click definitions
size_t w_statuscol_click_defs_size; // Size of the w_statuscol_click_defs array
// Window bar click definitions buf_T *w_conceal_line_buf; // buffer in win when first invoked
StlClickDefinition *w_winbar_click_defs; bool w_conceal_line_provider; // whether conceal_line provider is active
// Size of the w_winbar_click_defs array
size_t w_winbar_click_defs_size;
// Status column click definitions
StlClickDefinition *w_statuscol_click_defs;
// Size of the w_statuscol_click_defs array
size_t w_statuscol_click_defs_size;
}; };

View File

@ -12,6 +12,7 @@
#include "nvim/buffer_defs.h" #include "nvim/buffer_defs.h"
#include "nvim/change.h" #include "nvim/change.h"
#include "nvim/decoration.h" #include "nvim/decoration.h"
#include "nvim/decoration_provider.h"
#include "nvim/drawscreen.h" #include "nvim/drawscreen.h"
#include "nvim/extmark.h" #include "nvim/extmark.h"
#include "nvim/fold.h" #include "nvim/fold.h"
@ -843,7 +844,8 @@ next_mark:
static const uint32_t conceal_filter[kMTMetaCount] = {[kMTMetaConcealLines] = kMTFilterSelect }; static const uint32_t conceal_filter[kMTMetaCount] = {[kMTMetaConcealLines] = kMTFilterSelect };
/// Called by draw, move and plines code to determine whether a line is concealed. /// Called by draw, move and plines code to determine whether a line is concealed.
/// Scans the marktree for conceal_line marks on "row", if necessary. /// Scans the marktree for conceal_line marks on "row" and invokes any
/// _on_conceal_line decoration provider callbacks, if necessary.
/// ///
/// @param check_cursor If true, avoid an early return for an unconcealed cursorline. /// @param check_cursor If true, avoid an early return for an unconcealed cursorline.
/// Depending on the callsite, we still want to know whether the /// Depending on the callsite, we still want to know whether the
@ -852,12 +854,25 @@ static const uint32_t conceal_filter[kMTMetaCount] = {[kMTMetaConcealLines] = kM
/// @return whether "row" is concealed /// @return whether "row" is concealed
bool decor_conceal_line(win_T *wp, int row, bool check_cursor) bool decor_conceal_line(win_T *wp, int row, bool check_cursor)
{ {
if (wp->w_p_cole < 2 || !buf_meta_total(wp->w_buffer, kMTMetaConcealLines) if (wp->w_p_cole < 2
|| (!check_cursor && wp == curwin && row + 1 == wp->w_cursor.lnum || (!check_cursor && wp == curwin && row + 1 == wp->w_cursor.lnum
&& !conceal_cursor_line(wp))) { && !conceal_cursor_line(wp))) {
return false; return false;
} }
// No need to scan the marktree if there are no conceal_line marks.
if (!buf_meta_total(wp->w_buffer, kMTMetaConcealLines)) {
// Only invoke callback when a decor provider has indicated that it may
// conceal lines in a certain buffer (the callback's return value).
if (wp->w_conceal_line_buf != wp->w_buffer) {
wp->w_conceal_line_provider = false;
}
if (wp->w_conceal_line_provider || wp->w_conceal_line_buf != wp->w_buffer) {
goto invoke;
}
return false;
}
// Scan the marktree for any conceal_line marks on this row. // Scan the marktree for any conceal_line marks on this row.
MTPair pair; MTPair pair;
MarkTreeIter itr[1]; MarkTreeIter itr[1];
@ -880,14 +895,20 @@ bool decor_conceal_line(win_T *wp, int row, bool check_cursor)
} }
marktree_itr_next_filter(wp->w_buffer->b_marktree, itr, row + 1, 0, conceal_filter); marktree_itr_next_filter(wp->w_buffer->b_marktree, itr, row + 1, 0, conceal_filter);
} }
return false;
invoke:;
// Interpret increase in keys to mean this row is concealed by a callback.
size_t keys = wp->w_buffer->b_marktree->n_keys;
decor_providers_invoke_conceal_line(wp, row);
return wp->w_buffer->b_marktree->n_keys > keys;
} }
/// @return whether a window may have folded or concealed lines /// @return whether a window may have folded or concealed lines
bool win_lines_concealed(win_T *wp) bool win_lines_concealed(win_T *wp)
{ {
return hasAnyFolding(wp) return hasAnyFolding(wp)
|| (wp->w_p_cole >= 2 && buf_meta_total(wp->w_buffer, kMTMetaConcealLines)); || (wp->w_p_cole >= 2
&& (wp->w_conceal_line_provider || buf_meta_total(wp->w_buffer, kMTMetaConcealLines)));
} }
int sign_item_cmp(const void *p1, const void *p2) int sign_item_cmp(const void *p1, const void *p2)

View File

@ -152,6 +152,7 @@ typedef struct {
LuaRef redraw_end; LuaRef redraw_end;
LuaRef hl_def; LuaRef hl_def;
LuaRef spell_nav; LuaRef spell_nav;
LuaRef conceal_line;
int hl_valid; int hl_valid;
bool hl_cached; bool hl_cached;

View File

@ -30,7 +30,7 @@ static kvec_t(DecorProvider) decor_providers = KV_INITIAL_VALUE;
#define DECORATION_PROVIDER_INIT(ns_id) (DecorProvider) \ #define DECORATION_PROVIDER_INIT(ns_id) (DecorProvider) \
{ ns_id, kDecorProviderDisabled, LUA_NOREF, LUA_NOREF, \ { ns_id, kDecorProviderDisabled, LUA_NOREF, LUA_NOREF, \
LUA_NOREF, LUA_NOREF, LUA_NOREF, \ LUA_NOREF, LUA_NOREF, LUA_NOREF, \
LUA_NOREF, -1, false, false, 0 } LUA_NOREF, LUA_NOREF, -1, false, false, 0 }
static void decor_provider_error(DecorProvider *provider, const char *name, const char *msg) static void decor_provider_error(DecorProvider *provider, const char *name, const char *msg)
{ {
@ -92,6 +92,23 @@ void decor_providers_invoke_spell(win_T *wp, int start_row, int start_col, int e
} }
} }
void decor_providers_invoke_conceal_line(win_T *wp, int row)
{
wp->w_conceal_line_buf = wp->w_buffer;
for (size_t i = 0; i < kv_size(decor_providers); i++) {
DecorProvider *p = &kv_A(decor_providers, i);
if (p->state != kDecorProviderDisabled && p->conceal_line != LUA_NOREF) {
MAXSIZE_TEMP_ARRAY(args, 4);
ADD_C(args, INTEGER_OBJ(wp->handle));
ADD_C(args, INTEGER_OBJ(wp->w_buffer->handle));
ADD_C(args, INTEGER_OBJ(row));
if (decor_provider_invoke((int)i, "conceal_line", p->conceal_line, args, false)) {
wp->w_conceal_line_provider = true;
}
}
}
}
/// For each provider invoke the 'start' callback /// For each provider invoke the 'start' callback
/// ///
/// @param[out] providers Decoration providers /// @param[out] providers Decoration providers
@ -262,6 +279,12 @@ void decor_provider_clear(DecorProvider *p)
NLUA_CLEAR_REF(p->redraw_line); NLUA_CLEAR_REF(p->redraw_line);
NLUA_CLEAR_REF(p->redraw_end); NLUA_CLEAR_REF(p->redraw_end);
NLUA_CLEAR_REF(p->spell_nav); NLUA_CLEAR_REF(p->spell_nav);
if (p->conceal_line != LUA_NOREF) {
NLUA_CLEAR_REF(p->conceal_line);
FOR_ALL_TAB_WINDOWS(tp, wp) {
wp->w_conceal_line_buf = NULL; // invoke at least once
}
}
p->state = kDecorProviderDisabled; p->state = kDecorProviderDisabled;
} }

View File

@ -1152,14 +1152,16 @@ describe('treesitter highlighting (markdown)', function()
}) })
end) end)
it('works with spellchecked and smoothscrolled topline', function() local code_block = [[
insert([[
- $f(0)=\sum_{k=1}^{\infty}\frac{2}{\pi^{2}k^{2}}+\lim_{w \to 0}x$. - $f(0)=\sum_{k=1}^{\infty}\frac{2}{\pi^{2}k^{2}}+\lim_{w \to 0}x$.
```c ```c
printf('Hello World!'); printf('Hello World!');
``` ```
]]) ]]
it('works with spellchecked and smoothscrolled topline', function()
insert(code_block)
command('set spell smoothscroll') command('set spell smoothscroll')
feed('gg<C-E>') feed('gg<C-E>')
screen:add_extra_attr_ids({ [100] = { undercurl = true, special = Screen.colors.Red } }) screen:add_extra_attr_ids({ [100] = { undercurl = true, special = Screen.colors.Red } })
@ -1174,6 +1176,105 @@ printf('Hello World!');
]], ]],
}) })
end) end)
it('works with concealed lines', function()
insert(code_block)
screen:expect({
grid = [[
|
{18:```}{15:c} |
{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{18:```} |
^ |
|
]],
})
feed('ggj')
command('set number conceallevel=3')
screen:expect({
grid = [[
{8: 1 }{16:- }$f(0)=\sum_{k=1}^{\infty}\frac{2}{|
{8: }\pi^{2}k^{2}}+\lim_{w \to 0}x$. |
{8: 2 }^ |
{8: 4 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8: 6 } |
|
]],
})
feed('j')
screen:expect({
grid = [[
{8: 1 }{16:- }$f(0)=\sum_{k=1}^{\infty}\frac{2}{|
{8: }\pi^{2}k^{2}}+\lim_{w \to 0}x$. |
{8: 2 } |
{8: 3 }{18:^```}{15:c} |
{8: 4 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
|
]],
})
feed('j')
screen:expect({
grid = [[
{8: 1 }{16:- }$f(0)=\sum_{k=1}^{\infty}\frac{2}{|
{8: }\pi^{2}k^{2}}+\lim_{w \to 0}x$. |
{8: 2 } |
{8: 4 }{25:^printf}{16:(}{26:'Hello World!'}{16:);} |
{8: 6 } |
|
]],
})
feed('j')
screen:expect({
grid = [[
{8: 1 }{16:- }$f(0)=\sum_{k=1}^{\infty}\frac{2}{|
{8: }\pi^{2}k^{2}}+\lim_{w \to 0}x$. |
{8: 2 } |
{8: 4 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8: 5 }{18:^```} |
|
]],
})
-- Concealed lines highlight until changed botline
screen:try_resize(screen._width, 16)
feed('y3k30P:<Esc><C-F><C-B>')
screen:expect([[
{8: 1 }{16:- }$f(0)=\sum_{k=1}^{\infty}\frac{2}{|
{8: }\pi^{2}k^{2}}+\lim_{w \to 0}x$. |
{8: 2 } |
{8: 4 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8: 6 } |
{8: 8 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8: 10 } |
{8: 12 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8: 14 } |
{8: 16 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8: 18 } |
{8: 20 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8: 22 } |
{8: 24 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8: 25 }{18:^```} |
|
]])
feed('G')
screen:expect([[
{8: 98 } |
{8:100 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8:102 } |
{8:104 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8:106 } |
{8:108 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8:110 } |
{8:112 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8:114 } |
{8:116 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8:118 } |
{8:120 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8:122 } |
{8:124 }{25:printf}{16:(}{26:'Hello World!'}{16:);} |
{8:126 } ^ |
|
]])
end)
end) end)
it('starting and stopping treesitter highlight in init.lua works #29541', function() it('starting and stopping treesitter highlight in init.lua works #29541', function()