Compare commits

...

19 Commits

Author SHA1 Message Date
Artem
2ba46fcdd8
Merge 35cea0b28d into 02bc40c194 2024-12-19 01:09:56 +01:00
zeertzjq
02bc40c194
vim-patch:9.1.0945: ComplMatchIns highlight doesn't end after inserted text (#31628)
Problem:  ComplMatchIns highlight doesn't end after inserted text.
Solution: Handle ComplMatchIns highlight more like search highlight.
          Fix off-by-one error. Handle deleting text properly.
          (zeertzjq)

closes: vim/vim#16244

f25d8f9312
2024-12-18 23:59:03 +00:00
luukvbaal
160cbd0ef4
test(cursor_spec): global highlight definitions (#31613) 2024-12-18 19:06:16 +00:00
vanaigr
35cea0b28d refactor: cleanup 2024-12-18 12:10:50 -06:00
vanaigr
b037af3f2c refactor: use function in exec_lua 2024-12-18 12:10:21 -06:00
Gregory Anders
3db3947b0e
fix(terminal): restore cursor from 'guicursor' on TermLeave (#31620)
Fixes: https://github.com/neovim/neovim/issues/31612
2024-12-18 11:41:05 -06:00
vanaigr
fab5cbad00 test: add on_range test 2024-12-14 23:01:16 -06:00
vanaigr
a61c1eee21 docs: add docs for on_range() 2024-12-14 23:01:16 -06:00
vanaigr
be6b7cdc5c feat: on_range bounds are always on the first byte 2024-12-14 23:01:16 -06:00
vanaigr
e007e977d2 docs: update news 2024-12-14 23:01:16 -06:00
vanaigr
7d2d0a5549 test: test iter_captures columns 2024-12-14 23:01:16 -06:00
vanaigr
1e42a0e748 docs: generate docs 2024-12-14 23:01:16 -06:00
vanaigr
fb58bc8da5 refactor: improve API 2024-12-14 23:01:16 -06:00
vanaigr
b730a4c625 fix: make range estimation more optimal 2024-12-14 23:01:16 -06:00
vanaigr
f94e98556e fix: invalidate line after calling decor providers 2024-12-14 23:01:16 -06:00
vanaigr
112a5cf679 feat: approximate decor provider range 2024-12-14 23:01:16 -06:00
vanaigr
d43690d82a fix: check columns in root bounds 2024-12-14 23:01:16 -06:00
vanaigr
260b4bb3a5 perf: add on_range in treesitter highlighting 2024-12-14 23:01:16 -06:00
vanaigr
d2ce9e21c8 test: add treesitter long lines benchmark 2024-12-14 23:01:16 -06:00
27 changed files with 897 additions and 212 deletions

View File

@ -60,7 +60,7 @@ hi('PmenuMatch', { link = 'Pmenu' })
hi('PmenuMatchSel', { link = 'PmenuSel' }) hi('PmenuMatchSel', { link = 'PmenuSel' })
hi('PmenuExtra', { link = 'Pmenu' }) hi('PmenuExtra', { link = 'Pmenu' })
hi('PmenuExtraSel', { link = 'PmenuSel' }) hi('PmenuExtraSel', { link = 'PmenuSel' })
hi('ComplMatchIns', { link = 'Normal' }) hi('ComplMatchIns', {})
hi('Substitute', { link = 'Search' }) hi('Substitute', { link = 'Search' })
hi('Whitespace', { link = 'NonText' }) hi('Whitespace', { link = 'NonText' })
hi('MsgSeparator', { link = 'StatusLine' }) hi('MsgSeparator', { link = 'StatusLine' })

View File

@ -2823,11 +2823,12 @@ nvim_set_decoration_provider({ns_id}, {opts})
Note: this function should not be called often. Rather, the callbacks Note: this function should not be called often. Rather, the callbacks
themselves can be used to throttle unneeded callbacks. the `on_start` themselves can be used to throttle unneeded callbacks. the `on_start`
callback can return `false` to disable the provider until the next redraw. callback can return `false` to disable the provider until the next redraw.
Similarly, return `false` in `on_win` will skip the `on_line` calls for Similarly, return `false` in `on_win` will skip the `on_line` and
that window (but any extmarks set in `on_win` will still be used). A `on_range` calls for that window (but any extmarks set in `on_win` will
plugin managing multiple sources of decoration should ideally only set one still be used). A plugin managing multiple sources of decoration should
provider, and merge the sources internally. You can use multiple `ns_id` ideally only set one provider, and merge the sources internally. You can
for the extmarks set/modified inside the callback anyway. use multiple `ns_id` for the extmarks set/modified inside the callback
anyway.
Note: doing anything other than setting extmarks is considered Note: doing anything other than setting extmarks is considered
experimental. Doing things like changing options are not explicitly experimental. Doing things like changing options are not explicitly
@ -2835,8 +2836,8 @@ nvim_set_decoration_provider({ns_id}, {opts})
consumption). Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is consumption). Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is
quite dubious for the moment. quite dubious for the moment.
Note: It is not allowed to remove or update extmarks in `on_line` Note: It is not allowed to remove or update extmarks in `on_line` and
callbacks. `on_range` callbacks.
Attributes: ~ Attributes: ~
Lua |vim.api| only Lua |vim.api| only
@ -2857,6 +2858,14 @@ nvim_set_decoration_provider({ns_id}, {opts})
• on_line: called for each buffer line being redrawn. (The • on_line: called for each buffer line being redrawn. (The
interaction with fold lines is subject to change) > interaction with fold lines is subject to change) >
["line", winid, bufnr, row] ["line", winid, bufnr, row]
<
• on_range: called for each buffer range being redrawn. Range
is end-exclusive and may span multiple lines. Range bounds
point to the first byte of a character. An end position of
the form (lnum, 0), including (number of lines, 0), is
valid and indicates that EOL of the preceding line is
included. >
["range", winid, bufnr, begin_row, begin_col, end_row, end_col]
< <
• on_end: called at the end of a redraw cycle > • on_end: called at the end of a redraw cycle >
["end", tick] ["end", tick]

View File

@ -178,6 +178,7 @@ API
• |nvim__ns_set()| can set properties for a namespace • |nvim__ns_set()| can set properties for a namespace
• |vim.json.encode()| has an option to enable forward slash escaping • |vim.json.encode()| has an option to enable forward slash escaping
• Decoration provider has `on_range()` callback.
DEFAULTS DEFAULTS
@ -303,6 +304,7 @@ TREESITTER
• |treesitter-directive-trim!| can trim all whitespace (not just empty lines) • |treesitter-directive-trim!| can trim all whitespace (not just empty lines)
from both sides of a node. from both sides of a node.
• |vim.treesitter.get_captures_at_pos()| now returns the `id` of each capture • |vim.treesitter.get_captures_at_pos()| now returns the `id` of each capture
• |Query:iter_captures()| supports specifying starting and ending columns.
TUI TUI

View File

@ -1348,7 +1348,7 @@ parse({lang}, {query}) *vim.treesitter.query.parse()*
• |vim.treesitter.query.get()| • |vim.treesitter.query.get()|
*Query:iter_captures()* *Query:iter_captures()*
Query:iter_captures({node}, {source}, {start}, {stop}) Query:iter_captures({node}, {source}, {start}, {stop}, {opts})
Iterate over all captures from all matches inside {node} Iterate over all captures from all matches inside {node}
{source} is needed if the query contains predicates; then the caller must {source} is needed if the query contains predicates; then the caller must
@ -1382,9 +1382,11 @@ Query:iter_captures({node}, {source}, {start}, {stop})
`node:start()`. `node:start()`.
• {stop} (`integer?`) Stopping line for the search (end-exclusive). • {stop} (`integer?`) Stopping line for the search (end-exclusive).
Defaults to `node:end_()`. Defaults to `node:end_()`.
• {opts} (`{ col_begin?: integer, col_end?: integer }?`) Optional
parameters.
Return: ~ Return: ~
(`fun(end_line: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch`) (`fun(end_line: integer?, end_col: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch`)
capture id, capture node, metadata, match capture id, capture node, metadata, match
*Query:iter_matches()* *Query:iter_matches()*

View File

@ -2114,8 +2114,8 @@ function vim.api.nvim_set_current_win(window) end
--- Note: this function should not be called often. Rather, the callbacks --- Note: this function should not be called often. Rather, the callbacks
--- themselves can be used to throttle unneeded callbacks. the `on_start` --- themselves can be used to throttle unneeded callbacks. the `on_start`
--- callback can return `false` to disable the provider until the next redraw. --- callback can return `false` to disable the provider until the next redraw.
--- Similarly, return `false` in `on_win` will skip the `on_line` calls --- Similarly, return `false` in `on_win` will skip the `on_line` and `on_range`
--- for that window (but any extmarks set in `on_win` will still be used). --- calls for that window (but any extmarks set in `on_win` will still be used).
--- A plugin managing multiple sources of decoration should ideally only set --- A plugin managing multiple sources of decoration should ideally only set
--- one provider, and merge the sources internally. You can use multiple `ns_id` --- one provider, and merge the sources internally. You can use multiple `ns_id`
--- for the extmarks set/modified inside the callback anyway. --- for the extmarks set/modified inside the callback anyway.
@ -2126,7 +2126,8 @@ function vim.api.nvim_set_current_win(window) end
--- Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious --- Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious
--- for the moment. --- for the moment.
--- ---
--- Note: It is not allowed to remove or update extmarks in `on_line` callbacks. --- Note: It is not allowed to remove or update extmarks
--- in `on_line` and `on_range` callbacks.
--- ---
--- @param ns_id integer Namespace id from `nvim_create_namespace()` --- @param ns_id integer Namespace id from `nvim_create_namespace()`
--- @param opts vim.api.keyset.set_decoration_provider Table of callbacks: --- @param opts vim.api.keyset.set_decoration_provider Table of callbacks:
@ -2148,6 +2149,14 @@ function vim.api.nvim_set_current_win(window) end
--- ``` --- ```
--- ["line", winid, bufnr, row] --- ["line", winid, bufnr, row]
--- ``` --- ```
--- - on_range: called for each buffer range being redrawn.
--- Range is end-exclusive and may span multiple lines. Range
--- bounds point to the first byte of a character. An end position
--- of the form (lnum, 0), including (number of lines, 0), is valid
--- and indicates that EOL of the preceding line is included.
--- ```
--- ["range", winid, bufnr, begin_row, begin_col, end_row, end_col]
--- ```
--- - on_end: called at the end of a redraw cycle --- - on_end: called at the end of a redraw cycle
--- ``` --- ```
--- ["end", tick] --- ["end", tick]

