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('PmenuExtra', { link = 'Pmenu' })
hi('PmenuExtraSel', { link = 'PmenuSel' })
hi('ComplMatchIns', { link = 'Normal' })
hi('ComplMatchIns', {})
hi('Substitute', { link = 'Search' })
hi('Whitespace', { link = 'NonText' })
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
themselves can be used to throttle unneeded callbacks. the `on_start`
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
that window (but any extmarks set in `on_win` will still be used). A
plugin managing multiple sources of decoration should ideally only set one
provider, and merge the sources internally. You can use multiple `ns_id`
for the extmarks set/modified inside the callback anyway.
Similarly, return `false` in `on_win` will skip the `on_line` and
`on_range` 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 one provider, and merge the sources internally. You can
use multiple `ns_id` for the extmarks set/modified inside the callback
anyway.
Note: doing anything other than setting extmarks is considered
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
quite dubious 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.
Attributes: ~
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
interaction with fold lines is subject to change) >
["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 >
["end", tick]

View File

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

View File

@ -1348,7 +1348,7 @@ parse({lang}, {query}) *vim.treesitter.query.parse()*
• |vim.treesitter.query.get()|
*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}
{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()`.
• {stop} (`integer?`) Stopping line for the search (end-exclusive).
Defaults to `node:end_()`.
• {opts} (`{ col_begin?: integer, col_end?: integer }?`) Optional
parameters.
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
*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
--- themselves can be used to throttle unneeded callbacks. the `on_start`
--- 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 that window (but any extmarks set in `on_win` will still be used).
--- Similarly, return `false` in `on_win` will skip the `on_line` and `on_range`
--- 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
--- one provider, and merge the sources internally. You can use multiple `ns_id`
--- 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
--- 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 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]
--- ```
--- - 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
--- ```
--- ["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_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: 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_hl_def? fun(_: "hl_def")
--- @field _on_spell_nav? fun(_: "spell_nav")

View File

@ -71,8 +71,6 @@ function TSQueryCursor:next_match() end
--- @param node TSNode
--- @param query TSQuery
--- @param start integer?
--- @param stop integer?
--- @param opts? { max_start_depth?: integer, match_limit?: integer}
--- @param opts? { row_begin?: integer, col_begin?: integer, row_end?: integer, col_end?: integer, max_start_depth?: integer, match_limit?: integer }
--- @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 query = vim.treesitter.query
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')
---@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
---@field private _query vim.treesitter.Query?
@ -55,6 +58,7 @@ end
---@class (private) vim.treesitter.highlighter.State
---@field tstree TSTree
---@field next_row integer
---@field next_col integer
---@field iter vim.treesitter.highlighter.Iter?
---@field highlighter_query vim.treesitter.highlighter.Query
@ -197,6 +201,7 @@ function TSHighlighter:prepare_highlight_states(srow, erow)
table.insert(self._highlight_states, {
tstree = tstree,
next_row = 0,
next_col = 0,
iter = nil,
highlighter_query = highlighter_query,
})
@ -278,41 +283,73 @@ end
---@param self vim.treesitter.highlighter
---@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
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)
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
if root_start_row > line or root_end_row < line then
--- @type boolean
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
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
-- 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.
state.iter =
state.highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1)
state.iter = state.highlighter_query:query():iter_captures(
root_node,
self.bufnr,
range_br,
root_er,
{ col_begin = range_bc, col_end = root_ec }
)
end
while line >= state.next_row do
local capture, node, metadata, match = state.iter(line)
local range = { root_end_row + 1, 0, root_end_row + 1, 0 }
if node then
range = vim.treesitter.get_range(node, buf, metadata and metadata[capture])
while cmp_lt(next_row, next_col, range_er, range_ec) do
local capture, node, metadata, match = state.iter(range_er, range_ec)
if not node then
next_row = math.huge
next_col = math.huge
break
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)
if capture then
if cmp_lt(next_row, next_col, start_row, start_col) then
next_row = start_row
next_col = start_col
end
if not capture then
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
@ -326,13 +363,13 @@ local function on_line_impl(self, buf, line, is_spell_nav)
local url = get_url(match, buf, capture, metadata)
if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then
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,
hl_group = hl,
ephemeral = true,
priority = priority,
hl_group = hl,
conceal = conceal,
spell = spell,
url = url,
@ -340,24 +377,25 @@ local function on_line_impl(self, buf, line, is_spell_nav)
end
end
if start_row > line then
state.next_row = start_row
end
end
state.next_row = next_row
state.next_col = next_col
end)
end
---@private
---@param _win integer
---@param buf integer
---@param line integer
function TSHighlighter._on_line(_, _win, buf, line, _)
---@param br integer
---@param bc integer
---@param er integer
---@param ec integer
function TSHighlighter._on_range(_, _win, buf, br, bc, er, ec, _)
local self = TSHighlighter.active[buf]
if not self then
return
end
on_line_impl(self, buf, line, false)
on_range_impl(self, buf, br, bc, er, ec, false)
end
---@private
@ -375,9 +413,7 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
local highlight_states = self._highlight_states
self:prepare_highlight_states(srow, erow)
for row = srow, erow do
on_line_impl(self, buf, row, true)
end
on_range_impl(self, buf, srow, 0, erow, 0, true)
self._highlight_states = highlight_states
end
@ -399,7 +435,7 @@ end
api.nvim_set_decoration_provider(ns, {
on_win = TSHighlighter._on_win,
on_line = TSHighlighter._on_line,
on_range = TSHighlighter._on_range,
_on_spell_nav = TSHighlighter._on_spell_nav,
})

View File

@ -1,6 +1,7 @@
local api = vim.api
local language = require('vim.treesitter.language')
local memoize = vim.func._memoize
local cmp_ge = require('vim.treesitter._range').cmp_pos.ge
local M = {}
@ -875,24 +876,33 @@ end
---@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 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
---
---@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
source = api.nvim_get_current_buf()
end
opts = opts or {}
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 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()
if not capture then
@ -902,10 +912,11 @@ function Query:iter_captures(node, source, start, stop)
if not match_preds(self, match, source) then
local match_id = match:info()
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
end
return iter(end_line) -- tail call: try next match
return iter(end_line, end_col) -- tail call: try next match
end
local metadata = apply_directives(self, match, source)
@ -961,8 +972,12 @@ function Query:iter_matches(node, source, start, stop, opts)
end
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 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
/// themselves can be used to throttle unneeded callbacks. the `on_start`
/// 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 that window (but any extmarks set in `on_win` will still be used).
/// Similarly, return `false` in `on_win` will skip the `on_line` and `on_range`
/// 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
/// one provider, and merge the sources internally. You can use multiple `ns_id`
/// 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
/// 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 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]
/// ```
/// - 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
/// ```
/// ["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_win", &opts->on_win, &p->redraw_win },
{ "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_hl_def", &opts->_on_hl_def, &p->hl_def },
{ "_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(("win" _, Integer winid, Integer bufnr, Integer toprow, Integer botrow)) on_win;
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(("hl_def" _)) _on_hl_def;
LuaRefOf(("spell_nav" _)) _on_spell_nav;

View File

@ -140,6 +140,7 @@ typedef struct {
LuaRef redraw_buf;
LuaRef redraw_win;
LuaRef redraw_line;
LuaRef redraw_range;
LuaRef redraw_end;
LuaRef hl_def;
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) \
{ 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 }
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;
}
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.
///
/// @param buf Buffer
@ -262,6 +288,7 @@ void decor_provider_clear(DecorProvider *p)
NLUA_CLEAR_REF(p->redraw_buf);
NLUA_CLEAR_REF(p->redraw_win);
NLUA_CLEAR_REF(p->redraw_line);
NLUA_CLEAR_REF(p->redraw_range);
NLUA_CLEAR_REF(p->redraw_end);
NLUA_CLEAR_REF(p->spell_nav);
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
int vi_attr = 0; // attributes for Visual and incsearch 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 decor_attr = 0; // attributes desired by syntax and extmarks
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 word_end = 0; // last byte with same spell_attr
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 mb_l = 1; // multi-byte byte length
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;
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) {
// To speed up the loop below, set extra_check when there is linebreak,
// 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);
if (!end_fill) {
decor_providers_invoke_line(wp, lnum - 1, &has_decor);
}
if (has_decor) {
extra_check = true;
check_decor_providers = true;
}
// 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.
// Avoids having to check this for each character.
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
}
if ((State & MODE_INSERT) && in_curline && ins_compl_active()) {
area_highlighting = true;
}
win_line_start(wp, &wlv);
bool draw_cols = true;
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;
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.
if (draw_cols) {
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) {
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) {
@ -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);
}
// 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) {
const int v = (int)(ptr - line);
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_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 PmenuKindSel PmenuSel",
"default link PmenuSbar Pmenu",
"default link ComplMatchIns Normal",
"default link ComplMatchIns NONE",
"default link Substitute Search",
"default link StatusLineTerm StatusLine",
"default link StatusLineTermNC StatusLineNC",

