mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	Merge pull request #31400 from vanaigr/decor-provider-range
feat(decor): add range-based highlighting
This commit is contained in:
		| @@ -3290,11 +3290,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 | ||||||
| @@ -3302,8 +3303,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` or | ||||||
|     callbacks. |     `on_range` callbacks. | ||||||
|  |  | ||||||
|     Attributes: ~ |     Attributes: ~ | ||||||
|         Lua |vim.api| only |         Lua |vim.api| only | ||||||
| @@ -3326,6 +3327,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] | ||||||
|   | |||||||
| @@ -51,7 +51,7 @@ These changes may require adaptations in your config or plugins. | |||||||
|  |  | ||||||
| API | API | ||||||
|  |  | ||||||
| • todo | • Decoration provider has `on_range()` callback. | ||||||
|  |  | ||||||
| BUILD | BUILD | ||||||
|  |  | ||||||
| @@ -118,6 +118,7 @@ TREESITTER | |||||||
|   `metadata[capture_id].offset`. The offset will be applied in |   `metadata[capture_id].offset`. The offset will be applied in | ||||||
|   |vim.treesitter.get_range()|, which should be preferred over reading |   |vim.treesitter.get_range()|, which should be preferred over reading | ||||||
|   metadata directly for retrieving node ranges. |   metadata directly for retrieving node ranges. | ||||||
|  | • |Query:iter_captures()| supports specifying starting and ending columns. | ||||||
|  |  | ||||||
| TUI | TUI | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1522,7 +1522,7 @@ them to parse text. See |vim.treesitter.query.parse()| for a working example. | |||||||
|       • {has_combined_injections}  (`boolean`) whether the query contains |       • {has_combined_injections}  (`boolean`) whether the query contains | ||||||
|                                    combined injections |                                    combined injections | ||||||
|       • {query}                    (`TSQuery`) userdata query object |       • {query}                    (`TSQuery`) userdata query object | ||||||
|       • {iter_captures}            (`fun(self: vim.treesitter.Query, node: TSNode, source: integer|string, start: integer?, stop: integer?, opts: table?): fun(end_line: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree`) |       • {iter_captures}            (`fun(self: vim.treesitter.Query, node: TSNode, source: integer|string, start_row: integer?, end_row: integer?, opts: table?): fun(end_line: integer?, end_col: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree`) | ||||||
|                                    See |Query:iter_captures()|. |                                    See |Query:iter_captures()|. | ||||||
|       • {iter_matches}             (`fun(self: vim.treesitter.Query, node: TSNode, source: integer|string, start: integer?, stop: integer?, opts: table?): fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata, TSTree`) |       • {iter_matches}             (`fun(self: vim.treesitter.Query, node: TSNode, source: integer|string, start: integer?, stop: integer?, opts: table?): fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata, TSTree`) | ||||||
|                                    See |Query:iter_matches()|. |                                    See |Query:iter_matches()|. | ||||||
| @@ -1693,7 +1693,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}, {opts}) | Query:iter_captures({node}, {source}, {start_row}, {end_row}, {opts}) | ||||||
|     Iterates over all captures from all matches in {node}. |     Iterates over all captures from all matches in {node}. | ||||||
|  |  | ||||||
|     {source} is required if the query contains predicates; then the caller |     {source} is required if the query contains predicates; then the caller | ||||||
| @@ -1726,21 +1726,25 @@ Query:iter_captures({node}, {source}, {start}, {stop}, {opts}) | |||||||
|  |  | ||||||
|     Parameters: ~ |     Parameters: ~ | ||||||
|       • {node}       (`TSNode`) under which the search will occur |       • {node}       (`TSNode`) under which the search will occur | ||||||
|       • {source}  (`integer|string`) Source buffer or string to extract text |       • {source}     (`integer|string`) Source buffer or string to extract | ||||||
|                   from |                      text from | ||||||
|       • {start}   (`integer?`) Starting line for the search. Defaults to |       • {start_row}  (`integer?`) Starting line for the search. Defaults to | ||||||
|                      `node:start()`. |                      `node:start()`. | ||||||
|       • {stop}    (`integer?`) Stopping line for the search (end-exclusive). |       • {end_row}    (`integer?`) Stopping line for the search (end-inclusive, | ||||||
|                   Defaults to `node:end_()`. |                      unless `stop_col` is provided). Defaults to | ||||||
|  |                      `node:end_()`. | ||||||
|       • {opts}       (`table?`) Optional keyword arguments: |       • {opts}       (`table?`) Optional keyword arguments: | ||||||
|                      • max_start_depth (integer) if non-zero, sets the maximum |                      • max_start_depth (integer) if non-zero, sets the maximum | ||||||
|                        start depth for each match. This is used to prevent |                        start depth for each match. This is used to prevent | ||||||
|                        traversing too deep into a tree. |                        traversing too deep into a tree. | ||||||
|                      • match_limit (integer) Set the maximum number of |                      • match_limit (integer) Set the maximum number of | ||||||
|                        in-progress matches (Default: 256). |                        in-progress matches (Default: 256). | ||||||
|  |                      • start_col (integer) Starting column for the search. | ||||||
|  |                      • end_col (integer) Stopping column for the search | ||||||
|  |                        (end-exclusive). | ||||||
|  |  | ||||||
|     Return: ~ |     Return: ~ | ||||||
|         (`fun(end_line: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree`) |         (`fun(end_line: integer?, end_col: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree`) | ||||||
|         capture id, capture node, metadata, match, tree |         capture id, capture node, metadata, match, tree | ||||||
|  |  | ||||||
|                                                         *Query:iter_matches()* |                                                         *Query:iter_matches()* | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								runtime/lua/vim/_meta/api.lua
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								runtime/lua/vim/_meta/api.lua
									
									
									
										generated
									
									
									
								
							| @@ -2128,7 +2128,7 @@ 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` calls | ||||||
| --- for that window (but any extmarks set in `on_win` will still be used). | --- 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` | ||||||
| @@ -2140,7 +2140,7 @@ 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` or `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: | ||||||
| @@ -2162,6 +2162,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] | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								runtime/lua/vim/_meta/api_keysets.lua
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								runtime/lua/vim/_meta/api_keysets.lua
									
									
									
										generated
									
									
									
								
							| @@ -382,6 +382,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): boolean? | --- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer): boolean? | ||||||
| --- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer): boolean? | --- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer): boolean? | ||||||
|  | --- @field on_range? fun(_: "range", winid: integer, bufnr: integer, start_row: integer, start_col: integer, end_row: integer, end_col: integer): boolean? | ||||||
| --- @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") | ||||||
|   | |||||||
| @@ -80,8 +80,6 @@ function TSQueryCursor:next_match() end | |||||||
|  |  | ||||||
| --- @param node TSNode | --- @param node TSNode | ||||||
| --- @param query TSQuery | --- @param query TSQuery | ||||||
| --- @param start integer? | --- @param opts? { start_row: integer, start_col: integer, end_row: integer, end_col: 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 | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| 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_lt = Range.cmp_pos.lt | ||||||
|  |  | ||||||
| local ns = api.nvim_create_namespace('nvim.treesitter.highlighter') | local ns = api.nvim_create_namespace('nvim.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, TSTree | ||||||
|  |  | ||||||
| ---@class (private) vim.treesitter.highlighter.Query | ---@class (private) vim.treesitter.highlighter.Query | ||||||
| ---@field private _query vim.treesitter.Query? | ---@field private _query vim.treesitter.Query? | ||||||
| @@ -57,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 | ||||||
| ---@field prev_marks MarkInfo[] | ---@field prev_marks MarkInfo[] | ||||||
| @@ -233,6 +235,7 @@ function TSHighlighter:prepare_highlight_states(win, srow, erow) | |||||||
|     table.insert(self._highlight_states[win], { |     table.insert(self._highlight_states[win], { | ||||||
|       tstree = tstree, |       tstree = tstree, | ||||||
|       next_row = 0, |       next_row = 0, | ||||||
|  |       next_col = 0, | ||||||
|       iter = nil, |       iter = nil, | ||||||
|       highlighter_query = hl_query, |       highlighter_query = hl_query, | ||||||
|       prev_marks = {}, |       prev_marks = {}, | ||||||
| @@ -331,27 +334,36 @@ end | |||||||
| ---Queues the remainder if the mark continues after the line. | ---Queues the remainder if the mark continues after the line. | ||||||
| ---@param m MarkInfo | ---@param m MarkInfo | ||||||
| ---@param buf integer | ---@param buf integer | ||||||
| ---@param line integer | ---@param range_start_row integer | ||||||
|  | ---@param range_start_col integer | ||||||
|  | ---@param range_end_row integer | ||||||
|  | ---@param range_end_col integer | ||||||
| ---@param next_marks MarkInfo[] | ---@param next_marks MarkInfo[] | ||||||
| local function add_mark(m, buf, line, next_marks) | local function add_mark( | ||||||
|  |   m, | ||||||
|  |   buf, | ||||||
|  |   range_start_row, | ||||||
|  |   range_start_col, | ||||||
|  |   range_end_row, | ||||||
|  |   range_end_col, | ||||||
|  |   next_marks | ||||||
|  | ) | ||||||
|   local cur_start_l = m.start_line |   local cur_start_l = m.start_line | ||||||
|   local cur_start_c = m.start_col |   local cur_start_c = m.start_col | ||||||
|   if cur_start_l < line then |   if cmp_lt(cur_start_l, cur_start_c, range_start_row, range_start_col) then | ||||||
|     cur_start_l = line |     cur_start_l = range_start_row | ||||||
|     cur_start_c = 0 |     cur_start_c = range_start_col | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   local cur_opts = m.opts |   local cur_opts = m.opts | ||||||
|   if cur_opts.end_line >= line + 1 then |   if cmp_lt(range_end_row, range_end_col, cur_opts.end_line, cur_opts.end_col) then | ||||||
|     cur_opts = vim.deepcopy(cur_opts, true) |     cur_opts = vim.deepcopy(cur_opts, true) | ||||||
|     cur_opts.end_line = line + 1 |     cur_opts.end_line = range_end_row | ||||||
|     cur_opts.end_col = 0 |     cur_opts.end_col = range_end_col | ||||||
|     table.insert(next_marks, m) |     table.insert(next_marks, m) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   local empty = cur_opts.end_line < cur_start_l |   if cmp_lt(cur_start_l, cur_start_c, cur_opts.end_line, cur_opts.end_col) then | ||||||
|     or (cur_opts.end_line == cur_start_l and cur_opts.end_col <= cur_start_c) |  | ||||||
|   if cur_start_l <= line and not empty then |  | ||||||
|     api.nvim_buf_set_extmark(buf, ns, cur_start_l, cur_start_c, cur_opts) |     api.nvim_buf_set_extmark(buf, ns, cur_start_l, cur_start_c, cur_opts) | ||||||
|   end |   end | ||||||
| end | end | ||||||
| @@ -359,17 +371,44 @@ end | |||||||
| ---@param self vim.treesitter.highlighter | ---@param self vim.treesitter.highlighter | ||||||
| ---@param win integer | ---@param win integer | ||||||
| ---@param buf integer | ---@param buf integer | ||||||
| ---@param line integer | ---@param range_start_row integer | ||||||
|  | ---@param range_start_col integer | ||||||
|  | ---@param range_end_row integer | ||||||
|  | ---@param range_end_col integer | ||||||
| ---@param on_spell boolean | ---@param on_spell boolean | ||||||
| ---@param on_conceal boolean | ---@param on_conceal boolean | ||||||
| local function on_line_impl(self, win, buf, line, on_spell, on_conceal) | local function on_range_impl( | ||||||
|   self._conceal_checked[line] = self._conceal_line and true or nil |   self, | ||||||
|  |   win, | ||||||
|  |   buf, | ||||||
|  |   range_start_row, | ||||||
|  |   range_start_col, | ||||||
|  |   range_end_row, | ||||||
|  |   range_end_col, | ||||||
|  |   on_spell, | ||||||
|  |   on_conceal | ||||||
|  | ) | ||||||
|  |   if self._conceal_line then | ||||||
|  |     range_start_col = 0 | ||||||
|  |     if range_end_col ~= 0 then | ||||||
|  |       range_end_row = range_end_row + 1 | ||||||
|  |       range_end_col = 0 | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |   for i = range_start_row, range_end_row - 1 do | ||||||
|  |     self._conceal_checked[i] = self._conceal_line or nil | ||||||
|  |   end | ||||||
|   self:for_each_highlight_state(win, function(state) |   self:for_each_highlight_state(win, function(state) | ||||||
|     local root_node = state.tstree:root() |     local root_node = state.tstree:root() | ||||||
|     local root_start_row, _, root_end_row, _ = root_node:range() |     ---@type { [1]: integer, [2]: integer, [3]: integer, [4]: integer } | ||||||
|  |     local root_range = { root_node:range() } | ||||||
|  |  | ||||||
|     -- Only consider trees that contain this line |     if | ||||||
|     if root_start_row > line or root_end_row < line then |       not Range.intercepts( | ||||||
|  |         root_range, | ||||||
|  |         { range_start_row, range_start_col, range_end_row, range_end_col } | ||||||
|  |       ) | ||||||
|  |     then | ||||||
|       return |       return | ||||||
|     end |     end | ||||||
|  |  | ||||||
| @@ -378,35 +417,59 @@ local function on_line_impl(self, win, buf, line, on_spell, on_conceal) | |||||||
|     local next_marks = {} |     local next_marks = {} | ||||||
|  |  | ||||||
|     for _, mark in ipairs(state.prev_marks) do |     for _, mark in ipairs(state.prev_marks) do | ||||||
|       add_mark(mark, buf, line, next_marks) |       add_mark( | ||||||
|  |         mark, | ||||||
|  |         buf, | ||||||
|  |         range_start_row, | ||||||
|  |         range_start_col, | ||||||
|  |         range_end_row, | ||||||
|  |         range_end_col, | ||||||
|  |         next_marks | ||||||
|  |       ) | ||||||
|     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_start_row, range_start_col) 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_start_row, | ||||||
|  |         root_range[3], | ||||||
|  |         { start_col = range_start_col, end_col = root_range[4] } | ||||||
|  |       ) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     local captures = state.highlighter_query:query().captures |     local captures = state.highlighter_query:query().captures | ||||||
|  |  | ||||||
|     while line >= state.next_row do |     while cmp_lt(next_row, next_col, range_end_row, range_end_col) do | ||||||
|       local capture, node, metadata, match = state.iter(line) |       local capture, node, metadata, match = state.iter(range_end_row, range_end_col) | ||||||
|  |       if not node then | ||||||
|       local outer_range = { root_end_row + 1, 0, root_end_row + 1, 0 } |         next_row = math.huge | ||||||
|       if node then |         next_col = math.huge | ||||||
|         outer_range = vim.treesitter.get_range(node, buf, metadata and metadata[capture]) |         break | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       local outer_range = vim.treesitter.get_range(node, buf, metadata and metadata[capture]) | ||||||
|  |       if cmp_lt(next_row, next_col, outer_range[1], outer_range[2]) then | ||||||
|  |         next_row = outer_range[1] | ||||||
|  |         next_col = outer_range[2] | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       if not capture then | ||||||
|  |         break | ||||||
|       end |       end | ||||||
|       local outer_range_start_row = outer_range[1] |  | ||||||
|  |  | ||||||
|       for _, range in ipairs(tree_region) do |       for _, range in ipairs(tree_region) do | ||||||
|         local intersection = Range.intersection(range, outer_range) |         local intersection = Range.intersection(range, outer_range) | ||||||
|         if intersection then |         if intersection then | ||||||
|           local start_row, start_col, end_row, end_col = Range.unpack4(intersection) |           local start_row, start_col, end_row, end_col = Range.unpack4(intersection) | ||||||
|  |  | ||||||
|           if capture then |  | ||||||
|           local hl = state.highlighter_query:get_hl_from_capture(capture) |           local hl = state.highlighter_query:get_hl_from_capture(capture) | ||||||
|  |  | ||||||
|           local capture_name = captures[capture] |           local capture_name = captures[capture] | ||||||
| @@ -424,7 +487,7 @@ local function on_line_impl(self, win, buf, line, on_spell, on_conceal) | |||||||
|  |  | ||||||
|           local url = get_url(match, buf, capture, metadata) |           local url = get_url(match, buf, capture, metadata) | ||||||
|  |  | ||||||
|             if hl and end_row >= line and not on_conceal and (not on_spell or spell ~= nil) then |           if hl and not on_conceal and (not on_spell or spell ~= nil) then | ||||||
|             local opts = { |             local opts = { | ||||||
|               end_line = end_row, |               end_line = end_row, | ||||||
|               end_col = end_col, |               end_col = end_col, | ||||||
| @@ -436,7 +499,15 @@ local function on_line_impl(self, win, buf, line, on_spell, on_conceal) | |||||||
|               url = url, |               url = url, | ||||||
|             } |             } | ||||||
|             local mark = { start_line = start_row, start_col = start_col, opts = opts } |             local mark = { start_line = start_row, start_col = start_col, opts = opts } | ||||||
|               add_mark(mark, buf, line, next_marks) |             add_mark( | ||||||
|  |               mark, | ||||||
|  |               buf, | ||||||
|  |               range_start_row, | ||||||
|  |               range_start_col, | ||||||
|  |               range_end_row, | ||||||
|  |               range_end_col, | ||||||
|  |               next_marks | ||||||
|  |             ) | ||||||
|           end |           end | ||||||
|  |  | ||||||
|           if |           if | ||||||
| @@ -452,11 +523,8 @@ local function on_line_impl(self, win, buf, line, on_spell, on_conceal) | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|       if outer_range_start_row > line then |     state.next_row = next_row | ||||||
|         state.next_row = outer_range_start_row |     state.next_col = next_col | ||||||
|       end |  | ||||||
|     end |  | ||||||
|  |  | ||||||
|     state.prev_marks = next_marks |     state.prev_marks = next_marks | ||||||
|   end) |   end) | ||||||
| end | end | ||||||
| @@ -464,14 +532,17 @@ 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, win, buf, line, false, false) |   on_range_impl(self, win, buf, br, bc, er, ec, false, false) | ||||||
| end | end | ||||||
|  |  | ||||||
| ---@private | ---@private | ||||||
| @@ -490,9 +561,7 @@ function TSHighlighter._on_spell_nav(_, win, buf, srow, _, erow, _) | |||||||
|   local highlight_states = self._highlight_states[win] |   local highlight_states = self._highlight_states[win] | ||||||
|   self:prepare_highlight_states(win, srow, erow) |   self:prepare_highlight_states(win, srow, erow) | ||||||
|  |  | ||||||
|   for row = srow, erow do |   on_range_impl(self, win, buf, srow, 0, erow, 0, true, false) | ||||||
|     on_line_impl(self, win, buf, row, true, false) |  | ||||||
|   end |  | ||||||
|   self._highlight_states[win] = highlight_states |   self._highlight_states[win] = highlight_states | ||||||
| end | end | ||||||
|  |  | ||||||
| @@ -510,7 +579,7 @@ function TSHighlighter._on_conceal_line(_, win, buf, row) | |||||||
|   local highlight_states = self._highlight_states[win] |   local highlight_states = self._highlight_states[win] | ||||||
|   self.tree:parse({ row, row }) |   self.tree:parse({ row, row }) | ||||||
|   self:prepare_highlight_states(win, row, row) |   self:prepare_highlight_states(win, row, row) | ||||||
|   on_line_impl(self, win, buf, row, false, true) |   on_range_impl(self, win, buf, row, 0, row + 1, 0, false, true) | ||||||
|   self._highlight_states[win] = highlight_states |   self._highlight_states[win] = highlight_states | ||||||
| end | end | ||||||
|  |  | ||||||
| @@ -554,7 +623,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, | ||||||
|   _on_conceal_line = TSHighlighter._on_conceal_line, |   _on_conceal_line = TSHighlighter._on_conceal_line, | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -4,6 +4,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 MODELINE_FORMAT = '^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$' | local MODELINE_FORMAT = '^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$' | ||||||
| local EXTENDS_FORMAT = '^;+%s*extends%s*$' | local EXTENDS_FORMAT = '^;+%s*extends%s*$' | ||||||
| @@ -951,18 +952,20 @@ end | |||||||
| --- | --- | ||||||
| ---@param node TSNode under which the search will occur | ---@param node TSNode under which the search will occur | ||||||
| ---@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_row? 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 end_row? integer Stopping line for the search (end-inclusive, unless `stop_col` is provided). Defaults to `node:end_()`. | ||||||
| ---@param opts? table Optional keyword arguments: | ---@param opts? table Optional keyword arguments: | ||||||
| ---   - max_start_depth (integer) if non-zero, sets the maximum start depth | ---   - max_start_depth (integer) if non-zero, sets the maximum start depth | ||||||
| ---     for each match. This is used to prevent traversing too deep into a tree. | ---     for each match. This is used to prevent traversing too deep into a tree. | ||||||
| ---   - match_limit (integer) Set the maximum number of in-progress matches (Default: 256). | ---   - match_limit (integer) Set the maximum number of in-progress matches (Default: 256). | ||||||
|  | ---   - start_col (integer) Starting column for the search. | ||||||
|  | ---   - end_col (integer) Stopping column for the search (end-exclusive). | ||||||
| --- | --- | ||||||
| ---@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree): | ---@return (fun(end_line: integer|nil, end_col: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree): | ||||||
| ---        capture id, capture node, metadata, match, tree | ---        capture id, capture node, metadata, match, tree | ||||||
| --- | --- | ||||||
| ---@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, opts) | function Query:iter_captures(node, source, start_row, end_row, opts) | ||||||
|   opts = opts or {} |   opts = opts or {} | ||||||
|   opts.match_limit = opts.match_limit or 256 |   opts.match_limit = opts.match_limit or 256 | ||||||
|  |  | ||||||
| @@ -970,17 +973,24 @@ function Query:iter_captures(node, source, start, stop, opts) | |||||||
|     source = api.nvim_get_current_buf() |     source = api.nvim_get_current_buf() | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   start, stop = value_or_node_range(start, stop, node) |   start_row, end_row = value_or_node_range(start_row, end_row, node) | ||||||
|  |  | ||||||
|   local tree = node:tree() |   local tree = node:tree() | ||||||
|   local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts) |   local cursor = vim._create_ts_querycursor(node, self.query, { | ||||||
|  |     start_row = start_row, | ||||||
|  |     start_col = opts.start_col or 0, | ||||||
|  |     end_row = end_row, | ||||||
|  |     end_col = opts.end_col or 0, | ||||||
|  |     max_start_depth = opts.max_start_depth, | ||||||
|  |     match_limit = opts.match_limit or 256, | ||||||
|  |   }) | ||||||
|  |  | ||||||
|   -- For faster checks that a match is not in the cache. |   -- For faster checks that a match is not in the cache. | ||||||
|   local highest_cached_match_id = -1 |   local highest_cached_match_id = -1 | ||||||
|   ---@type table<integer, vim.treesitter.query.TSMetadata> |   ---@type table<integer, vim.treesitter.query.TSMetadata> | ||||||
|   local match_cache = {} |   local match_cache = {} | ||||||
|  |  | ||||||
|   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 | ||||||
| @@ -1005,9 +1015,22 @@ function Query:iter_captures(node, source, start, stop, opts) | |||||||
|         local predicates = processed_pattern.predicates |         local predicates = processed_pattern.predicates | ||||||
|         if not self:_match_predicates(predicates, pattern_i, captures, source) then |         if not self:_match_predicates(predicates, pattern_i, captures, source) then | ||||||
|           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() | ||||||
|  |  | ||||||
|  |           local outside = false | ||||||
|  |           if end_line then | ||||||
|  |             if end_col then | ||||||
|  |               outside = cmp_ge(row, col, end_line, end_col) | ||||||
|  |             else | ||||||
|  |               outside = row > end_line | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  |  | ||||||
|  |           if outside 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) -- tail call: try next match | ||||||
|         end |         end | ||||||
|  |  | ||||||
| @@ -1072,7 +1095,14 @@ function Query:iter_matches(node, source, start, stop, opts) | |||||||
|   start, stop = value_or_node_range(start, stop, node) |   start, stop = value_or_node_range(start, stop, node) | ||||||
|  |  | ||||||
|   local tree = node:tree() |   local tree = node:tree() | ||||||
|   local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts) |   local cursor = vim._create_ts_querycursor(node, self.query, { | ||||||
|  |     start_row = start, | ||||||
|  |     start_col = 0, | ||||||
|  |     end_row = stop, | ||||||
|  |     end_col = 0, | ||||||
|  |     max_start_depth = opts.max_start_depth, | ||||||
|  |     match_limit = opts.match_limit or 256, | ||||||
|  |   }) | ||||||
|  |  | ||||||
|   local function iter() |   local function iter() | ||||||
|     local match = cursor:next_match() |     local match = cursor:next_match() | ||||||
|   | |||||||
| @@ -1004,7 +1004,7 @@ 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` calls | ||||||
| /// for that window (but any extmarks set in `on_win` will still be used). | /// 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` | ||||||
| @@ -1016,7 +1016,7 @@ 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` or `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: | ||||||
| @@ -1038,6 +1038,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] | ||||||
| @@ -1061,6 +1069,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 }, | ||||||
|   | |||||||
| @@ -18,6 +18,8 @@ typedef struct { | |||||||
|   LuaRefOf(("win" _, Integer winid, Integer bufnr, Integer toprow, Integer botrow), |   LuaRefOf(("win" _, Integer winid, Integer bufnr, Integer toprow, Integer botrow), | ||||||
|            *Boolean) on_win; |            *Boolean) on_win; | ||||||
|   LuaRefOf(("line" _, Integer winid, Integer bufnr, Integer row), *Boolean) on_line; |   LuaRefOf(("line" _, Integer winid, Integer bufnr, Integer row), *Boolean) on_line; | ||||||
|  |   LuaRefOf(("range" _, Integer winid, Integer bufnr, Integer start_row, Integer start_col, | ||||||
|  |             Integer end_row, Integer end_col), *Boolean) 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; | ||||||
|   | |||||||
| @@ -513,7 +513,7 @@ static void decor_state_pack(DecorState *state) | |||||||
|   state->future_begin = fut_beg; |   state->future_begin = fut_beg; | ||||||
| } | } | ||||||
|  |  | ||||||
| bool decor_redraw_line(win_T *wp, int row, DecorState *state) | void decor_redraw_line(win_T *wp, int row, DecorState *state) | ||||||
| { | { | ||||||
|   decor_state_pack(state); |   decor_state_pack(state); | ||||||
|  |  | ||||||
| @@ -527,7 +527,11 @@ bool decor_redraw_line(win_T *wp, int row, DecorState *state) | |||||||
|   state->row = row; |   state->row = row; | ||||||
|   state->col_until = -1; |   state->col_until = -1; | ||||||
|   state->eol_col = -1; |   state->eol_col = -1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Checks if there are (likely) more decorations on the current line. | ||||||
|  | bool decor_has_more_decorations(DecorState *state, int row) | ||||||
|  | { | ||||||
|   if (state->current_end != 0 || state->future_begin != (int)kv_size(state->ranges_i)) { |   if (state->current_end != 0 || state->future_begin != (int)kv_size(state->ranges_i)) { | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -149,6 +149,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; | ||||||
|   | |||||||
| @@ -25,7 +25,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, LUA_NOREF, -1, false, false, 0 } |     LUA_NOREF, LUA_NOREF, -1, false, false, 0 } | ||||||
|  |  | ||||||
| static void decor_provider_error(DecorProvider *provider, const char *name, const char *msg) | static void decor_provider_error(DecorProvider *provider, const char *name, const char *msg) | ||||||
| @@ -189,6 +189,30 @@ void decor_providers_invoke_line(win_T *wp, int row) | |||||||
|   decor_state.running_decor_provider = false; |   decor_state.running_decor_provider = false; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void decor_providers_invoke_range(win_T *wp, int start_row, int start_col, int end_row, int end_col) | ||||||
|  | { | ||||||
|  |   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(start_row)); | ||||||
|  |       ADD_C(args, INTEGER_OBJ(start_col)); | ||||||
|  |       ADD_C(args, INTEGER_OBJ(end_row)); | ||||||
|  |       ADD_C(args, INTEGER_OBJ(end_col)); | ||||||
|  |       if (!decor_provider_invoke((int)i, "range", p->redraw_range, args, true)) { | ||||||
|  |         // return 'false' or error: skip rest of this window | ||||||
|  |         kv_A(decor_providers, i).state = kDecorProviderWinDisabled; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       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 | ||||||
| @@ -272,6 +296,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); | ||||||
|   NLUA_CLEAR_REF(p->conceal_line); |   NLUA_CLEAR_REF(p->conceal_line); | ||||||
|   | |||||||
| @@ -1142,6 +1142,9 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b | |||||||
|   // Not drawing text when line is concealed or drawing filler lines beyond last line. |   // Not drawing text when line is concealed or drawing filler lines beyond last line. | ||||||
|   const bool draw_text = !concealed && (lnum != buf->b_ml.ml_line_count + 1); |   const bool draw_text = !concealed && (lnum != buf->b_ml.ml_line_count + 1); | ||||||
|  |  | ||||||
|  |   int decor_provider_end_col; | ||||||
|  |   bool check_decor_providers = false; | ||||||
|  |  | ||||||
|   if (col_rows == 0 && draw_text) { |   if (col_rows == 0 && draw_text) { | ||||||
|     // 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. | ||||||
| @@ -1163,14 +1166,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     decor_providers_invoke_line(wp, lnum - 1);  // may invalidate wp->w_virtcol |     check_decor_providers = true; | ||||||
|     validate_virtcol(wp); |  | ||||||
|  |  | ||||||
|     has_decor = decor_redraw_line(wp, lnum - 1, &decor_state); |  | ||||||
|  |  | ||||||
|     if (has_decor) { |  | ||||||
|       extra_check = true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Check for columns to display for 'colorcolumn'. |     // Check for columns to display for 'colorcolumn'. | ||||||
|     wlv.color_cols = wp->w_buffer->terminal ? NULL : wp->w_p_cc_cols; |     wlv.color_cols = wp->w_buffer->terminal ? NULL : wp->w_p_cc_cols; | ||||||
| @@ -1466,22 +1462,22 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b | |||||||
|  |  | ||||||
|   // 'nowrap' or 'wrap' and a single line that doesn't fit: Advance to the |   // 'nowrap' or 'wrap' and a single line that doesn't fit: Advance to the | ||||||
|   // first character to be displayed. |   // first character to be displayed. | ||||||
|   const int start_col = wp->w_p_wrap |   const int start_vcol = wp->w_p_wrap | ||||||
|                          ? (startrow == 0 ? wp->w_skipcol : 0) |                          ? (startrow == 0 ? wp->w_skipcol : 0) | ||||||
|                          : wp->w_leftcol; |                          : wp->w_leftcol; | ||||||
|  |  | ||||||
|   if (has_foldtext) { |   if (has_foldtext) { | ||||||
|     wlv.vcol = start_col; |     wlv.vcol = start_vcol; | ||||||
|   } else if (start_col > 0 && col_rows == 0) { |   } else if (start_vcol > 0 && col_rows == 0) { | ||||||
|     char *prev_ptr = ptr; |     char *prev_ptr = ptr; | ||||||
|     CharSize cs = { 0 }; |     CharSize cs = { 0 }; | ||||||
|  |  | ||||||
|     CharsizeArg csarg; |     CharsizeArg csarg; | ||||||
|     CSType cstype = init_charsize_arg(&csarg, wp, lnum, line); |     CSType cstype = init_charsize_arg(&csarg, wp, lnum, line); | ||||||
|     csarg.max_head_vcol = start_col; |     csarg.max_head_vcol = start_vcol; | ||||||
|     int vcol = wlv.vcol; |     int vcol = wlv.vcol; | ||||||
|     StrCharInfo ci = utf_ptr2StrCharInfo(ptr); |     StrCharInfo ci = utf_ptr2StrCharInfo(ptr); | ||||||
|     while (vcol < start_col && *ci.ptr != NUL) { |     while (vcol < start_vcol && *ci.ptr != NUL) { | ||||||
|       cs = win_charsize(cstype, vcol, ci.ptr, ci.chr.value, &csarg); |       cs = win_charsize(cstype, vcol, ci.ptr, ci.chr.value, &csarg); | ||||||
|       vcol += cs.width; |       vcol += cs.width; | ||||||
|       prev_ptr = ci.ptr; |       prev_ptr = ci.ptr; | ||||||
| @@ -1518,23 +1514,23 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b | |||||||
|     // - the visual mode is active, or |     // - the visual mode is active, or | ||||||
|     // - drawing a fold |     // - drawing a fold | ||||||
|     // the end of the line may be before the start of the displayed part. |     // the end of the line may be before the start of the displayed part. | ||||||
|     if (wlv.vcol < start_col && (wp->w_p_cuc |     if (wlv.vcol < start_vcol && (wp->w_p_cuc | ||||||
|                                   || wlv.color_cols |                                   || wlv.color_cols | ||||||
|                                   || virtual_active(wp) |                                   || virtual_active(wp) | ||||||
|                                   || (VIsual_active && wp->w_buffer == curwin->w_buffer) |                                   || (VIsual_active && wp->w_buffer == curwin->w_buffer) | ||||||
|                                   || has_fold)) { |                                   || has_fold)) { | ||||||
|       wlv.vcol = start_col; |       wlv.vcol = start_vcol; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Handle a character that's not completely on the screen: Put ptr at |     // Handle a character that's not completely on the screen: Put ptr at | ||||||
|     // that character but skip the first few screen characters. |     // that character but skip the first few screen characters. | ||||||
|     if (wlv.vcol > start_col) { |     if (wlv.vcol > start_vcol) { | ||||||
|       wlv.vcol -= charsize; |       wlv.vcol -= charsize; | ||||||
|       ptr = prev_ptr; |       ptr = prev_ptr; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (start_col > wlv.vcol) { |     if (start_vcol > wlv.vcol) { | ||||||
|       wlv.skip_cells = start_col - wlv.vcol - head; |       wlv.skip_cells = start_vcol - wlv.vcol - head; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Adjust for when the inverted text is before the screen, |     // Adjust for when the inverted text is before the screen, | ||||||
| @@ -1588,6 +1584,23 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (check_decor_providers) { | ||||||
|  |     int const col = (int)(ptr - line); | ||||||
|  |     decor_provider_end_col = decor_providers_setup(endrow - startrow, | ||||||
|  |                                                    start_vcol == 0, | ||||||
|  |                                                    lnum, | ||||||
|  |                                                    col, | ||||||
|  |                                                    wp); | ||||||
|  |     line = ml_get_buf(wp->w_buffer, lnum); | ||||||
|  |     ptr = line + col; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   decor_redraw_line(wp, lnum - 1, &decor_state); | ||||||
|  |   if (!has_decor && decor_has_more_decorations(&decor_state, lnum - 1)) { | ||||||
|  |     has_decor = true; | ||||||
|  |     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) { | ||||||
| @@ -1642,6 +1655,18 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b | |||||||
|  |  | ||||||
|     bool did_decrement_ptr = false; |     bool did_decrement_ptr = false; | ||||||
|  |  | ||||||
|  |     // Get next chunk of extmark highlights if previous approximation was smaller than needed. | ||||||
|  |     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); | ||||||
|  |       line = ml_get_buf(wp->w_buffer, lnum); | ||||||
|  |       ptr = line + col; | ||||||
|  |       if (!has_decor && decor_has_more_decorations(&decor_state, lnum - 1)) { | ||||||
|  |         has_decor = true; | ||||||
|  |         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) { | ||||||
| @@ -2740,7 +2765,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b | |||||||
|       // Highlight 'cursorcolumn' & 'colorcolumn' past end of the line. |       // Highlight 'cursorcolumn' & 'colorcolumn' past end of the line. | ||||||
|  |  | ||||||
|       // check if line ends before left margin |       // check if line ends before left margin | ||||||
|       wlv.vcol = MAX(wlv.vcol, start_col + wlv.col - win_col_off(wp)); |       wlv.vcol = MAX(wlv.vcol, start_vcol + wlv.col - win_col_off(wp)); | ||||||
|       // Get rid of the boguscols now, we want to draw until the right |       // Get rid of the boguscols now, we want to draw until the right | ||||||
|       // edge for 'cursorcolumn'. |       // edge for 'cursorcolumn'. | ||||||
|       wlv.col -= wlv.boguscols; |       wlv.col -= wlv.boguscols; | ||||||
| @@ -2762,7 +2787,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b | |||||||
|  |  | ||||||
|       if (((wp->w_p_cuc |       if (((wp->w_p_cuc | ||||||
|             && wp->w_virtcol >= vcol_hlc(wlv) - eol_hl_off |             && wp->w_virtcol >= vcol_hlc(wlv) - eol_hl_off | ||||||
|             && wp->w_virtcol < view_width * (ptrdiff_t)(wlv.row - startrow + 1) + start_col |             && wp->w_virtcol < view_width * (ptrdiff_t)(wlv.row - startrow + 1) + start_vcol | ||||||
|             && lnum != wp->w_cursor.lnum) |             && lnum != wp->w_cursor.lnum) | ||||||
|            || wlv.color_cols || wlv.line_attr_lowprio || wlv.line_attr |            || wlv.color_cols || wlv.line_attr_lowprio || wlv.line_attr | ||||||
|            || wlv.diff_hlf != 0 || wp->w_buffer->terminal)) { |            || wlv.diff_hlf != 0 || wp->w_buffer->terminal)) { | ||||||
| @@ -3180,3 +3205,50 @@ static void wlv_put_linebuf(win_T *wp, const winlinevars_T *wlv, int endcol, boo | |||||||
|   ScreenGrid *g = grid_adjust(grid, &row, &coloff); |   ScreenGrid *g = grid_adjust(grid, &row, &coloff); | ||||||
|   grid_put_linebuf(g, row, coloff, startcol, endcol, clear_width, bg_attr, 0, wlv->vcol - 1, flags); |   grid_put_linebuf(g, row, coloff, startcol, endcol, clear_width, bg_attr, 0, wlv->vcol - 1, flags); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | static int decor_providers_setup(int rows_to_draw, bool draw_from_line_start, linenr_T lnum, | ||||||
|  |                                  colnr_T col, win_T *wp) | ||||||
|  | { | ||||||
|  |   // 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_view_width - win_col_off(wp); | ||||||
|  |     int width2 = width + win_col_off2(wp); | ||||||
|  |  | ||||||
|  |     int first_row_width = draw_from_line_start ? width : width2; | ||||||
|  |     rem_vcols = first_row_width + (rows_to_draw - 1) * width2; | ||||||
|  |   } else { | ||||||
|  |     rem_vcols = wp->w_view_height - win_col_off(wp); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Call it here since we need to invalidate the line pointer anyway. | ||||||
|  |   decor_providers_invoke_line(wp, lnum - 1); | ||||||
|  |   validate_virtcol(wp); | ||||||
|  |  | ||||||
|  |   return invoke_range_next(wp, lnum, col, rem_vcols + 1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @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) | ||||||
|  | { | ||||||
|  |   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); | ||||||
|  |     validate_virtcol(wp); | ||||||
|  |     new_col = end_col; | ||||||
|  |   } else { | ||||||
|  |     decor_providers_invoke_range(wp, lnum - 1, begin_col, lnum, 0); | ||||||
|  |     validate_virtcol(wp); | ||||||
|  |     new_col = INT_MAX; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return new_col; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1361,33 +1361,45 @@ static int tslua_push_querycursor(lua_State *L) | |||||||
|  |  | ||||||
|   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)) { |   lua_getfield(L, 3, "start_row"); | ||||||
|     luaL_argcheck(L, lua_istable(L, 5), 5, "table expected"); |   uint32_t start_row = (uint32_t)luaL_checkinteger(L, -1); | ||||||
|     lua_pushnil(L);  // [dict, ..., nil] |   lua_pop(L, 1); | ||||||
|     while (lua_next(L, 5)) { |  | ||||||
|       // [dict, ..., key, value] |   lua_getfield(L, 3, "start_col"); | ||||||
|       if (lua_type(L, -2) == LUA_TSTRING) { |   uint32_t start_col = (uint32_t)luaL_checkinteger(L, -1); | ||||||
|         char *k = (char *)lua_tostring(L, -2); |   lua_pop(L, 1); | ||||||
|         if (strequal("max_start_depth", k)) { |  | ||||||
|           uint32_t max_start_depth = (uint32_t)lua_tointeger(L, -1); |   lua_getfield(L, 3, "end_row"); | ||||||
|  |   uint32_t end_row = (uint32_t)luaL_checkinteger(L, -1); | ||||||
|  |   lua_pop(L, 1); | ||||||
|  |  | ||||||
|  |   lua_getfield(L, 3, "end_col"); | ||||||
|  |   uint32_t end_col = (uint32_t)luaL_checkinteger(L, -1); | ||||||
|  |   lua_pop(L, 1); | ||||||
|  |  | ||||||
|  |   ts_query_cursor_set_point_range(cursor, (TSPoint){ start_row, start_col }, | ||||||
|  |                                   (TSPoint){ end_row, end_col }); | ||||||
|  |  | ||||||
|  |   lua_getfield(L, 3, "max_start_depth"); | ||||||
|  |   if (!lua_isnil(L, -1)) { | ||||||
|  |     uint32_t max_start_depth = (uint32_t)luaL_checkinteger(L, -1); | ||||||
|     ts_query_cursor_set_max_start_depth(cursor, 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); |   lua_pop(L, 1); | ||||||
|  |  | ||||||
|  |   lua_getfield(L, 3, "match_limit"); | ||||||
|  |   if (!lua_isnil(L, -1)) { | ||||||
|  |     uint32_t match_limit = (uint32_t)luaL_checkinteger(L, -1); | ||||||
|     ts_query_cursor_set_match_limit(cursor, match_limit); |     ts_query_cursor_set_match_limit(cursor, match_limit); | ||||||
|   } |   } | ||||||
|       } |   lua_pop(L, 1); | ||||||
|       // 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] |   TSQueryCursor **ud = lua_newuserdata(L, sizeof(*ud));  // [node, query, ..., udata] | ||||||
|   *ud = cursor; |   *ud = cursor; | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ describe('decor perf', function() | |||||||
|   it('can handle long lines', function() |   it('can handle long lines', function() | ||||||
|     Screen.new(100, 101) |     Screen.new(100, 101) | ||||||
|  |  | ||||||
|     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 }, | ||||||
| @@ -69,7 +69,7 @@ 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() | ||||||
| @@ -78,7 +78,7 @@ describe('decor perf', function() | |||||||
|       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) | ||||||
| @@ -137,4 +137,39 @@ describe('decor perf', function() | |||||||
|     ) |     ) | ||||||
|     print('\nTotal ' .. res) |     print('\nTotal ' .. res) | ||||||
|   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) | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -747,6 +747,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 = table.concat({ | ||||||
|  |       'int aaa = 1, bbb = 2;', | ||||||
|  |       'int foo = 1, bar = 2;', | ||||||
|  |       'int baz = 3, qux = 4;', | ||||||
|  |       'int ccc = 1, ddd = 2;', | ||||||
|  |     }, '\n') | ||||||
|  |  | ||||||
|  |     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, { start_col = 7, end_col = 13 }) | ||||||
|  |     eq({ { 1, 13, 1, 16 }, { 2, 4, 2, 7 } }, ret) | ||||||
|  |  | ||||||
|  |     ret = exec_lua(test, { start_col = 7 }) | ||||||
|  |     eq({ { 1, 13, 1, 16 } }, ret) | ||||||
|  |  | ||||||
|  |     ret = exec_lua(test, { end_col = 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)) | ||||||
|   | |||||||
| @@ -28,18 +28,14 @@ local function setup_provider(code) | |||||||
|   ]]) .. [[ |   ]]) .. [[ | ||||||
|     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 | ||||||
|   ]]) |   ]]) | ||||||
| end | end | ||||||
|  |  | ||||||
| describe('decorations providers', function() | local function setup_screen(screen) | ||||||
|   local screen ---@type test.functional.ui.screen |  | ||||||
|   before_each(function() |  | ||||||
|     clear() |  | ||||||
|     screen = Screen.new(40, 8) |  | ||||||
|   screen:set_default_attr_ids { |   screen:set_default_attr_ids { | ||||||
|     [1] = { bold = true, foreground = Screen.colors.Blue }, |     [1] = { bold = true, foreground = Screen.colors.Blue }, | ||||||
|     [2] = { foreground = Screen.colors.Grey100, background = Screen.colors.Red }, |     [2] = { foreground = Screen.colors.Grey100, background = Screen.colors.Red }, | ||||||
| @@ -61,6 +57,14 @@ describe('decorations providers', function() | |||||||
|     [18] = { bold = true, foreground = Screen.colors.SeaGreen }, |     [18] = { bold = true, foreground = Screen.colors.SeaGreen }, | ||||||
|     [19] = { bold = true }, |     [19] = { bold = true }, | ||||||
|   } |   } | ||||||
|  | end | ||||||
|  |  | ||||||
|  | describe('decorations providers', function() | ||||||
|  |   local screen ---@type test.functional.ui.screen | ||||||
|  |   before_each(function() | ||||||
|  |     clear() | ||||||
|  |     screen = Screen.new(40, 8) | ||||||
|  |     setup_screen(screen) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|   local mulholland = [[ |   local mulholland = [[ | ||||||
| @@ -110,12 +114,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 }, | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -137,6 +148,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) | ||||||
| @@ -206,9 +218,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 }, | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -806,6 +822,126 @@ 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 }) | ||||||
|  |  | ||||||
|  |     -- Multibyte characters. | ||||||
|  |     exec_lua([[ | ||||||
|  |       local lines = { ('\195\162'):rep(100) } | ||||||
|  |       vim.api.nvim_buf_set_lines(0, 0, -1, true, lines) | ||||||
|  |       vim.api.nvim_win_set_cursor(0, { 1, 70 * 2 }) | ||||||
|  |     ]]) | ||||||
|  |     record() | ||||||
|  |     screen:expect([[ | ||||||
|  |       {1:<<<}âââââââââââââââââ| | ||||||
|  |       ââââââââââââââââââââ| | ||||||
|  |       ââââââââââ^ââââââââââ| | ||||||
|  |                           | | ||||||
|  |     ]]) | ||||||
|  |     check({ 0, 20 * 2 }, { 0, 81 * 2 }) | ||||||
|  |  | ||||||
|  |     -- Tabs. | ||||||
|  |     exec_lua([[ | ||||||
|  |       local lines = { 'a' .. ('\t'):rep(100) } | ||||||
|  |       vim.api.nvim_buf_set_lines(0, 0, -1, true, lines) | ||||||
|  |       vim.api.nvim_win_set_cursor(0, { 1, 39 }) | ||||||
|  |     ]]) | ||||||
|  |     record() | ||||||
|  |     screen:expect([[ | ||||||
|  |       {1:<<<}                 | | ||||||
|  |                           | | ||||||
|  |                  ^         | | ||||||
|  |                           | | ||||||
|  |     ]]) | ||||||
|  |     check({ 0, 33 }, { 0, 94 }) | ||||||
|  |  | ||||||
|  |     -- One long line without wrapping. | ||||||
|  |     command('set nowrap') | ||||||
|  |     exec_lua([[ | ||||||
|  |       local lines = { ('a'):rep(50) .. ('b'):rep(50) } | ||||||
|  |       vim.api.nvim_buf_set_lines(0, 0, -1, true, lines) | ||||||
|  |       vim.api.nvim_win_set_cursor(0, { 1, 50 }) | ||||||
|  |     ]]) | ||||||
|  |     record() | ||||||
|  |     screen:expect([[ | ||||||
|  |       aaaaaaaaaa^bbbbbbbbbb| | ||||||
|  |       {1:~                   }|*2 | ||||||
|  |                           | | ||||||
|  |     ]]) | ||||||
|  |     check({ 0, 40 }, { 0, 60 }) | ||||||
|  |   end) | ||||||
|  |  | ||||||
|   it('can add new providers during redraw #26652', function() |   it('can add new providers during redraw #26652', function() | ||||||
|     setup_provider [[ |     setup_provider [[ | ||||||
|     local ns = api.nvim_create_namespace('test_no_add') |     local ns = api.nvim_create_namespace('test_no_add') | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 bfredl
					bfredl