View File

@ -231,6 +231,7 @@ error('Cannot require a meta file')
--- @field on_buf? fun(_: "buf", bufnr: integer, tick: integer) --- @field on_buf? fun(_: "buf", bufnr: integer, tick: integer)
--- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer) --- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer)
--- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer) --- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer)
--- @field on_range? fun(_: "range", winid: integer, bufnr: integer, row_begin: integer, col_begin: integer, row_end: integer, col_end: integer)
--- @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")

View File

@ -71,8 +71,6 @@ function TSQueryCursor:next_match() end
--- @param node TSNode --- @param node TSNode
--- @param query TSQuery --- @param query TSQuery
--- @param start integer? --- @param opts? { row_begin?: integer, col_begin?: integer, row_end?: integer, col_end?: integer, max_start_depth?: integer, match_limit?: integer }
--- @param stop integer?
--- @param opts? { max_start_depth?: integer, match_limit?: integer}
--- @return TSQueryCursor --- @return TSQueryCursor
function vim._create_ts_querycursor(node, query, start, stop, opts) end function vim._create_ts_querycursor(node, query, opts) end

View File

@ -1,10 +1,13 @@
local api = vim.api local api = vim.api
local query = vim.treesitter.query local query = vim.treesitter.query
local Range = require('vim.treesitter._range') local Range = require('vim.treesitter._range')
local cmp_eq = Range.cmp_pos.eq
local cmp_lt = Range.cmp_pos.lt
local cmp_lte = Range.cmp_pos.le
local ns = api.nvim_create_namespace('treesitter/highlighter') local ns = api.nvim_create_namespace('treesitter/highlighter')
---@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch ---@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil, end_col: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch
---@class (private) vim.treesitter.highlighter.Query ---@class (private) vim.treesitter.highlighter.Query
---@field private _query vim.treesitter.Query? ---@field private _query vim.treesitter.Query?
@ -55,6 +58,7 @@ end
---@class (private) vim.treesitter.highlighter.State ---@class (private) vim.treesitter.highlighter.State
---@field tstree TSTree ---@field tstree TSTree
---@field next_row integer ---@field next_row integer
---@field next_col integer
---@field iter vim.treesitter.highlighter.Iter? ---@field iter vim.treesitter.highlighter.Iter?
---@field highlighter_query vim.treesitter.highlighter.Query ---@field highlighter_query vim.treesitter.highlighter.Query
@ -197,6 +201,7 @@ function TSHighlighter:prepare_highlight_states(srow, erow)
table.insert(self._highlight_states, { table.insert(self._highlight_states, {
tstree = tstree, tstree = tstree,
next_row = 0, next_row = 0,
next_col = 0,
iter = nil, iter = nil,
highlighter_query = highlighter_query, highlighter_query = highlighter_query,
}) })
@ -278,86 +283,119 @@ end
---@param self vim.treesitter.highlighter ---@param self vim.treesitter.highlighter
---@param buf integer ---@param buf integer
---@param line integer ---@param range_br integer
---@param range_bc integer
---@param range_er integer
---@param range_ec integer
---@param is_spell_nav boolean ---@param is_spell_nav boolean
local function on_line_impl(self, buf, line, is_spell_nav) local function on_range_impl(self, buf, range_br, range_bc, range_er, range_ec, is_spell_nav)
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_br, root_bc, root_er, root_ec = root_node:range()
-- Only consider trees that contain this line --- @type boolean
if root_start_row > line or root_end_row < line then local root_intersects
local range_empty = cmp_eq(range_br, range_bc, range_er, range_ec)
local root_empty = cmp_eq(root_br, root_bc, root_er, root_ec)
if not range_empty and not root_empty then
root_intersects = cmp_lt(range_br, range_bc, root_er, root_ec)
and cmp_lt(root_br, root_bc, range_er, range_ec)
else
root_intersects = cmp_lte(range_br, range_bc, root_er, root_ec)
and cmp_lte(root_br, root_bc, range_er, range_ec)
end
-- Only consider trees within the visible range
if not root_intersects then
return return
end end
if state.iter == nil or state.next_row < line then local next_row = state.next_row
local next_col = state.next_col
if state.iter == nil or cmp_lt(next_row, next_col, range_br, range_bc) then
-- Mainly used to skip over folds -- Mainly used to skip over folds
-- TODO(lewis6991): Creating a new iterator loses the cached predicate results for query -- TODO(lewis6991): Creating a new iterator loses the cached predicate results for query
-- matches. Move this logic inside iter_captures() so we can maintain the cache. -- matches. Move this logic inside iter_captures() so we can maintain the cache.
state.iter = state.iter = state.highlighter_query:query():iter_captures(
state.highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1) root_node,
self.bufnr,
range_br,
root_er,
{ col_begin = range_bc, col_end = root_ec }
)
end end
while line >= state.next_row do while cmp_lt(next_row, next_col, range_er, range_ec) do
local capture, node, metadata, match = state.iter(line) local capture, node, metadata, match = state.iter(range_er, range_ec)
if not node then
local range = { root_end_row + 1, 0, root_end_row + 1, 0 } next_row = math.huge
if node then next_col = math.huge
range = vim.treesitter.get_range(node, buf, metadata and metadata[capture]) break
end end
local range = vim.treesitter.get_range(node, buf, metadata and metadata[capture])
local start_row, start_col, end_row, end_col = Range.unpack4(range) local start_row, start_col, end_row, end_col = Range.unpack4(range)
if capture then if cmp_lt(next_row, next_col, start_row, start_col) then
local hl = state.highlighter_query:get_hl_from_capture(capture) next_row = start_row
next_col = start_col
local capture_name = state.highlighter_query:query().captures[capture]
local spell, spell_pri_offset = get_spell(capture_name)
-- The "priority" attribute can be set at the pattern level or on a particular capture
local priority = (
tonumber(metadata.priority or metadata[capture] and metadata[capture].priority)
or vim.hl.priorities.treesitter
) + spell_pri_offset
-- The "conceal" attribute can be set at the pattern level or on a particular capture
local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal
local url = get_url(match, buf, capture, metadata)
if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then
api.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
hl_group = hl,
ephemeral = true,
priority = priority,
conceal = conceal,
spell = spell,
url = url,
})
end
end end
if start_row > line then if not capture then
state.next_row = start_row break
end
local hl = state.highlighter_query:get_hl_from_capture(capture)
local capture_name = state.highlighter_query:query().captures[capture]
local spell, spell_pri_offset = get_spell(capture_name)
-- The "priority" attribute can be set at the pattern level or on a particular capture
local priority = (
tonumber(metadata.priority or metadata[capture] and metadata[capture].priority)
or vim.hl.priorities.treesitter
) + spell_pri_offset
-- The "conceal" attribute can be set at the pattern level or on a particular capture
local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal
local url = get_url(match, buf, capture, metadata)
if not is_spell_nav or spell ~= nil then
api.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
ephemeral = true,
priority = priority,
hl_group = hl,
conceal = conceal,
spell = spell,
url = url,
})
end end
end end
state.next_row = next_row
state.next_col = next_col
end) end)
end end
---@private ---@private
---@param _win integer ---@param _win integer
---@param buf integer ---@param buf integer
---@param line integer ---@param br integer
function TSHighlighter._on_line(_, _win, buf, line, _) ---@param bc integer
---@param er integer
---@param ec integer
function TSHighlighter._on_range(_, _win, buf, br, bc, er, ec, _)
local self = TSHighlighter.active[buf] local self = TSHighlighter.active[buf]
if not self then if not self then
return return
end end
on_line_impl(self, buf, line, false) on_range_impl(self, buf, br, bc, er, ec, false)
end end
---@private ---@private
@ -375,9 +413,7 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
local highlight_states = self._highlight_states local highlight_states = self._highlight_states
self:prepare_highlight_states(srow, erow) self:prepare_highlight_states(srow, erow)
for row = srow, erow do on_range_impl(self, buf, srow, 0, erow, 0, true)
on_line_impl(self, buf, row, true)
end
self._highlight_states = highlight_states self._highlight_states = highlight_states
end end
@ -399,7 +435,7 @@ 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_range = TSHighlighter._on_range,
_on_spell_nav = TSHighlighter._on_spell_nav, _on_spell_nav = TSHighlighter._on_spell_nav,
}) })

View File