View File

@ -958,7 +958,7 @@ static void ins_compl_insert_bytes(char *p, int len)
}
assert(len >= 0);
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
@ -2147,6 +2147,8 @@ static bool ins_compl_stop(const int c, const int prev_mode, bool retval)
&& pum_visible()) {
word = xstrdup(compl_shown_match->cp_str);
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.
@ -3648,6 +3650,7 @@ void ins_compl_delete(bool new_leader)
return;
}
backspace_until_column(col);
compl_ins_end_col = curwin->w_cursor.col;
}
// TODO(vim): is this sufficient for redrawing? Redrawing everything

View File

@ -1287,39 +1287,67 @@ static struct luaL_Reg querycursor_meta[] = {
{ 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)
{
TSNode node = node_check(L, 1);
TSQuery *query = query_check(L, 2);
TSQueryCursor *cursor = ts_query_cursor_new();
ts_query_cursor_exec(cursor, query, node);
if (lua_gettop(L) >= 3) {
uint32_t start = (uint32_t)luaL_checkinteger(L, 3);
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) >= 3 && !lua_isnil(L, 3)) {
luaL_argcheck(L, lua_istable(L, 3), 3, "table expected");
TSPoint begin = (TSPoint){ 0, 0 };
TSPoint end = (TSPoint){ UINT32_MAX, UINT32_MAX };
if (set_uint32(L, 3, "row_begin", &begin.row)) {
if (!set_uint32(L, 3, "col_begin", &begin.column)) {
begin.column = 0;
}
}
if (lua_gettop(L) >= 5 && !lua_isnil(L, 5)) {
luaL_argcheck(L, lua_istable(L, 5), 5, "table expected");
lua_pushnil(L); // [dict, ..., nil]
while (lua_next(L, 5)) {
// [dict, ..., key, value]
if (lua_type(L, -2) == LUA_TSTRING) {
char *k = (char *)lua_tostring(L, -2);
if (strequal("max_start_depth", k)) {
uint32_t max_start_depth = (uint32_t)lua_tointeger(L, -1);
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);
} else if (strequal("match_limit", k)) {
uint32_t match_limit = (uint32_t)lua_tointeger(L, -1);
}
uint32_t match_limit;
if (set_uint32(L, 3, "match_limit", &match_limit)) {
ts_query_cursor_set_match_limit(cursor, match_limit);
}
}
// pop the value; lua_next will pop the key.
lua_pop(L, 1); // [dict, ..., key]
}
}
ts_query_cursor_exec(cursor, query, node);
TSQueryCursor **ud = lua_newuserdata(L, sizeof(*ud)); // [node, query, ..., udata]
*ud = cursor;

View File

@ -641,9 +641,6 @@ bool terminal_enter(void)
curwin->w_p_so = 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
s->term->pending.cursor = true;
refresh_cursor(s->term);
@ -674,8 +671,8 @@ bool terminal_enter(void)
RedrawingDisabled = s->save_rd;
apply_autocmds(EVENT_TERMLEAVE, NULL, NULL, false, curbuf);
shape_table[SHAPE_IDX_TERM] = save_cursorentry;
ui_mode_info_set();
// Restore the terminal cursor to what is set in 'guicursor'
(void)parse_shape_opt(SHAPE_CURSOR);
if (save_curwin == curwin->handle) { // Else: window was closed.
curwin->w_p_cul = save_w_p_cul;

View File

@ -6,10 +6,9 @@ describe('decor perf', function()
before_each(n.clear)
it('can handle long lines', function()
local screen = Screen.new(100, 101)
screen:attach()
Screen.new(100, 101)
local result = exec_lua [==[
local result = exec_lua(function()
local ephemeral_pattern = {
{ 0, 4, 'Comment', 11 },
{ 0, 3, 'Keyword', 12 },
@ -70,7 +69,7 @@ describe('decor perf', function()
local total = {}
local provider = {}
for i = 1, 100 do
for _ = 1, 100 do
local tic = vim.uv.hrtime()
vim.cmd 'redraw!'
local toc = vim.uv.hrtime()
@ -79,7 +78,7 @@ describe('decor perf', function()
end
return { total, provider }
]==]
end)
local total, provider = unpack(result)
table.sort(total)
@ -99,4 +98,39 @@ describe('decor perf', function()
print('\nTotal ' .. fmt(total) .. '\nDecoration provider: ' .. fmt(provider))
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)

View File

@ -1,10 +1,11 @@
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local clear = n.clear
local exec_lua = n.exec_lua
describe('treesitter perf', function()
setup(function()
before_each(function()
clear()
end)
@ -47,4 +48,144 @@ describe('treesitter perf', function()
return vim.uv.hrtime() - start
]]
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)

View File

@ -15,9 +15,20 @@ local skip = t.skip
describe(':terminal cursor', function()
local screen
local terminal_mode_idx ---@type number
before_each(function()
clear()
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)
it('moves the screen cursor when focused', function()
@ -143,13 +154,6 @@ describe(':terminal cursor', function()
it('can be modified by application #3681', function()
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 = {
[1] = { blink = true, shape = 'block' },
@ -171,13 +175,13 @@ describe(':terminal cursor', function()
]],
condition = function()
if v.blink then
eq(500, screen._mode_info[idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff)
eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
else
eq(0, screen._mode_info[idx].blinkon)
eq(0, screen._mode_info[idx].blinkoff)
eq(0, screen._mode_info[terminal_mode_idx].blinkon)
eq(0, screen._mode_info[terminal_mode_idx].blinkoff)
end
eq(v.shape, screen._mode_info[idx].cursor_shape)
eq(v.shape, screen._mode_info[terminal_mode_idx].cursor_shape)
end,
})
end
@ -191,20 +195,13 @@ describe(':terminal cursor', function()
]])
-- Cursor returns to default on TermLeave
eq(500, screen._mode_info[idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff)
eq('block', screen._mode_info[idx].cursor_shape)
eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
eq('block', screen._mode_info[terminal_mode_idx].cursor_shape)
end)
it('can be modified per terminal', function()
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
tt.feed_csi('5 q')
@ -216,9 +213,9 @@ describe(':terminal cursor', function()
{3:-- TERMINAL --} |
]],
condition = function()
eq(500, screen._mode_info[idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff)
eq('vertical', screen._mode_info[idx].cursor_shape)
eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
eq('vertical', screen._mode_info[terminal_mode_idx].cursor_shape)
end,
})
@ -231,9 +228,9 @@ describe(':terminal cursor', function()
{3:-- TERMINAL --} |
]],
condition = function()
eq(500, screen._mode_info[idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff)
eq('vertical', screen._mode_info[idx].cursor_shape)
eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
eq('vertical', screen._mode_info[terminal_mode_idx].cursor_shape)
end,
})
@ -256,9 +253,9 @@ describe(':terminal cursor', function()
]],
condition = function()
-- New terminal, cursor resets to defaults
eq(500, screen._mode_info[idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff)
eq('block', screen._mode_info[idx].cursor_shape)
eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
eq('block', screen._mode_info[terminal_mode_idx].cursor_shape)
end,
})
@ -275,9 +272,9 @@ describe(':terminal cursor', function()
{3:-- TERMINAL --} |
]],
condition = function()
eq(0, screen._mode_info[idx].blinkon)
eq(0, screen._mode_info[idx].blinkoff)
eq('horizontal', screen._mode_info[idx].cursor_shape)
eq(0, screen._mode_info[terminal_mode_idx].blinkon)
eq(0, screen._mode_info[terminal_mode_idx].blinkoff)
eq('horizontal', screen._mode_info[terminal_mode_idx].cursor_shape)
end,
})
@ -294,9 +291,9 @@ describe(':terminal cursor', function()
{3:-- TERMINAL --} |
]],
condition = function()
eq(500, screen._mode_info[idx].blinkon)
eq(500, screen._mode_info[idx].blinkoff)
eq('vertical', screen._mode_info[idx].cursor_shape)
eq(500, screen._mode_info[terminal_mode_idx].blinkon)
eq(500, screen._mode_info[terminal_mode_idx].blinkoff)
eq('vertical', screen._mode_info[terminal_mode_idx].cursor_shape)
end,
})
end)
@ -326,6 +323,32 @@ describe(':terminal cursor', function()
{3:-- TERMINAL --} |
]])
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)
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,
})
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
screen._handle_busy_start = 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,
})
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
screen._handle_busy_start = function() end
screen._handle_busy_stop = function() end