@ -1,6 +1,7 @@
local api = vim.api local api = vim.api
local language = require('vim.treesitter.language') local language = require('vim.treesitter.language')
local memoize = vim.func._memoize local memoize = vim.func._memoize
local cmp_ge = require('vim.treesitter._range').cmp_pos.ge
local M = {} local M = {}
@ -875,24 +876,33 @@ end
---@param source (integer|string) Source buffer or string to extract text from ---@param source (integer|string) Source buffer or string to extract text from
---@param start? integer Starting line for the search. Defaults to `node:start()`. ---@param start? integer Starting line for the search. Defaults to `node:start()`.
---@param stop? integer Stopping line for the search (end-exclusive). Defaults to `node:end_()`. ---@param stop? integer Stopping line for the search (end-exclusive). Defaults to `node:end_()`.
---@param opts? { col_begin?: integer, col_end?: integer } Optional parameters.
--- ---
---@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch): ---@return (fun(end_line: integer|nil, end_col: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch):
--- capture id, capture node, metadata, match --- capture id, capture node, metadata, match
--- ---
---@note Captures are only returned if the query pattern of a specific capture contained predicates. ---@note Captures are only returned if the query pattern of a specific capture contained predicates.
function Query:iter_captures(node, source, start, stop) function Query:iter_captures(node, source, start, stop, opts)
if type(source) == 'number' and source == 0 then if type(source) == 'number' and source == 0 then
source = api.nvim_get_current_buf() source = api.nvim_get_current_buf()
end end
opts = opts or {}
start, stop = value_or_node_range(start, stop, node) start, stop = value_or_node_range(start, stop, node)
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, { match_limit = 256 }) local cursor_opts = {
row_begin = start,
col_begin = opts.col_begin,
row_end = stop,
col_end = opts.col_end,
match_limit = 256,
}
local cursor = vim._create_ts_querycursor(node, self.query, cursor_opts)
local apply_directives = memoize(match_id_hash, self.apply_directives, true) local apply_directives = memoize(match_id_hash, self.apply_directives, true)
local match_preds = memoize(match_id_hash, self.match_preds, true) local match_preds = memoize(match_id_hash, self.match_preds, true)
local function iter(end_line) local function iter(end_line, end_col)
local capture, captured_node, match = cursor:next_capture() local capture, captured_node, match = cursor:next_capture()
if not capture then if not capture then
@ -902,10 +912,11 @@ function Query:iter_captures(node, source, start, stop)
if not match_preds(self, match, source) then if not match_preds(self, match, source) then
local match_id = match:info() local match_id = match:info()
cursor:remove_match(match_id) cursor:remove_match(match_id)
if end_line and captured_node:range() > end_line then local row, col = captured_node:range()
if end_line and cmp_ge(row, col, end_line, end_col or 0) then
return nil, captured_node, nil, nil return nil, captured_node, nil, nil
end end
return iter(end_line) -- tail call: try next match return iter(end_line, end_col) -- tail call: try next match
end end
local metadata = apply_directives(self, match, source) local metadata = apply_directives(self, match, source)
@ -961,8 +972,12 @@ function Query:iter_matches(node, source, start, stop, opts)
end end
start, stop = value_or_node_range(start, stop, node) start, stop = value_or_node_range(start, stop, node)
opts.row_begin = start
opts.col_begin = 0
opts.row_end = stop
opts.col_end = 0
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts) local cursor = vim._create_ts_querycursor(node, self.query, opts)
local function iter() local function iter()
local match = cursor:next_match() local match = cursor:next_match()

View File

@ -992,8 +992,8 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start,
/// Note: this function should not be called often. Rather, the callbacks /// Note: this function should not be called often. Rather, the callbacks
/// themselves can be used to throttle unneeded callbacks. the `on_start` /// themselves can be used to throttle unneeded callbacks. the `on_start`
/// callback can return `false` to disable the provider until the next redraw. /// callback can return `false` to disable the provider until the next redraw.
/// Similarly, return `false` in `on_win` will skip the `on_line` calls /// Similarly, return `false` in `on_win` will skip the `on_line` and `on_range`
/// for that window (but any extmarks set in `on_win` will still be used). /// calls for that window (but any extmarks set in `on_win` will still be used).
/// A plugin managing multiple sources of decoration should ideally only set /// A plugin managing multiple sources of decoration should ideally only set
/// one provider, and merge the sources internally. You can use multiple `ns_id` /// one provider, and merge the sources internally. You can use multiple `ns_id`
/// for the extmarks set/modified inside the callback anyway. /// for the extmarks set/modified inside the callback anyway.
@ -1004,7 +1004,8 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start,
/// Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious /// Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious
/// for the moment. /// for the moment.
/// ///
/// Note: It is not allowed to remove or update extmarks in `on_line` callbacks. /// Note: It is not allowed to remove or update extmarks
/// in `on_line` and `on_range` callbacks.
/// ///
/// @param ns_id Namespace id from |nvim_create_namespace()| /// @param ns_id Namespace id from |nvim_create_namespace()|
/// @param opts Table of callbacks: /// @param opts Table of callbacks:
@ -1026,6 +1027,14 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start,
/// ``` /// ```
/// ["line", winid, bufnr, row] /// ["line", winid, bufnr, row]
/// ``` /// ```
/// - on_range: called for each buffer range being redrawn.
/// Range is end-exclusive and may span multiple lines. Range
/// bounds point to the first byte of a character. An end position
/// of the form (lnum, 0), including (number of lines, 0), is valid
/// and indicates that EOL of the preceding line is included.
/// ```
/// ["range", winid, bufnr, begin_row, begin_col, end_row, end_col]
/// ```
/// - on_end: called at the end of a redraw cycle /// - on_end: called at the end of a redraw cycle
/// ``` /// ```
/// ["end", tick] /// ["end", tick]
@ -1049,6 +1058,7 @@ void nvim_set_decoration_provider(Integer ns_id, Dict(set_decoration_provider) *
{ "on_buf", &opts->on_buf, &p->redraw_buf }, { "on_buf", &opts->on_buf, &p->redraw_buf },
{ "on_win", &opts->on_win, &p->redraw_win }, { "on_win", &opts->on_win, &p->redraw_win },
{ "on_line", &opts->on_line, &p->redraw_line }, { "on_line", &opts->on_line, &p->redraw_line },
{ "on_range", &opts->on_range, &p->redraw_range },
{ "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 },

View File

@ -17,6 +17,8 @@ typedef struct {
LuaRefOf(("buf" _, Integer bufnr, Integer tick)) on_buf; LuaRefOf(("buf" _, Integer bufnr, Integer tick)) on_buf;
LuaRefOf(("win" _, Integer winid, Integer bufnr, Integer toprow, Integer botrow)) on_win; LuaRefOf(("win" _, Integer winid, Integer bufnr, Integer toprow, Integer botrow)) on_win;
LuaRefOf(("line" _, Integer winid, Integer bufnr, Integer row)) on_line; LuaRefOf(("line" _, Integer winid, Integer bufnr, Integer row)) on_line;
LuaRefOf(("range" _, Integer winid, Integer bufnr, Integer row_begin, Integer col_begin,
Integer row_end, Integer col_end)) on_range;
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;

View File

@ -140,6 +140,7 @@ typedef struct {
LuaRef redraw_buf; LuaRef redraw_buf;
LuaRef redraw_win; LuaRef redraw_win;
LuaRef redraw_line; LuaRef redraw_line;
LuaRef redraw_range;
LuaRef redraw_end; LuaRef redraw_end;
LuaRef hl_def; LuaRef hl_def;
LuaRef spell_nav; LuaRef spell_nav;

View File

@ -29,7 +29,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, \
LUA_NOREF, -1, false, false, 0 } 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)
@ -178,6 +178,32 @@ void decor_providers_invoke_line(win_T *wp, int row, bool *has_decor)
decor_state.running_decor_provider = false; decor_state.running_decor_provider = false;
} }
void decor_providers_invoke_range(win_T *wp, int sr, int sc, int er, int ec, bool *has_decor)
{
decor_state.running_decor_provider = true;
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) {
MAXSIZE_TEMP_ARRAY(args, 6);
ADD_C(args, WINDOW_OBJ(wp->handle));
ADD_C(args, BUFFER_OBJ(wp->w_buffer->handle));
ADD_C(args, INTEGER_OBJ(sr));
ADD_C(args, INTEGER_OBJ(sc));
ADD_C(args, INTEGER_OBJ(er));
ADD_C(args, INTEGER_OBJ(ec));
if (decor_provider_invoke((int)i, "range", p->redraw_range, args, true)) {
*has_decor = true;
} else {
// return 'false' or error: skip rest of this window
kv_A(decor_providers, i).state = kDecorProviderWinDisabled;
}
hl_check_ns();
}
}
decor_state.running_decor_provider = false;
}
/// For each provider invoke the 'buf' callback for a given buffer. /// For each provider invoke the 'buf' callback for a given buffer.
/// ///
/// @param buf Buffer /// @param buf Buffer
@ -262,6 +288,7 @@ void decor_provider_clear(DecorProvider *p)
NLUA_CLEAR_REF(p->redraw_buf); NLUA_CLEAR_REF(p->redraw_buf);
NLUA_CLEAR_REF(p->redraw_win); NLUA_CLEAR_REF(p->redraw_win);
NLUA_CLEAR_REF(p->redraw_line); NLUA_CLEAR_REF(p->redraw_line);
NLUA_CLEAR_REF(p->redraw_range);
NLUA_CLEAR_REF(p->redraw_end); NLUA_CLEAR_REF(p->redraw_end);
NLUA_CLEAR_REF(p->spell_nav); NLUA_CLEAR_REF(p->spell_nav);
p->state = kDecorProviderDisabled; p->state = kDecorProviderDisabled;

View File

@ -955,7 +955,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
bool area_highlighting = false; // Visual or incsearch highlighting in this line bool area_highlighting = false; // Visual or incsearch highlighting in this line
int vi_attr = 0; // attributes for Visual and incsearch highlighting int vi_attr = 0; // attributes for Visual and incsearch highlighting
int area_attr = 0; // attributes desired by highlighting int area_attr = 0; // attributes desired by highlighting
int search_attr = 0; // attributes desired by 'hlsearch' int search_attr = 0; // attributes desired by 'hlsearch' or ComplMatchIns
int vcol_save_attr = 0; // saved attr for 'cursorcolumn' int vcol_save_attr = 0; // saved attr for 'cursorcolumn'
int decor_attr = 0; // attributes desired by syntax and extmarks int decor_attr = 0; // attributes desired by syntax and extmarks
bool has_syntax = false; // this buffer has syntax highl. bool has_syntax = false; // this buffer has syntax highl.
@ -969,7 +969,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
int spell_attr = 0; // attributes desired by spelling int spell_attr = 0; // attributes desired by spelling
int word_end = 0; // last byte with same spell_attr int word_end = 0; // last byte with same spell_attr
int cur_checked_col = 0; // checked column for current line int cur_checked_col = 0; // checked column for current line
bool extra_check = 0; // has syntax or linebreak bool extra_check = false; // has extra highlighting
int multi_attr = 0; // attributes desired by multibyte int multi_attr = 0; // attributes desired by multibyte
int mb_l = 1; // multi-byte byte length int mb_l = 1; // multi-byte byte length
int mb_c = 0; // decoded multi-byte character int mb_c = 0; // decoded multi-byte character
@ -1029,6 +1029,9 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
buf_T *buf = wp->w_buffer; buf_T *buf = wp->w_buffer;
const bool end_fill = (lnum == buf->b_ml.ml_line_count + 1); const bool end_fill = (lnum == buf->b_ml.ml_line_count + 1);
int decor_provider_end_col;
bool check_decor_providers = false;
if (col_rows == 0) { if (col_rows == 0) {
// To speed up the loop below, set extra_check when there is linebreak, // To speed up the loop below, set extra_check when there is linebreak,
// trailing white space and/or syntax processing to be done. // trailing white space and/or syntax processing to be done.
@ -1054,11 +1057,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
has_decor = decor_redraw_line(wp, lnum - 1, &decor_state); has_decor = decor_redraw_line(wp, lnum - 1, &decor_state);
if (!end_fill) { if (!end_fill) {
decor_providers_invoke_line(wp, lnum - 1, &has_decor); check_decor_providers = true;
}
if (has_decor) {
extra_check = true;
} }
// Check for columns to display for 'colorcolumn'. // Check for columns to display for 'colorcolumn'.
@ -1468,6 +1467,35 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
} }
} }
if (check_decor_providers) {
int const col = (int)(ptr - line);
// Approximate the number of bytes that will be drawn.
// Assume we're dealing with 1-cell ascii and ignore
// the effects of 'linebreak', 'breakindent', etc.
int rem_vcols;
if (wp->w_p_wrap) {
int width = wp->w_width_inner - win_col_off(wp);
int width2 = width + win_col_off2(wp);
int first_row_width = start_col == 0 ? width : width2;
rem_vcols = first_row_width + (endrow - startrow - 1) * width2;
} else {
rem_vcols = wp->w_width_inner - win_col_off(wp);
}
// Call it here since we need to invalidate the line pointer anyway.
decor_providers_invoke_line(wp, lnum - 1, &has_decor);
decor_provider_end_col = invoke_range_next(wp, lnum, col, rem_vcols + 1, &has_decor);
line = ml_get_buf(wp->w_buffer, lnum);
ptr = line + col;
}
if (has_decor) {
extra_check = true;
}
// Correct highlighting for cursor that can't be disabled. // Correct highlighting for cursor that can't be disabled.
// Avoids having to check this for each character. // Avoids having to check this for each character.
if (wlv.fromcol >= 0) { if (wlv.fromcol >= 0) {
@ -1495,6 +1523,10 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
ptr = line + v; // "line" may have been updated ptr = line + v; // "line" may have been updated
} }
if ((State & MODE_INSERT) && in_curline && ins_compl_active()) {
area_highlighting = true;
}
win_line_start(wp, &wlv); win_line_start(wp, &wlv);
bool draw_cols = true; bool draw_cols = true;
int leftcols_width = 0; int leftcols_width = 0;
@ -1517,6 +1549,16 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
bool did_decrement_ptr = false; bool did_decrement_ptr = false;
if (check_decor_providers && (int)(ptr - line) >= decor_provider_end_col) {
int const col = (int)(ptr - line);
decor_provider_end_col = invoke_range_next(wp, lnum, col, 100, &has_decor);
line = ml_get_buf(wp->w_buffer, lnum);
ptr = line + col;
}
if (has_decor) {
extra_check = true;
}
// Skip this quickly when working on the text. // Skip this quickly when working on the text.
if (draw_cols) { if (draw_cols) {
if (cul_screenline) { if (cul_screenline) {
@ -1740,6 +1782,14 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
if (*ptr == NUL) { if (*ptr == NUL) {
has_match_conc = 0; has_match_conc = 0;
} }
// Check if ComplMatchIns highlight is needed.
if ((State & MODE_INSERT) && in_curline && ins_compl_active()) {
int ins_match_attr = ins_compl_col_range_attr((int)(ptr - line));
if (ins_match_attr > 0) {
search_attr = hl_combine_attr(search_attr, ins_match_attr);
}
}
} }
if (wlv.diff_hlf != (hlf_T)0) { if (wlv.diff_hlf != (hlf_T)0) {
@ -1787,16 +1837,6 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
wlv.char_attr = hl_combine_attr(char_attr_base, char_attr_pri); wlv.char_attr = hl_combine_attr(char_attr_base, char_attr_pri);
} }
// Apply ComplMatchIns highlight if needed.
if (wlv.filler_todo <= 0
&& (State & MODE_INSERT) && in_curline && ins_compl_active()) {
int ins_match_attr = ins_compl_col_range_attr((int)(ptr - line));
if (ins_match_attr > 0) {
char_attr_pri = hl_combine_attr(char_attr_pri, ins_match_attr);
wlv.char_attr = hl_combine_attr(char_attr_base, char_attr_pri);
}
}
if (draw_folded && has_foldtext && wlv.n_extra == 0 && wlv.col == win_col_offset) { if (draw_folded && has_foldtext && wlv.n_extra == 0 && wlv.col == win_col_offset) {
const int v = (int)(ptr - line); const int v = (int)(ptr - line);
linenr_T lnume = lnum + foldinfo.fi_lines - 1; linenr_T lnume = lnum + foldinfo.fi_lines - 1;
@ -2999,3 +3039,25 @@ static void wlv_put_linebuf(win_T *wp, const winlinevars_T *wlv, int endcol, boo
grid_adjust(&grid, &row, &coloff); grid_adjust(&grid, &row, &coloff);
grid_put_linebuf(grid, row, coloff, startcol, endcol, clear_width, bg_attr, wlv->vcol - 1, flags); grid_put_linebuf(grid, row, coloff, startcol, endcol, clear_width, bg_attr, wlv->vcol - 1, flags);
} }
/// @return New begin column, or INT_MAX.
static int invoke_range_next(win_T *wp, int lnum, colnr_T begin_col, colnr_T col_off,
bool *has_decor)
{
char const *const line = ml_get_buf(wp->w_buffer, lnum);
int const line_len = ml_get_buf_len(wp->w_buffer, lnum);
col_off = MAX(col_off, 1);
colnr_T new_col;
if (col_off <= line_len - begin_col) {
int end_col = begin_col + col_off;
end_col += mb_off_next(line, line + end_col);
decor_providers_invoke_range(wp, lnum - 1, begin_col, lnum - 1, end_col, has_decor);
new_col = end_col;
} else {
decor_providers_invoke_range(wp, lnum - 1, begin_col, lnum, 0, has_decor);
new_col = INT_MAX;
}
return new_col;
}

View File

@ -173,7 +173,7 @@ static const char *highlight_init_both[] = {
"default link PmenuKind Pmenu", "default link PmenuKind Pmenu",
"default link PmenuKindSel PmenuSel", "default link PmenuKindSel PmenuSel",
"default link PmenuSbar Pmenu", "default link PmenuSbar Pmenu",
"default link ComplMatchIns Normal", "default link ComplMatchIns NONE",
"default link Substitute Search", "default link Substitute Search",
"default link StatusLineTerm StatusLine", "default link StatusLineTerm StatusLine",
"default link StatusLineTermNC StatusLineNC", "default link StatusLineTermNC StatusLineNC",

View File

@ -958,7 +958,7 @@ static void ins_compl_insert_bytes(char *p, int len)
} }
assert(len >= 0); assert(len >= 0);
ins_bytes_len(p, (size_t)len); ins_bytes_len(p, (size_t)len);
compl_ins_end_col = curwin->w_cursor.col - 1; compl_ins_end_col = curwin->w_cursor.col;
} }
/// Checks if the column is within the currently inserted completion text /// Checks if the column is within the currently inserted completion text
@ -2147,6 +2147,8 @@ static bool ins_compl_stop(const int c, const int prev_mode, bool retval)
&& pum_visible()) { && pum_visible()) {
word = xstrdup(compl_shown_match->cp_str); word = xstrdup(compl_shown_match->cp_str);
retval = true; retval = true;
// May need to remove ComplMatchIns highlight.
redrawWinline(curwin, curwin->w_cursor.lnum);
} }
// CTRL-E means completion is Ended, go back to the typed text. // CTRL-E means completion is Ended, go back to the typed text.
@ -3648,6 +3650,7 @@ void ins_compl_delete(bool new_leader)
return; return;
} }
backspace_until_column(col); backspace_until_column(col);
compl_ins_end_col = curwin->w_cursor.col;
} }
// TODO(vim): is this sufficient for redrawing? Redrawing everything // TODO(vim): is this sufficient for redrawing? Redrawing everything