View File

@ -717,6 +717,47 @@ void ui_refresh(void)
eq({ { 1, 10, 1, 13 } }, ret)
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()
local function test(exp, cquery)
eq(exp, pcall_err(exec_lua, "vim.treesitter.query.parse('c', ...)", cquery))

View File

@ -15,11 +15,7 @@ local eq = t.eq
local assert_alive = n.assert_alive
local pcall_err = t.pcall_err
describe('decorations providers', function()
local screen
before_each(function()
clear()
screen = Screen.new(40, 8)
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};
@ -41,6 +37,14 @@ describe('decorations providers', function()
[18] = {bold = true, foreground = Screen.colors.SeaGreen};
[19] = {bold = true};
}
end
describe('decorations providers', function()
local screen
before_each(function()
clear()
screen = Screen.new(40, 8)
setup_screen(screen)
end)
local mulholland = [[
@ -64,7 +68,7 @@ describe('decorations providers', function()
]]) .. [[
api.nvim_set_decoration_provider(_G.ns1, {
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;
})
return _G.ns1
@ -107,12 +111,19 @@ describe('decorations providers', function()
{ "start", 4 };
{ "win", 1000, 1, 0, 6 };
{ "line", 1000, 1, 0 };
{ "range", 1000, 1, 0, 0, 1, 0 };
{ "line", 1000, 1, 1 };
{ "range", 1000, 1, 1, 0, 2, 0 };
{ "line", 1000, 1, 2 };
{ "range", 1000, 1, 2, 0, 3, 0 };
{ "line", 1000, 1, 3 };
{ "range", 1000, 1, 3, 0, 4, 0 };
{ "line", 1000, 1, 4 };
{ "range", 1000, 1, 4, 0, 5, 0 };
{ "line", 1000, 1, 5 };
{ "range", 1000, 1, 5, 0, 6, 0 };
{ "line", 1000, 1, 6 };
{ "range", 1000, 1, 6, 0, 7, 0 };
{ "end", 4 };
}
@ -132,6 +143,7 @@ describe('decorations providers', function()
{ "buf", 1, 5 };
{ "win", 1000, 1, 0, 6 };
{ "line", 1000, 1, 6 };
{ "range", 1000, 1, 6, 0, 7, 0 };
{ "end", 5 };
}
end)
@ -199,9 +211,13 @@ describe('decorations providers', function()
{ "start", 5 };
{ "win", 1000, 1, 0, 3 };
{ "line", 1000, 1, 0 };
{ "range", 1000, 1, 0, 0, 1, 0 };
{ "line", 1000, 1, 1 };
{ "range", 1000, 1, 1, 0, 2, 0 };
{ "line", 1000, 1, 2 };
{ "range", 1000, 1, 2, 0, 3, 0 };
{ "line", 1000, 1, 3 };
{ "range", 1000, 1, 3, 0, 4, 0 };
{ "end", 5 };
}
@ -694,6 +710,83 @@ describe('decorations providers', function()
]])
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()
insert(mulholland)

View File

@ -94,7 +94,7 @@ describe('ui mode_change event', function()
}
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()
screen:try_resize(60, 6)
n.exec([[

View File

@ -1162,6 +1162,8 @@ describe('builtin popupmenu', function()
[6] = { foreground = Screen.colors.White, background = Screen.colors.Red },
[7] = { background = Screen.colors.Yellow }, -- Search
[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 },
kn = { foreground = Screen.colors.Red, background = Screen.colors.Plum1 },
xs = { foreground = Screen.colors.Black, background = Screen.colors.Grey },
@ -5625,6 +5627,114 @@ describe('builtin popupmenu', function()
{2:-- INSERT --} |
]])
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

View File

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

View File

@ -1747,13 +1747,67 @@ func Test_pum_matchins_highlight()
call TermWait(buf)
call term_sendkeys(buf, "Sαβγ \<C-X>\<C-O>\<C-Y>")
call VerifyScreenDump(buf, 'Test_pum_matchins_04', {})
call term_sendkeys(buf, "\<C-E>\<Esc>")
call term_sendkeys(buf, "\<Esc>")
" restore after cancel completion
call TermWait(buf)
call term_sendkeys(buf, "Sαβγ \<C-X>\<C-O>\<Space>")
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)
endfunc