View File

@ -1287,40 +1287,68 @@ static struct luaL_Reg querycursor_meta[] = {
{ NULL, NULL } { NULL, NULL }
}; };
static inline bool set_uint32(lua_State *L, int idx, char const *name, uint32_t *res)
FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL
{
lua_getfield(L, idx, name);
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
return false;
}
int64_t value = lua_tointeger(L, -1);
lua_pop(L, 1);
if (value < 0U) {
*res = 0U;
} else if (value > UINT32_MAX) {
*res = UINT32_MAX;
} else {
*res = (uint32_t)value;
}
return true;
}
int tslua_push_querycursor(lua_State *L) int tslua_push_querycursor(lua_State *L)
{ {
TSNode node = node_check(L, 1); TSNode node = node_check(L, 1);
TSQuery *query = query_check(L, 2); TSQuery *query = query_check(L, 2);
TSQueryCursor *cursor = ts_query_cursor_new(); TSQueryCursor *cursor = ts_query_cursor_new();
ts_query_cursor_exec(cursor, query, node);
if (lua_gettop(L) >= 3) { if (lua_gettop(L) >= 3 && !lua_isnil(L, 3)) {
uint32_t start = (uint32_t)luaL_checkinteger(L, 3); luaL_argcheck(L, lua_istable(L, 3), 3, "table expected");
uint32_t end = lua_gettop(L) >= 4 ? (uint32_t)luaL_checkinteger(L, 4) : MAXLNUM;
ts_query_cursor_set_point_range(cursor, (TSPoint){ start, 0 }, (TSPoint){ end, 0 });
}
if (lua_gettop(L) >= 5 && !lua_isnil(L, 5)) { TSPoint begin = (TSPoint){ 0, 0 };
luaL_argcheck(L, lua_istable(L, 5), 5, "table expected"); TSPoint end = (TSPoint){ UINT32_MAX, UINT32_MAX };
lua_pushnil(L); // [dict, ..., nil]
while (lua_next(L, 5)) { if (set_uint32(L, 3, "row_begin", &begin.row)) {
// [dict, ..., key, value] if (!set_uint32(L, 3, "col_begin", &begin.column)) {
if (lua_type(L, -2) == LUA_TSTRING) { begin.column = 0;
char *k = (char *)lua_tostring(L, -2);
if (strequal("max_start_depth", k)) {
uint32_t max_start_depth = (uint32_t)lua_tointeger(L, -1);
ts_query_cursor_set_max_start_depth(cursor, max_start_depth);
} else if (strequal("match_limit", k)) {
uint32_t match_limit = (uint32_t)lua_tointeger(L, -1);
ts_query_cursor_set_match_limit(cursor, match_limit);
}
} }
// pop the value; lua_next will pop the key. }
lua_pop(L, 1); // [dict, ..., key]
if (set_uint32(L, 3, "row_end", &end.row)) {
if (!set_uint32(L, 3, "col_end", &end.column)) {
end.column = 0;
}
}
ts_query_cursor_set_point_range(cursor, begin, end);
uint32_t max_start_depth;
if (set_uint32(L, 3, "max_start_depth", &max_start_depth)) {
ts_query_cursor_set_max_start_depth(cursor, max_start_depth);
}
uint32_t match_limit;
if (set_uint32(L, 3, "match_limit", &match_limit)) {
ts_query_cursor_set_match_limit(cursor, match_limit);
} }
} }
ts_query_cursor_exec(cursor, query, node);
TSQueryCursor **ud = lua_newuserdata(L, sizeof(*ud)); // [node, query, ..., udata] TSQueryCursor **ud = lua_newuserdata(L, sizeof(*ud)); // [node, query, ..., udata]
*ud = cursor; *ud = cursor;
lua_getfield(L, LUA_REGISTRYINDEX, TS_META_QUERYCURSOR); // [node, query, ..., udata, meta] lua_getfield(L, LUA_REGISTRYINDEX, TS_META_QUERYCURSOR); // [node, query, ..., udata, meta]

View File

@ -641,9 +641,6 @@ bool terminal_enter(void)
curwin->w_p_so = 0; curwin->w_p_so = 0;
curwin->w_p_siso = 0; curwin->w_p_siso = 0;
// Save the existing cursor entry since it may be modified by the application
cursorentry_T save_cursorentry = shape_table[SHAPE_IDX_TERM];
// Update the cursor shape table and flush changes to the UI // Update the cursor shape table and flush changes to the UI
s->term->pending.cursor = true; s->term->pending.cursor = true;
refresh_cursor(s->term); refresh_cursor(s->term);
@ -674,8 +671,8 @@ bool terminal_enter(void)
RedrawingDisabled = s->save_rd; RedrawingDisabled = s->save_rd;
apply_autocmds(EVENT_TERMLEAVE, NULL, NULL, false, curbuf); apply_autocmds(EVENT_TERMLEAVE, NULL, NULL, false, curbuf);
shape_table[SHAPE_IDX_TERM] = save_cursorentry; // Restore the terminal cursor to what is set in 'guicursor'
ui_mode_info_set(); (void)parse_shape_opt(SHAPE_CURSOR);
if (save_curwin == curwin->handle) { // Else: window was closed. if (save_curwin == curwin->handle) { // Else: window was closed.
curwin->w_p_cul = save_w_p_cul; curwin->w_p_cul = save_w_p_cul;

View File

@ -6,10 +6,9 @@ describe('decor perf', function()
before_each(n.clear) before_each(n.clear)
it('can handle long lines', function() it('can handle long lines', function()
local screen = Screen.new(100, 101) Screen.new(100, 101)
screen:attach()
local result = exec_lua [==[ local result = exec_lua(function()
local ephemeral_pattern = { local ephemeral_pattern = {
{ 0, 4, 'Comment', 11 }, { 0, 4, 'Comment', 11 },
{ 0, 3, 'Keyword', 12 }, { 0, 3, 'Keyword', 12 },
@ -62,7 +61,7 @@ describe('decor perf', function()
return true return true
end, end,
on_line = function() on_line = function()
add_pattern(ephemeral_pattern, true) add_pattern(ephemeral_pattern, true)
end, end,
}) })
@ -70,16 +69,16 @@ describe('decor perf', function()
local total = {} local total = {}
local provider = {} local provider = {}
for i = 1, 100 do for _ = 1, 100 do
local tic = vim.uv.hrtime() local tic = vim.uv.hrtime()
vim.cmd'redraw!' vim.cmd 'redraw!'
local toc = vim.uv.hrtime() local toc = vim.uv.hrtime()
table.insert(total, toc - tic) table.insert(total, toc - tic)
table.insert(provider, pe - ps) table.insert(provider, pe - ps)
end end
return { total, provider } return { total, provider }
]==] end)
local total, provider = unpack(result) local total, provider = unpack(result)
table.sort(total) table.sort(total)
@ -99,4 +98,39 @@ describe('decor perf', function()
print('\nTotal ' .. fmt(total) .. '\nDecoration provider: ' .. fmt(provider)) print('\nTotal ' .. fmt(total) .. '\nDecoration provider: ' .. fmt(provider))
end) end)
it('can handle long lines with treesitter highlighting', function()
Screen.new(100, 51)
local result = exec_lua(function()
local long_line = 'local a = { ' .. ('a = 5, '):rep(2000) .. '}'
vim.api.nvim_buf_set_lines(0, 0, 0, false, { long_line })
vim.api.nvim_win_set_cursor(0, { 1, 0 })
vim.treesitter.start(0, 'lua')
local total = {}
for _ = 1, 50 do
local tic = vim.uv.hrtime()
vim.cmd 'redraw!'
local toc = vim.uv.hrtime()
table.insert(total, toc - tic)
end
return { total }
end)
local total = unpack(result)
table.sort(total)
local ms = 1 / 1000000
local res = string.format(
'min, 25%%, median, 75%%, max:\n\t%0.1fms,\t%0.1fms,\t%0.1fms,\t%0.1fms,\t%0.1fms',
total[1] * ms,
total[1 + math.floor(#total * 0.25)] * ms,
total[1 + math.floor(#total * 0.5)] * ms,
total[1 + math.floor(#total * 0.75)] * ms,
total[#total] * ms
)
print('\nTotal ' .. res)
end)
end) end)

View File

@ -1,10 +1,11 @@
local n = require('test.functional.testnvim')() local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local clear = n.clear local clear = n.clear
local exec_lua = n.exec_lua local exec_lua = n.exec_lua
describe('treesitter perf', function() describe('treesitter perf', function()
setup(function() before_each(function()
clear() clear()
end) end)
@ -47,4 +48,144 @@ describe('treesitter perf', function()
return vim.uv.hrtime() - start return vim.uv.hrtime() - start
]] ]]
end) end)
local function test_long_line(_pos, _wrap, _line, grid)
local screen = Screen.new(20, 11)
local result = exec_lua(function(...)
local pos, wrap, line = ...
vim.api.nvim_buf_set_lines(0, 0, 0, false, { line })
vim.api.nvim_win_set_cursor(0, pos)
vim.api.nvim_set_option_value('wrap', wrap, { win = 0 })
vim.treesitter.start(0, 'lua')
local total = {}
for _ = 1, 100 do
local tic = vim.uv.hrtime()
vim.cmd 'redraw!'
local toc = vim.uv.hrtime()
table.insert(total, toc - tic)
end
return { total }
end, _pos, _wrap, _line)
screen:expect({ grid = grid or '' })
local total = unpack(result)
table.sort(total)
local ms = 1 / 1000000
local res = string.format(
'min, 25%%, median, 75%%, max:\n\t%0.2fms,\t%0.2fms,\t%0.2fms,\t%0.2fms,\t%0.2fms',
total[1] * ms,
total[1 + math.floor(#total * 0.25)] * ms,
total[1 + math.floor(#total * 0.5)] * ms,
total[1 + math.floor(#total * 0.75)] * ms,
total[#total] * ms
)
print('\nTotal ' .. res)
end
local long_line = 'local a = { ' .. ('a = 5, '):rep(500) .. '}'
it('can redraw the beginning of a long line with wrapping', function()
local grid = [[
{15:^local} {25:a} {15:=} {16:{} {25:a} {15:=} {26:5}{16:,} {25:a}|
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} |
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,}|
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}|
{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} |
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=}|
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} |
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a}|
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} |
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,}|
|
]]
test_long_line({ 1, 0 }, true, long_line, grid)
end)
it('can redraw the middle of a long line with wrapping', function()
local grid = [[
{1:<<<}{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} |
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,}|
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}|
{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} |
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=}|
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} |
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a}|
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} |
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,}|
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a}^ {15:=} {26:5}|
|
]]
test_long_line({ 1, math.floor(#long_line / 2) }, true, long_line, grid)
end)
it('can redraw the end of a long line with wrapping', function()
local grid = [[
{1:<<<}{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=}|
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} |
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a}|
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} |
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,}|
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}|
{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} |
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=}|
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} |
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {16:^}} |
|
]]
test_long_line({ 1, #long_line - 1 }, true, long_line, grid)
end)
it('can redraw the beginning of a long line without wrapping', function()
local grid = [[
{15:^local} {25:a} {15:=} {16:{} {25:a} {15:=} {26:5}{16:,} {25:a}|
|
{1:~ }|*8
|
]]
test_long_line({ 1, 0 }, false, long_line, grid)
end)
it('can redraw the middle of a long line without wrapping', function()
local grid = [[
{16:,} {25:a} {15:=} {26:5}{16:,} {25:a}^ {15:=} {26:5}{16:,} {25:a} {15:=} |
|
{1:~ }|*8
|
]]
test_long_line({ 1, math.floor(#long_line / 2) }, false, long_line, grid)
end)
it('can redraw the end of a long line without wrapping', function()
local grid = [[
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {16:^}} |
|
{1:~ }|*8
|
]]
test_long_line({ 1, #long_line - 1 }, false, long_line, grid)
end)
local long_line_mb = 'local a = { ' .. ('À = 5, '):rep(500) .. '}'
it('can redraw the middle of a long line with multibyte characters', function()
local grid = [[
{1:<<<}{26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} |
{25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,}|
{25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}|
{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} |
{26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=}|
{26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} |
{15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À}|
{15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} |
{25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,}|
{25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À}^ {15:=} {26:5}|
|
]]
test_long_line({ 1, math.floor(#long_line_mb / 2) }, true, long_line_mb, grid)
end)
end) end)

View File

@ -15,9 +15,20 @@ local skip = t.skip
describe(':terminal cursor', function() describe(':terminal cursor', function()
local screen local screen
local terminal_mode_idx ---@type number
before_each(function() before_each(function()
clear() clear()
screen = tt.setup_screen() screen = tt.setup_screen()
if terminal_mode_idx == nil then
for i, v in ipairs(screen._mode_info) do
if v.name == 'terminal' then
terminal_mode_idx = i
end
end
assert(terminal_mode_idx)
end
end) end)
it('moves the screen cursor when focused', function() it('moves the screen cursor when focused', function()
@ -143,13 +154,6 @@ describe(':terminal cursor', function()
it('can be modified by application #3681', function() it('can be modified by application #3681', function()
skip(is_os('win'), '#31587') skip(is_os('win'), '#31587')
local idx ---@type number
for i, v in ipairs(screen._mode_info) do
if v.name == 'terminal' then
idx = i
end
end
assert(idx)
local states = { local states = {
[1] = { blink = true, shape = 'block' }, [1] = { blink = true, shape = 'block' },
@ -171,13 +175,13 @@ describe(':terminal cursor', function()
]], ]],
condition = function() condition = function()
if v.blink then if v.blink then
eq(500, screen._mode_info[idx].blinkon) eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff) eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
else else
eq(0, screen._mode_info[idx].blinkon) eq(0, screen._mode_info[terminal_mode_idx].blinkon)
eq(0, screen._mode_info[idx].blinkoff) eq(0, screen._mode_info[terminal_mode_idx].blinkoff)
end end
eq(v.shape, screen._mode_info[idx].cursor_shape) eq(v.shape, screen._mode_info[terminal_mode_idx].cursor_shape)
end, end,
}) })
end end
@ -191,20 +195,13 @@ describe(':terminal cursor', function()
]]) ]])
-- Cursor returns to default on TermLeave -- Cursor returns to default on TermLeave
eq(500, screen._mode_info[idx].blinkon) eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff) eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
eq('block', screen._mode_info[idx].cursor_shape) eq('block', screen._mode_info[terminal_mode_idx].cursor_shape)
end) end)
it('can be modified per terminal', function() it('can be modified per terminal', function()
skip(is_os('win'), '#31587') skip(is_os('win'), '#31587')
local idx ---@type number
for i, v in ipairs(screen._mode_info) do
if v.name == 'terminal' then
idx = i
end
end
assert(idx)
-- Set cursor to vertical bar with blink -- Set cursor to vertical bar with blink
tt.feed_csi('5 q') tt.feed_csi('5 q')
@ -216,9 +213,9 @@ describe(':terminal cursor', function()
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]], ]],
condition = function() condition = function()
eq(500, screen._mode_info[idx].blinkon) eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff) eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
eq('vertical', screen._mode_info[idx].cursor_shape) eq('vertical', screen._mode_info[terminal_mode_idx].cursor_shape)
end, end,
}) })
@ -231,9 +228,9 @@ describe(':terminal cursor', function()
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]], ]],
condition = function() condition = function()
eq(500, screen._mode_info[idx].blinkon) eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff) eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
eq('vertical', screen._mode_info[idx].cursor_shape) eq('vertical', screen._mode_info[terminal_mode_idx].cursor_shape)
end, end,
}) })
@ -256,9 +253,9 @@ describe(':terminal cursor', function()
]], ]],
condition = function() condition = function()
-- New terminal, cursor resets to defaults -- New terminal, cursor resets to defaults
eq(500, screen._mode_info[idx].blinkon) eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff) eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
eq('block', screen._mode_info[idx].cursor_shape) eq('block', screen._mode_info[terminal_mode_idx].cursor_shape)
end, end,
}) })
@ -275,9 +272,9 @@ describe(':terminal cursor', function()
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]], ]],
condition = function() condition = function()
eq(0, screen._mode_info[idx].blinkon) eq(0, screen._mode_info[terminal_mode_idx].blinkon)
eq(0, screen._mode_info[idx].blinkoff) eq(0, screen._mode_info[terminal_mode_idx].blinkoff)
eq('horizontal', screen._mode_info[idx].cursor_shape) eq('horizontal', screen._mode_info[terminal_mode_idx].cursor_shape)
end, end,
}) })
@ -294,9 +291,9 @@ describe(':terminal cursor', function()
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]], ]],
condition = function() condition = function()
eq(500, screen._mode_info[idx].blinkon) eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff) eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
eq('vertical', screen._mode_info[idx].cursor_shape) eq('vertical', screen._mode_info[terminal_mode_idx].cursor_shape)
end, end,
}) })
end) end)
@ -326,6 +323,32 @@ describe(':terminal cursor', function()
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
end) end)
it('preserves guicursor value on TermLeave #31612', function()
eq(3, screen._mode_info[terminal_mode_idx].hl_id)
-- Change 'guicursor' while terminal mode is active
command('set guicursor+=t:Error')
local error_hl_id = call('hlID', 'Error')
screen:expect({
condition = function()
eq(error_hl_id, screen._mode_info[terminal_mode_idx].hl_id)
end,
})
-- Exit terminal mode
feed([[<C-\><C-N>]])
screen:expect([[
tty ready |
^ |
|*5
]])
eq(error_hl_id, screen._mode_info[terminal_mode_idx].hl_id)
end)
end) end)
describe('buffer cursor position is correct in terminal without number column', function() describe('buffer cursor position is correct in terminal without number column', function()
@ -350,12 +373,6 @@ describe('buffer cursor position is correct in terminal without number column',
}, { }, {
cols = 70, cols = 70,
}) })
screen:set_default_attr_ids({
[1] = { foreground = 253, background = 11 },
[2] = { reverse = true },
[3] = { bold = true },
[4] = { background = 11 },
})
-- Also check for real cursor position, as it is used for stuff like input methods -- Also check for real cursor position, as it is used for stuff like input methods
screen._handle_busy_start = function() end screen._handle_busy_start = function() end
screen._handle_busy_stop = function() end screen._handle_busy_stop = function() end
@ -667,13 +684,6 @@ describe('buffer cursor position is correct in terminal with number column', fun
}, { }, {
cols = 70, cols = 70,
}) })
screen:set_default_attr_ids({
[1] = { foreground = 253, background = 11 },
[2] = { reverse = true },
[3] = { bold = true },
[4] = { background = 11 },
[7] = { foreground = 130 },
})
-- Also check for real cursor position, as it is used for stuff like input methods -- Also check for real cursor position, as it is used for stuff like input methods
screen._handle_busy_start = function() end screen._handle_busy_start = function() end
screen._handle_busy_stop = function() end screen._handle_busy_stop = function() end

View File

@ -717,6 +717,47 @@ void ui_refresh(void)
eq({ { 1, 10, 1, 13 } }, ret) eq({ { 1, 10, 1, 13 } }, ret)
end) end)
it('iter_captures supports columns', function()
local txt = [[
int aaa = 1, bbb = 2;
int foo = 1, bar = 2;
int baz = 3, qux = 4;
int ccc = 1, ddd = 2;
]]
local function test(opts)
local parser = vim.treesitter.get_string_parser(txt, 'c')
local nodes = {}
local query = vim.treesitter.query.parse('c', '((identifier) @foo)')
local root = assert(parser:parse()[1]:root())
local iter = query:iter_captures(root, txt, 1, 2, opts)
while true do
local capture, node = iter()
if not capture then
break
end
table.insert(nodes, { node:range() })
end
return nodes
end
local ret
ret = exec_lua(test, { col_begin = 7, col_end = 13 })
eq({ { 1, 13, 1, 16 }, { 2, 4, 2, 7 } }, ret)
ret = exec_lua(test, { col_begin = 7 })
eq({ { 1, 13, 1, 16 } }, ret)
ret = exec_lua(test, { col_end = 13 })
eq({ { 1, 4, 1, 7 }, { 1, 13, 1, 16 }, { 2, 4, 2, 7 } }, ret)
ret = exec_lua(test, {})
eq({ { 1, 4, 1, 7 }, { 1, 13, 1, 16 } }, ret)
end)
it('fails to load queries', function() it('fails to load queries', function()
local function test(exp, cquery) local function test(exp, cquery)
eq(exp, pcall_err(exec_lua, "vim.treesitter.query.parse('c', ...)", cquery)) eq(exp, pcall_err(exec_lua, "vim.treesitter.query.parse('c', ...)", cquery))

View File

@ -15,32 +15,36 @@ local eq = t.eq
local assert_alive = n.assert_alive local assert_alive = n.assert_alive
local pcall_err = t.pcall_err local pcall_err = t.pcall_err
local function setup_screen(screen)
screen:set_default_attr_ids {
[1] = {bold=true, foreground=Screen.colors.Blue};
[2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red};
[3] = {foreground = Screen.colors.Brown};
[4] = {foreground = Screen.colors.Blue1};
[5] = {foreground = Screen.colors.Magenta};
[6] = {bold = true, foreground = Screen.colors.Brown};
[7] = {background = Screen.colors.Gray90};
[8] = {bold = true, reverse = true};
[9] = {reverse = true};
[10] = {italic = true, background = Screen.colors.Magenta};
[11] = {foreground = Screen.colors.Red, background = tonumber('0x005028')};
[12] = {foreground = tonumber('0x990000')};
[13] = {background = Screen.colors.LightBlue};
[14] = {background = Screen.colors.WebGray, foreground = Screen.colors.DarkBlue};
[15] = {special = Screen.colors.Blue, undercurl = true},
[16] = {special = Screen.colors.Red, undercurl = true},
[17] = {foreground = Screen.colors.Red},
[18] = {bold = true, foreground = Screen.colors.SeaGreen};
[19] = {bold = true};
}
end
describe('decorations providers', function() describe('decorations providers', function()
local screen local screen
before_each(function() before_each(function()
clear() clear()
screen = Screen.new(40, 8) screen = Screen.new(40, 8)
screen:set_default_attr_ids { setup_screen(screen)
[1] = {bold=true, foreground=Screen.colors.Blue};
[2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red};
[3] = {foreground = Screen.colors.Brown};
[4] = {foreground = Screen.colors.Blue1};
[5] = {foreground = Screen.colors.Magenta};
[6] = {bold = true, foreground = Screen.colors.Brown};
[7] = {background = Screen.colors.Gray90};
[8] = {bold = true, reverse = true};
[9] = {reverse = true};
[10] = {italic = true, background = Screen.colors.Magenta};
[11] = {foreground = Screen.colors.Red, background = tonumber('0x005028')};
[12] = {foreground = tonumber('0x990000')};
[13] = {background = Screen.colors.LightBlue};
[14] = {background = Screen.colors.WebGray, foreground = Screen.colors.DarkBlue};
[15] = {special = Screen.colors.Blue, undercurl = true},
[16] = {special = Screen.colors.Red, undercurl = true},
[17] = {foreground = Screen.colors.Red},
[18] = {bold = true, foreground = Screen.colors.SeaGreen};
[19] = {bold = true};
}
end) end)
local mulholland = [[ local mulholland = [[
@ -64,7 +68,7 @@ describe('decorations providers', function()
]]) .. [[ ]]) .. [[
api.nvim_set_decoration_provider(_G.ns1, { api.nvim_set_decoration_provider(_G.ns1, {
on_start = on_do; on_buf = on_do; on_start = on_do; on_buf = on_do;
on_win = on_do; on_line = on_do; on_win = on_do; on_line = on_do; on_range = on_do;
on_end = on_do; _on_spell_nav = on_do; on_end = on_do; _on_spell_nav = on_do;
}) })
return _G.ns1 return _G.ns1
@ -107,12 +111,19 @@ describe('decorations providers', function()
{ "start", 4 }; { "start", 4 };
{ "win", 1000, 1, 0, 6 }; { "win", 1000, 1, 0, 6 };
{ "line", 1000, 1, 0 }; { "line", 1000, 1, 0 };
{ "range", 1000, 1, 0, 0, 1, 0 };
{ "line", 1000, 1, 1 }; { "line", 1000, 1, 1 };
{ "range", 1000, 1, 1, 0, 2, 0 };
{ "line", 1000, 1, 2 }; { "line", 1000, 1, 2 };
{ "range", 1000, 1, 2, 0, 3, 0 };
{ "line", 1000, 1, 3 }; { "line", 1000, 1, 3 };
{ "range", 1000, 1, 3, 0, 4, 0 };
{ "line", 1000, 1, 4 }; { "line", 1000, 1, 4 };
{ "range", 1000, 1, 4, 0, 5, 0 };
{ "line", 1000, 1, 5 }; { "line", 1000, 1, 5 };
{ "range", 1000, 1, 5, 0, 6, 0 };
{ "line", 1000, 1, 6 }; { "line", 1000, 1, 6 };
{ "range", 1000, 1, 6, 0, 7, 0 };
{ "end", 4 }; { "end", 4 };
} }
@ -132,6 +143,7 @@ describe('decorations providers', function()
{ "buf", 1, 5 }; { "buf", 1, 5 };
{ "win", 1000, 1, 0, 6 }; { "win", 1000, 1, 0, 6 };
{ "line", 1000, 1, 6 }; { "line", 1000, 1, 6 };
{ "range", 1000, 1, 6, 0, 7, 0 };
{ "end", 5 }; { "end", 5 };
} }
end) end)
@ -199,9 +211,13 @@ describe('decorations providers', function()
{ "start", 5 }; { "start", 5 };
{ "win", 1000, 1, 0, 3 }; { "win", 1000, 1, 0, 3 };
{ "line", 1000, 1, 0 }; { "line", 1000, 1, 0 };
{ "range", 1000, 1, 0, 0, 1, 0 };
{ "line", 1000, 1, 1 }; { "line", 1000, 1, 1 };
{ "range", 1000, 1, 1, 0, 2, 0 };
{ "line", 1000, 1, 2 }; { "line", 1000, 1, 2 };
{ "range", 1000, 1, 2, 0, 3, 0 };
{ "line", 1000, 1, 3 }; { "line", 1000, 1, 3 };
{ "range", 1000, 1, 3, 0, 4, 0 };
{ "end", 5 }; { "end", 5 };
} }
@ -694,6 +710,83 @@ describe('decorations providers', function()
]]) ]])
end) end)
it('on_range is invoked on all visible characters', function()
clear()
screen = Screen.new(20, 4)
setup_screen(screen)
local function record()
exec_lua(function()
_G.p_min = { math.huge, math.huge }
_G.p_max = { -math.huge, -math.huge }
function _G.pos_gt(a, b)
return a[1] > b[1] or (a[1] == b[1] and a[2] > b[2])
end
function _G.pos_lt(a, b)
return a[1] < b[1] or (a[1] == b[1] and a[2] < b[2])
end
end)
setup_provider [[
local function on_do(kind, _, bufnr, br, bc, er, ec)
if kind == 'range' then
local b = { br, bc }
local e = { er, ec }
if _G.pos_gt(_G.p_min, b) then
_G.p_min = b
end
if _G.pos_lt(_G.p_max, e) then
_G.p_max = e
end
end
end
]]
end
local function check(min, max)
local p_min = exec_lua('return _G.p_min')
assert(
p_min[1] < min[1] or (p_min[1] == min[1] and p_min[2] <= min[2]),
"minimum position " .. vim.inspect(p_min)
.. " should be before the first char"
)
local p_max = exec_lua('return _G.p_max')
assert(
p_max[1] > max[1] or (p_max[1] == max[1] and p_max[2] >= max[2]),
"maximum position " .. vim.inspect(p_max)
.. " should be on or after the last char"
)
end
-- Multiple lines.
exec_lua([[
local lines = { ('a'):rep(40), ('b'):rep(40), ('c'):rep(40) }
vim.api.nvim_buf_set_lines(0, 0, -1, true, lines)
vim.api.nvim_win_set_cursor(0, { 2, 0 })
]])
record()
screen:expect([[
^bbbbbbbbbbbbbbbbbbbb|
bbbbbbbbbbbbbbbbbbbb|
ccccccccccccccccc{1:@@@}|
|
]])
check({ 1, 0 }, { 2, 21 })
-- One long line.
exec_lua([[
local lines = { ('a'):rep(100) }
vim.api.nvim_buf_set_lines(0, 0, -1, true, lines)
vim.api.nvim_win_set_cursor(0, { 1, 70 })
]])
record()
screen:expect([[
{1:<<<}aaaaaaaaaaaaaaaaa|
aaaaaaaaaaaaaaaaaaaa|
aaaaaaaaaa^aaaaaaaaaa|
|
]])
check({ 0, 20 }, { 0, 81 })
end)
it('errors gracefully', function() it('errors gracefully', function()
insert(mulholland) insert(mulholland)

View File

@ -94,7 +94,7 @@ describe('ui mode_change event', function()
} }
end) end)
-- oldtest: Test_indent_norm_with_gq() -- oldtest: Test_mouse_shape_indent_norm_with_gq()
it('is restored to Normal mode after "gq" indents using :normal #12309', function() it('is restored to Normal mode after "gq" indents using :normal #12309', function()
screen:try_resize(60, 6) screen:try_resize(60, 6)
n.exec([[ n.exec([[

View File

@ -1162,6 +1162,8 @@ describe('builtin popupmenu', function()
[6] = { foreground = Screen.colors.White, background = Screen.colors.Red }, [6] = { foreground = Screen.colors.White, background = Screen.colors.Red },
[7] = { background = Screen.colors.Yellow }, -- Search [7] = { background = Screen.colors.Yellow }, -- Search
[8] = { foreground = Screen.colors.Red }, [8] = { foreground = Screen.colors.Red },
[9] = { foreground = Screen.colors.Yellow, background = Screen.colors.Green },
[10] = { foreground = Screen.colors.White, background = Screen.colors.Green },
ks = { foreground = Screen.colors.Red, background = Screen.colors.Grey }, ks = { foreground = Screen.colors.Red, background = Screen.colors.Grey },
kn = { foreground = Screen.colors.Red, background = Screen.colors.Plum1 }, kn = { foreground = Screen.colors.Red, background = Screen.colors.Plum1 },
xs = { foreground = Screen.colors.Black, background = Screen.colors.Grey }, xs = { foreground = Screen.colors.Black, background = Screen.colors.Grey },
@ -5625,6 +5627,114 @@ describe('builtin popupmenu', function()
{2:-- INSERT --} | {2:-- INSERT --} |
]]) ]])
feed('<Esc>') feed('<Esc>')
-- text after the inserted text shouldn't be highlighted
feed('0ea <C-X><C-O>')
screen:expect([[
αβγ {8:foo}^ foo |
{1:~ }{s: foo }{1: }|
{1:~ }{n: bar }{1: }|
{1:~ }{n: }{1: }|
{1:~ }|*15
{2:-- }{5:match 1 of 3} |
]])
feed('<C-P>')
screen:expect([[
αβγ ^ foo |
{1:~ }{n: foo }{1: }|
{1:~ }{n: bar }{1: }|
{1:~ }{n: }{1: }|
{1:~ }|*15
{2:-- }{8:Back at original} |
]])
feed('<C-P>')
screen:expect([[
αβγ {8:}^ foo |
{1:~ }{n: foo }{1: }|
{1:~ }{n: bar }{1: }|
{1:~ }{s: }{1: }|
{1:~ }|*15
{2:-- }{5:match 3 of 3} |
]])
feed('<C-Y>')
screen:expect([[
αβγ ^ foo |
{1:~ }|*18
{2:-- INSERT --} |
]])
feed('<Esc>')
end)
-- oldtest: Test_pum_matchins_highlight_combine()
it('with ComplMatchIns, Normal and CursorLine highlights', function()
exec([[
func Omni_test(findstart, base)
if a:findstart
return col(".")
endif
return [#{word: "foo"}, #{word: "bar"}, #{word: "你好"}]
endfunc
set omnifunc=Omni_test
hi Normal guibg=blue
hi CursorLine guibg=green guifg=white
set cursorline
call setline(1, 'aaa bbb')
]])
-- when ComplMatchIns is not set, CursorLine applies normally
feed('0ea <C-X><C-O>')
screen:expect([[
{10:aaa foo^ bbb }|
{1:~ }{s: foo }{1: }|
{1:~ }{n: bar }{1: }|
{1:~ }{n: }{1: }|
{1:~ }|*15
{2:-- }{5:match 1 of 3} |
]])
feed('<C-E>')
screen:expect([[
{10:aaa ^ bbb }|
{1:~ }|*18
{2:-- INSERT --} |
]])
feed('<BS><Esc>')
-- when ComplMatchIns is set, it is applied over CursorLine
command('hi ComplMatchIns guifg=Yellow')
feed('0ea <C-X><C-O>')
screen:expect([[
{10:aaa }{9:foo}{10:^ bbb }|
{1:~ }{s: foo }{1: }|
{1:~ }{n: bar }{1: }|
{1:~ }{n: }{1: }|
{1:~ }|*15
{2:-- }{5:match 1 of 3} |
]])
feed('<C-P>')
screen:expect([[
{10:aaa ^ bbb }|
{1:~ }{n: foo }{1: }|
{1:~ }{n: bar }{1: }|
{1:~ }{n: }{1: }|
{1:~ }|*15
{2:-- }{8:Back at original} |
]])
feed('<C-P>')
screen:expect([[
{10:aaa }{9:}{10:^ bbb }|
{1:~ }{n: foo }{1: }|
{1:~ }{n: bar }{1: }|
{1:~ }{s: }{1: }|
{1:~ }|*15
{2:-- }{5:match 3 of 3} |
]])
feed('<C-E>')
screen:expect([[
{10:aaa ^ bbb }|
{1:~ }|*18
{2:-- INSERT --} |
]])
feed('<Esc>')
end) end)
end end
end end

View File

@ -308,7 +308,7 @@ endfunc
" Test that mouse shape is restored to Normal mode after using "gq" when " Test that mouse shape is restored to Normal mode after using "gq" when
" 'indentexpr' executes :normal. " 'indentexpr' executes :normal.
func Test_indent_norm_with_gq() func Test_mouse_shape_indent_norm_with_gq()
CheckFeature mouseshape CheckFeature mouseshape
CheckCanRunGui CheckCanRunGui

View File

@ -1747,13 +1747,67 @@ func Test_pum_matchins_highlight()
call TermWait(buf) call TermWait(buf)
call term_sendkeys(buf, "Sαβγ \<C-X>\<C-O>\<C-Y>") call term_sendkeys(buf, "Sαβγ \<C-X>\<C-O>\<C-Y>")
call VerifyScreenDump(buf, 'Test_pum_matchins_04', {}) call VerifyScreenDump(buf, 'Test_pum_matchins_04', {})
call term_sendkeys(buf, "\<C-E>\<Esc>") call term_sendkeys(buf, "\<Esc>")
" restore after cancel completion " restore after cancel completion
call TermWait(buf) call TermWait(buf)
call term_sendkeys(buf, "Sαβγ \<C-X>\<C-O>\<Space>") call term_sendkeys(buf, "Sαβγ \<C-X>\<C-O>\<Space>")
call VerifyScreenDump(buf, 'Test_pum_matchins_05', {}) call VerifyScreenDump(buf, 'Test_pum_matchins_05', {})
call term_sendkeys(buf, "\<C-E>\<Esc>") call term_sendkeys(buf, "\<Esc>")
" text after the inserted text shouldn't be highlighted
call TermWait(buf)
call term_sendkeys(buf, "0ea \<C-X>\<C-O>")
call VerifyScreenDump(buf, 'Test_pum_matchins_07', {})
call term_sendkeys(buf, "\<C-P>")
call VerifyScreenDump(buf, 'Test_pum_matchins_08', {})
call term_sendkeys(buf, "\<C-P>")
call VerifyScreenDump(buf, 'Test_pum_matchins_09', {})
call term_sendkeys(buf, "\<C-Y>")
call VerifyScreenDump(buf, 'Test_pum_matchins_10', {})
call term_sendkeys(buf, "\<Esc>")
call StopVimInTerminal(buf)
endfunc
func Test_pum_matchins_highlight_combine()
CheckScreendump
let lines =<< trim END
func Omni_test(findstart, base)
if a:findstart
return col(".")
endif
return [#{word: "foo"}, #{word: "bar"}, #{word: "你好"}]
endfunc
set omnifunc=Omni_test
hi Normal ctermbg=blue
hi CursorLine cterm=underline ctermbg=green
set cursorline
call setline(1, 'aaa bbb')
END
call writefile(lines, 'Xscript', 'D')
let buf = RunVimInTerminal('-S Xscript', {})
" when ComplMatchIns is not set, CursorLine applies normally
call term_sendkeys(buf, "0ea \<C-X>\<C-O>")
call VerifyScreenDump(buf, 'Test_pum_matchins_combine_01', {})
call term_sendkeys(buf, "\<C-E>")
call VerifyScreenDump(buf, 'Test_pum_matchins_combine_02', {})
call term_sendkeys(buf, "\<BS>\<Esc>")
" when ComplMatchIns is set, it is applied over CursorLine
call TermWait(buf)
call term_sendkeys(buf, ":hi ComplMatchIns ctermbg=red ctermfg=yellow\<CR>")
call TermWait(buf)
call term_sendkeys(buf, "0ea \<C-X>\<C-O>")
call VerifyScreenDump(buf, 'Test_pum_matchins_combine_03', {})
call term_sendkeys(buf, "\<C-P>")
call VerifyScreenDump(buf, 'Test_pum_matchins_combine_04', {})
call term_sendkeys(buf, "\<C-P>")
call VerifyScreenDump(buf, 'Test_pum_matchins_combine_05', {})
call term_sendkeys(buf, "\<C-E>")
call VerifyScreenDump(buf, 'Test_pum_matchins_combine_06', {})
call term_sendkeys(buf, "\<Esc>")
call StopVimInTerminal(buf) call StopVimInTerminal(buf)
endfunc endfunc