mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
feat(treesitter): async parsing
**Problem:** Parsing can be slow for large files, and it is a blocking operation which can be disruptive and annoying. **Solution:** Provide a function for asynchronous parsing, which accepts a callback to be run after parsing completes. Co-authored-by: Lewis Russell <lewis6991@gmail.com> Co-authored-by: Luuk van Baal <luukvbaal@gmail.com> Co-authored-by: VanaIgr <vanaigranov@gmail.com>
This commit is contained in:
parent
3fdc430241
commit
45e606b1fd
@ -297,6 +297,8 @@ PERFORMANCE
|
|||||||
• Strong |treesitter-query| caching makes repeat |vim.treesitter.query.get()|
|
• Strong |treesitter-query| caching makes repeat |vim.treesitter.query.get()|
|
||||||
and |vim.treesitter.query.parse()| calls significantly faster for large
|
and |vim.treesitter.query.parse()| calls significantly faster for large
|
||||||
queries.
|
queries.
|
||||||
|
• Treesitter highlighting is now asynchronous. To force synchronous parsing,
|
||||||
|
use `vim.g._ts_force_sync_parsing = true`.
|
||||||
|
|
||||||
PLUGINS
|
PLUGINS
|
||||||
|
|
||||||
@ -339,6 +341,8 @@ TREESITTER
|
|||||||
• New |TSNode:child_with_descendant()|, which is nearly identical to
|
• New |TSNode:child_with_descendant()|, which is nearly identical to
|
||||||
|TSNode:child_containing_descendant()| except that it can return the
|
|TSNode:child_containing_descendant()| except that it can return the
|
||||||
descendant itself.
|
descendant itself.
|
||||||
|
• |LanguageTree:parse()| optionally supports asynchronous invocation, which is
|
||||||
|
activated by passing the `on_parse` callback parameter.
|
||||||
|
|
||||||
TUI
|
TUI
|
||||||
|
|
||||||
|
@ -4657,8 +4657,8 @@ A jump table for the options with a short description can be found at |Q_op|.
|
|||||||
'redrawtime' 'rdt' number (default 2000)
|
'redrawtime' 'rdt' number (default 2000)
|
||||||
global
|
global
|
||||||
Time in milliseconds for redrawing the display. Applies to
|
Time in milliseconds for redrawing the display. Applies to
|
||||||
'hlsearch', 'inccommand', |:match| highlighting and syntax
|
'hlsearch', 'inccommand', |:match| highlighting, syntax highlighting,
|
||||||
highlighting.
|
and async |LanguageTree:parse()|.
|
||||||
When redrawing takes more than this many milliseconds no further
|
When redrawing takes more than this many milliseconds no further
|
||||||
matches will be highlighted.
|
matches will be highlighted.
|
||||||
For syntax highlighting the time applies per window. When over the
|
For syntax highlighting the time applies per window. When over the
|
||||||
|
@ -1090,6 +1090,9 @@ start({bufnr}, {lang}) *vim.treesitter.start()*
|
|||||||
required for some plugins. In this case, add `vim.bo.syntax = 'on'` after
|
required for some plugins. In this case, add `vim.bo.syntax = 'on'` after
|
||||||
the call to `start`.
|
the call to `start`.
|
||||||
|
|
||||||
|
Note: By default, the highlighter parses code asynchronously, using a
|
||||||
|
segment time of 3ms.
|
||||||
|
|
||||||
Example: >lua
|
Example: >lua
|
||||||
vim.api.nvim_create_autocmd( 'FileType', { pattern = 'tex',
|
vim.api.nvim_create_autocmd( 'FileType', { pattern = 'tex',
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
@ -1401,8 +1404,8 @@ Query:iter_captures({node}, {source}, {start}, {stop})
|
|||||||
Defaults to `node:end_()`.
|
Defaults to `node:end_()`.
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(`fun(end_line: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch`)
|
(`fun(end_line: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree`)
|
||||||
capture id, capture node, metadata, match
|
capture id, capture node, metadata, match, tree
|
||||||
|
|
||||||
*Query:iter_matches()*
|
*Query:iter_matches()*
|
||||||
Query:iter_matches({node}, {source}, {start}, {stop}, {opts})
|
Query:iter_matches({node}, {source}, {start}, {stop}, {opts})
|
||||||
@ -1447,8 +1450,8 @@ Query:iter_matches({node}, {source}, {start}, {stop}, {opts})
|
|||||||
compatibility and will be removed in a future release.
|
compatibility and will be removed in a future release.
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(`fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata`)
|
(`fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata, TSTree`)
|
||||||
pattern id, match, metadata
|
pattern id, match, metadata, tree
|
||||||
|
|
||||||
set({lang}, {query_name}, {text}) *vim.treesitter.query.set()*
|
set({lang}, {query_name}, {text}) *vim.treesitter.query.set()*
|
||||||
Sets the runtime query named {query_name} for {lang}
|
Sets the runtime query named {query_name} for {lang}
|
||||||
@ -1611,7 +1614,7 @@ LanguageTree:node_for_range({range}, {opts})
|
|||||||
Return: ~
|
Return: ~
|
||||||
(`TSNode?`)
|
(`TSNode?`)
|
||||||
|
|
||||||
LanguageTree:parse({range}) *LanguageTree:parse()*
|
LanguageTree:parse({range}, {on_parse}) *LanguageTree:parse()*
|
||||||
Recursively parse all regions in the language tree using
|
Recursively parse all regions in the language tree using
|
||||||
|treesitter-parsers| for the corresponding languages and run injection
|
|treesitter-parsers| for the corresponding languages and run injection
|
||||||
queries on the parsed trees to determine whether child trees should be
|
queries on the parsed trees to determine whether child trees should be
|
||||||
@ -1622,14 +1625,27 @@ LanguageTree:parse({range}) *LanguageTree:parse()*
|
|||||||
if {range} is `true`).
|
if {range} is `true`).
|
||||||
|
|
||||||
Parameters: ~
|
Parameters: ~
|
||||||
• {range} (`boolean|Range?`) Parse this range in the parser's source.
|
• {range} (`boolean|Range?`) Parse this range in the parser's
|
||||||
Set to `true` to run a complete parse of the source (Note:
|
source. Set to `true` to run a complete parse of the
|
||||||
Can be slow!) Set to `false|nil` to only parse regions with
|
source (Note: Can be slow!) Set to `false|nil` to only
|
||||||
empty ranges (typically only the root tree without
|
parse regions with empty ranges (typically only the root
|
||||||
injections).
|
tree without injections).
|
||||||
|
• {on_parse} (`fun(err?: string, trees?: table<integer, TSTree>)?`)
|
||||||
|
Function invoked when parsing completes. When provided and
|
||||||
|
`vim.g._ts_force_sync_parsing` is not set, parsing will
|
||||||
|
run asynchronously. The first argument to the function is
|
||||||
|
a string respresenting the error type, in case of a
|
||||||
|
failure (currently only possible for timeouts). The second
|
||||||
|
argument is the list of trees returned by the parse (upon
|
||||||
|
success), or `nil` if the parse timed out (determined by
|
||||||
|
'redrawtime').
|
||||||
|
|
||||||
|
If parsing was still able to finish synchronously (within
|
||||||
|
3ms), `parse()` returns the list of trees. Otherwise, it
|
||||||
|
returns `nil`.
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(`table<integer, TSTree>`)
|
(`table<integer, TSTree>?`)
|
||||||
|
|
||||||
*LanguageTree:register_cbs()*
|
*LanguageTree:register_cbs()*
|
||||||
LanguageTree:register_cbs({cbs}, {recursive})
|
LanguageTree:register_cbs({cbs}, {recursive})
|
||||||
|
4
runtime/lua/vim/_meta/options.lua
generated
4
runtime/lua/vim/_meta/options.lua
generated
@ -4845,8 +4845,8 @@ vim.go.redrawdebug = vim.o.redrawdebug
|
|||||||
vim.go.rdb = vim.go.redrawdebug
|
vim.go.rdb = vim.go.redrawdebug
|
||||||
|
|
||||||
--- Time in milliseconds for redrawing the display. Applies to
|
--- Time in milliseconds for redrawing the display. Applies to
|
||||||
--- 'hlsearch', 'inccommand', `:match` highlighting and syntax
|
--- 'hlsearch', 'inccommand', `:match` highlighting, syntax highlighting,
|
||||||
--- highlighting.
|
--- and async `LanguageTree:parse()`.
|
||||||
--- When redrawing takes more than this many milliseconds no further
|
--- When redrawing takes more than this many milliseconds no further
|
||||||
--- matches will be highlighted.
|
--- matches will be highlighted.
|
||||||
--- For syntax highlighting the time applies per window. When over the
|
--- For syntax highlighting the time applies per window. When over the
|
||||||
|
@ -61,7 +61,7 @@ function M._create_parser(bufnr, lang, opts)
|
|||||||
{ on_bytes = bytes_cb, on_detach = detach_cb, on_reload = reload_cb, preview = true }
|
{ on_bytes = bytes_cb, on_detach = detach_cb, on_reload = reload_cb, preview = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
self:parse()
|
self:parse(nil, function() end)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
@ -397,6 +397,8 @@ end
|
|||||||
--- Note: By default, disables regex syntax highlighting, which may be required for some plugins.
|
--- Note: By default, disables regex syntax highlighting, which may be required for some plugins.
|
||||||
--- In this case, add `vim.bo.syntax = 'on'` after the call to `start`.
|
--- In this case, add `vim.bo.syntax = 'on'` after the call to `start`.
|
||||||
---
|
---
|
||||||
|
--- Note: By default, the highlighter parses code asynchronously, using a segment time of 3ms.
|
||||||
|
---
|
||||||
--- Example:
|
--- Example:
|
||||||
---
|
---
|
||||||
--- ```lua
|
--- ```lua
|
||||||
@ -408,8 +410,8 @@ end
|
|||||||
--- })
|
--- })
|
||||||
--- ```
|
--- ```
|
||||||
---
|
---
|
||||||
---@param bufnr (integer|nil) Buffer to be highlighted (default: current buffer)
|
---@param bufnr integer? Buffer to be highlighted (default: current buffer)
|
||||||
---@param lang (string|nil) Language of the parser (default: from buffer filetype)
|
---@param lang string? Language of the parser (default: from buffer filetype)
|
||||||
function M.start(bufnr, lang)
|
function M.start(bufnr, lang)
|
||||||
bufnr = vim._resolve_bufnr(bufnr)
|
bufnr = vim._resolve_bufnr(bufnr)
|
||||||
local parser = assert(M.get_parser(bufnr, lang, { error = false }))
|
local parser = assert(M.get_parser(bufnr, lang, { error = false }))
|
||||||
|
@ -69,6 +69,7 @@ end
|
|||||||
---@field private _queries table<string,vim.treesitter.highlighter.Query>
|
---@field private _queries table<string,vim.treesitter.highlighter.Query>
|
||||||
---@field tree vim.treesitter.LanguageTree
|
---@field tree vim.treesitter.LanguageTree
|
||||||
---@field private redraw_count integer
|
---@field private redraw_count integer
|
||||||
|
---@field parsing boolean true if we are parsing asynchronously
|
||||||
local TSHighlighter = {
|
local TSHighlighter = {
|
||||||
active = {},
|
active = {},
|
||||||
}
|
}
|
||||||
@ -147,7 +148,7 @@ function TSHighlighter.new(tree, opts)
|
|||||||
vim.opt_local.spelloptions:append('noplainbuffer')
|
vim.opt_local.spelloptions:append('noplainbuffer')
|
||||||
end)
|
end)
|
||||||
|
|
||||||
self.tree:parse()
|
self.tree:parse(nil, function() end)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
@ -384,19 +385,23 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@private
|
---@private
|
||||||
---@param _win integer
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
---@param topline integer
|
---@param topline integer
|
||||||
---@param botline integer
|
---@param botline integer
|
||||||
function TSHighlighter._on_win(_, _win, buf, topline, botline)
|
function TSHighlighter._on_win(_, _, buf, topline, botline)
|
||||||
local self = TSHighlighter.active[buf]
|
local self = TSHighlighter.active[buf]
|
||||||
if not self then
|
if not self or self.parsing then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
self.tree:parse({ topline, botline + 1 })
|
self.parsing = self.tree:parse({ topline, botline + 1 }, function(_, trees)
|
||||||
self:prepare_highlight_states(topline, botline + 1)
|
if trees and self.parsing then
|
||||||
|
self.parsing = false
|
||||||
|
api.nvim__redraw({ buf = buf, valid = false, flush = false })
|
||||||
|
end
|
||||||
|
end) == nil
|
||||||
self.redraw_count = self.redraw_count + 1
|
self.redraw_count = self.redraw_count + 1
|
||||||
return true
|
self:prepare_highlight_states(topline, botline)
|
||||||
|
return #self._highlight_states > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
api.nvim_set_decoration_provider(ns, {
|
api.nvim_set_decoration_provider(ns, {
|
||||||
|
@ -44,6 +44,8 @@ local query = require('vim.treesitter.query')
|
|||||||
local language = require('vim.treesitter.language')
|
local language = require('vim.treesitter.language')
|
||||||
local Range = require('vim.treesitter._range')
|
local Range = require('vim.treesitter._range')
|
||||||
|
|
||||||
|
local default_parse_timeout_ms = 3
|
||||||
|
|
||||||
---@alias TSCallbackName
|
---@alias TSCallbackName
|
||||||
---| 'changedtree'
|
---| 'changedtree'
|
||||||
---| 'bytes'
|
---| 'bytes'
|
||||||
@ -76,6 +78,10 @@ local TSCallbackNames = {
|
|||||||
---@field private _injections_processed boolean
|
---@field private _injections_processed boolean
|
||||||
---@field private _opts table Options
|
---@field private _opts table Options
|
||||||
---@field private _parser TSParser Parser for language
|
---@field private _parser TSParser Parser for language
|
||||||
|
---Table of regions for which the tree is currently running an async parse
|
||||||
|
---@field private _ranges_being_parsed table<string, boolean>
|
||||||
|
---Table of callback queues, keyed by each region for which the callbacks should be run
|
||||||
|
---@field private _cb_queues table<string, fun(err?: string, trees?: table<integer, TSTree>)[]>
|
||||||
---@field private _has_regions boolean
|
---@field private _has_regions boolean
|
||||||
---@field private _regions table<integer, Range6[]>?
|
---@field private _regions table<integer, Range6[]>?
|
||||||
---List of regions this tree should manage and parse. If nil then regions are
|
---List of regions this tree should manage and parse. If nil then regions are
|
||||||
@ -130,6 +136,8 @@ function LanguageTree.new(source, lang, opts)
|
|||||||
_injections_processed = false,
|
_injections_processed = false,
|
||||||
_valid = false,
|
_valid = false,
|
||||||
_parser = vim._create_ts_parser(lang),
|
_parser = vim._create_ts_parser(lang),
|
||||||
|
_ranges_being_parsed = {},
|
||||||
|
_cb_queues = {},
|
||||||
_callbacks = {},
|
_callbacks = {},
|
||||||
_callbacks_rec = {},
|
_callbacks_rec = {},
|
||||||
}
|
}
|
||||||
@ -232,6 +240,7 @@ end
|
|||||||
---@param reload boolean|nil
|
---@param reload boolean|nil
|
||||||
function LanguageTree:invalidate(reload)
|
function LanguageTree:invalidate(reload)
|
||||||
self._valid = false
|
self._valid = false
|
||||||
|
self._parser:reset()
|
||||||
|
|
||||||
-- buffer was reloaded, reparse all trees
|
-- buffer was reloaded, reparse all trees
|
||||||
if reload then
|
if reload then
|
||||||
@ -334,10 +343,12 @@ end
|
|||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
--- @param range boolean|Range?
|
--- @param range boolean|Range?
|
||||||
|
--- @param timeout integer?
|
||||||
--- @return Range6[] changes
|
--- @return Range6[] changes
|
||||||
--- @return integer no_regions_parsed
|
--- @return integer no_regions_parsed
|
||||||
--- @return number total_parse_time
|
--- @return number total_parse_time
|
||||||
function LanguageTree:_parse_regions(range)
|
--- @return boolean finished whether async parsing still needs time
|
||||||
|
function LanguageTree:_parse_regions(range, timeout)
|
||||||
local changes = {}
|
local changes = {}
|
||||||
local no_regions_parsed = 0
|
local no_regions_parsed = 0
|
||||||
local total_parse_time = 0
|
local total_parse_time = 0
|
||||||
@ -357,9 +368,14 @@ function LanguageTree:_parse_regions(range)
|
|||||||
)
|
)
|
||||||
then
|
then
|
||||||
self._parser:set_included_ranges(ranges)
|
self._parser:set_included_ranges(ranges)
|
||||||
|
self._parser:set_timeout(timeout and timeout * 1000 or 0) -- ms -> micros
|
||||||
local parse_time, tree, tree_changes =
|
local parse_time, tree, tree_changes =
|
||||||
tcall(self._parser.parse, self._parser, self._trees[i], self._source, true)
|
tcall(self._parser.parse, self._parser, self._trees[i], self._source, true)
|
||||||
|
|
||||||
|
if not tree then
|
||||||
|
return changes, no_regions_parsed, total_parse_time, false
|
||||||
|
end
|
||||||
|
|
||||||
-- Pass ranges if this is an initial parse
|
-- Pass ranges if this is an initial parse
|
||||||
local cb_changes = self._trees[i] and tree_changes or tree:included_ranges(true)
|
local cb_changes = self._trees[i] and tree_changes or tree:included_ranges(true)
|
||||||
|
|
||||||
@ -373,7 +389,7 @@ function LanguageTree:_parse_regions(range)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return changes, no_regions_parsed, total_parse_time
|
return changes, no_regions_parsed, total_parse_time, true
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
@ -409,6 +425,82 @@ function LanguageTree:_add_injections()
|
|||||||
return query_time
|
return query_time
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @param range boolean|Range?
|
||||||
|
--- @return string
|
||||||
|
local function range_to_string(range)
|
||||||
|
return type(range) == 'table' and table.concat(range, ',') or tostring(range)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- @param range boolean|Range?
|
||||||
|
--- @param callback fun(err?: string, trees?: table<integer, TSTree>)
|
||||||
|
function LanguageTree:_push_async_callback(range, callback)
|
||||||
|
local key = range_to_string(range)
|
||||||
|
self._cb_queues[key] = self._cb_queues[key] or {}
|
||||||
|
local queue = self._cb_queues[key]
|
||||||
|
queue[#queue + 1] = callback
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- @param range boolean|Range?
|
||||||
|
--- @param err? string
|
||||||
|
--- @param trees? table<integer, TSTree>
|
||||||
|
function LanguageTree:_run_async_callbacks(range, err, trees)
|
||||||
|
local key = range_to_string(range)
|
||||||
|
for _, cb in ipairs(self._cb_queues[key]) do
|
||||||
|
cb(err, trees)
|
||||||
|
end
|
||||||
|
self._ranges_being_parsed[key] = false
|
||||||
|
self._cb_queues[key] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Run an asynchronous parse, calling {on_parse} when complete.
|
||||||
|
---
|
||||||
|
--- @private
|
||||||
|
--- @param range boolean|Range?
|
||||||
|
--- @param on_parse fun(err?: string, trees?: table<integer, TSTree>)
|
||||||
|
--- @return table<integer, TSTree>? trees the list of parsed trees, if parsing completed synchronously
|
||||||
|
function LanguageTree:_async_parse(range, on_parse)
|
||||||
|
self:_push_async_callback(range, on_parse)
|
||||||
|
|
||||||
|
-- If we are already running an async parse, just queue the callback.
|
||||||
|
local range_string = range_to_string(range)
|
||||||
|
if not self._ranges_being_parsed[range_string] then
|
||||||
|
self._ranges_being_parsed[range_string] = true
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local buf = vim.b[self._source]
|
||||||
|
local ct = buf.changedtick
|
||||||
|
local total_parse_time = 0
|
||||||
|
local redrawtime = vim.o.redrawtime
|
||||||
|
local timeout = not vim.g._ts_force_sync_parsing and default_parse_timeout_ms or nil
|
||||||
|
|
||||||
|
local function step()
|
||||||
|
-- If buffer was changed in the middle of parsing, reset parse state
|
||||||
|
if buf.changedtick ~= ct then
|
||||||
|
ct = buf.changedtick
|
||||||
|
total_parse_time = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local parse_time, trees, finished = tcall(self._parse, self, range, timeout)
|
||||||
|
total_parse_time = total_parse_time + parse_time
|
||||||
|
|
||||||
|
if finished then
|
||||||
|
self:_run_async_callbacks(range, nil, trees)
|
||||||
|
return trees
|
||||||
|
elseif total_parse_time > redrawtime then
|
||||||
|
self:_run_async_callbacks(range, 'TIMEOUT', nil)
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
vim.schedule(step)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return step()
|
||||||
|
end
|
||||||
|
|
||||||
--- Recursively parse all regions in the language tree using |treesitter-parsers|
|
--- Recursively parse all regions in the language tree using |treesitter-parsers|
|
||||||
--- for the corresponding languages and run injection queries on the parsed trees
|
--- for the corresponding languages and run injection queries on the parsed trees
|
||||||
--- to determine whether child trees should be created and parsed.
|
--- to determine whether child trees should be created and parsed.
|
||||||
@ -420,11 +512,33 @@ end
|
|||||||
--- Set to `true` to run a complete parse of the source (Note: Can be slow!)
|
--- Set to `true` to run a complete parse of the source (Note: Can be slow!)
|
||||||
--- Set to `false|nil` to only parse regions with empty ranges (typically
|
--- Set to `false|nil` to only parse regions with empty ranges (typically
|
||||||
--- only the root tree without injections).
|
--- only the root tree without injections).
|
||||||
--- @return table<integer, TSTree>
|
--- @param on_parse fun(err?: string, trees?: table<integer, TSTree>)? Function invoked when parsing completes.
|
||||||
function LanguageTree:parse(range)
|
--- When provided and `vim.g._ts_force_sync_parsing` is not set, parsing will run
|
||||||
|
--- asynchronously. The first argument to the function is a string respresenting the error type,
|
||||||
|
--- in case of a failure (currently only possible for timeouts). The second argument is the list
|
||||||
|
--- of trees returned by the parse (upon success), or `nil` if the parse timed out (determined
|
||||||
|
--- by 'redrawtime').
|
||||||
|
---
|
||||||
|
--- If parsing was still able to finish synchronously (within 3ms), `parse()` returns the list
|
||||||
|
--- of trees. Otherwise, it returns `nil`.
|
||||||
|
--- @return table<integer, TSTree>?
|
||||||
|
function LanguageTree:parse(range, on_parse)
|
||||||
|
if on_parse then
|
||||||
|
return self:_async_parse(range, on_parse)
|
||||||
|
end
|
||||||
|
local trees, _ = self:_parse(range)
|
||||||
|
return trees
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- @param range boolean|Range|nil
|
||||||
|
--- @param timeout integer?
|
||||||
|
--- @return table<integer, TSTree> trees
|
||||||
|
--- @return boolean finished
|
||||||
|
function LanguageTree:_parse(range, timeout)
|
||||||
if self:is_valid() then
|
if self:is_valid() then
|
||||||
self:_log('valid')
|
self:_log('valid')
|
||||||
return self._trees
|
return self._trees, true
|
||||||
end
|
end
|
||||||
|
|
||||||
local changes --- @type Range6[]?
|
local changes --- @type Range6[]?
|
||||||
@ -433,10 +547,15 @@ function LanguageTree:parse(range)
|
|||||||
local no_regions_parsed = 0
|
local no_regions_parsed = 0
|
||||||
local query_time = 0
|
local query_time = 0
|
||||||
local total_parse_time = 0
|
local total_parse_time = 0
|
||||||
|
local is_finished --- @type boolean
|
||||||
|
|
||||||
-- At least 1 region is invalid
|
-- At least 1 region is invalid
|
||||||
if not self:is_valid(true) then
|
if not self:is_valid(true) then
|
||||||
changes, no_regions_parsed, total_parse_time = self:_parse_regions(range)
|
changes, no_regions_parsed, total_parse_time, is_finished = self:_parse_regions(range, timeout)
|
||||||
|
timeout = timeout and math.max(timeout - total_parse_time, 0)
|
||||||
|
if not is_finished then
|
||||||
|
return self._trees, is_finished
|
||||||
|
end
|
||||||
-- Need to run injections when we parsed something
|
-- Need to run injections when we parsed something
|
||||||
if no_regions_parsed > 0 then
|
if no_regions_parsed > 0 then
|
||||||
self._injections_processed = false
|
self._injections_processed = false
|
||||||
@ -457,10 +576,17 @@ function LanguageTree:parse(range)
|
|||||||
})
|
})
|
||||||
|
|
||||||
for _, child in pairs(self._children) do
|
for _, child in pairs(self._children) do
|
||||||
child:parse(range)
|
if timeout == 0 then
|
||||||
|
return self._trees, false
|
||||||
|
end
|
||||||
|
local ctime, _, child_finished = tcall(child._parse, child, range, timeout)
|
||||||
|
timeout = timeout and math.max(timeout - ctime, 0)
|
||||||
|
if not child_finished then
|
||||||
|
return self._trees, child_finished
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return self._trees
|
return self._trees, true
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Invokes the callback for each |LanguageTree| recursively.
|
--- Invokes the callback for each |LanguageTree| recursively.
|
||||||
@ -907,6 +1033,7 @@ function LanguageTree:_edit(
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self._parser:reset()
|
||||||
self._regions = nil
|
self._regions = nil
|
||||||
|
|
||||||
local changed_range = {
|
local changed_range = {
|
||||||
|
@ -913,8 +913,8 @@ end
|
|||||||
---@param start? integer Starting line for the search. Defaults to `node:start()`.
|
---@param start? integer Starting line for the search. Defaults to `node:start()`.
|
||||||
---@param stop? integer Stopping line for the search (end-exclusive). Defaults to `node:end_()`.
|
---@param stop? integer Stopping line for the search (end-exclusive). Defaults to `node:end_()`.
|
||||||
---
|
---
|
||||||
---@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch):
|
---@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree):
|
||||||
--- capture id, capture node, metadata, match
|
--- 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)
|
function Query:iter_captures(node, source, start, stop)
|
||||||
@ -924,6 +924,8 @@ function Query:iter_captures(node, source, start, stop)
|
|||||||
|
|
||||||
start, stop = value_or_node_range(start, stop, node)
|
start, stop = value_or_node_range(start, stop, node)
|
||||||
|
|
||||||
|
-- Copy the tree to ensure it is valid during the entire lifetime of the iterator
|
||||||
|
local tree = node:tree():copy()
|
||||||
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, { match_limit = 256 })
|
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, { match_limit = 256 })
|
||||||
|
|
||||||
-- For faster checks that a match is not in the cache.
|
-- For faster checks that a match is not in the cache.
|
||||||
@ -970,7 +972,7 @@ function Query:iter_captures(node, source, start, stop)
|
|||||||
match_cache[match_id] = metadata
|
match_cache[match_id] = metadata
|
||||||
end
|
end
|
||||||
|
|
||||||
return capture, captured_node, metadata, match
|
return capture, captured_node, metadata, match, tree
|
||||||
end
|
end
|
||||||
return iter
|
return iter
|
||||||
end
|
end
|
||||||
@ -1011,7 +1013,7 @@ end
|
|||||||
--- (last) node instead of the full list of matching nodes. This option is only for backward
|
--- (last) node instead of the full list of matching nodes. This option is only for backward
|
||||||
--- compatibility and will be removed in a future release.
|
--- compatibility and will be removed in a future release.
|
||||||
---
|
---
|
||||||
---@return (fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata): pattern id, match, metadata
|
---@return (fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata, TSTree): pattern id, match, metadata, tree
|
||||||
function Query:iter_matches(node, source, start, stop, opts)
|
function Query:iter_matches(node, source, start, stop, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
opts.match_limit = opts.match_limit or 256
|
opts.match_limit = opts.match_limit or 256
|
||||||
@ -1022,6 +1024,8 @@ 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)
|
||||||
|
|
||||||
|
-- Copy the tree to ensure it is valid during the entire lifetime of the iterator
|
||||||
|
local tree = node:tree():copy()
|
||||||
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts)
|
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts)
|
||||||
|
|
||||||
local function iter()
|
local function iter()
|
||||||
@ -1059,7 +1063,7 @@ function Query:iter_matches(node, source, start, stop, opts)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- TODO(lewis6991): create a new function that returns {match, metadata}
|
-- TODO(lewis6991): create a new function that returns {match, metadata}
|
||||||
return pattern_i, captures, metadata
|
return pattern_i, captures, metadata, tree
|
||||||
end
|
end
|
||||||
return iter
|
return iter
|
||||||
end
|
end
|
||||||
|
@ -489,7 +489,11 @@ static int parser_parse(lua_State *L)
|
|||||||
// Sometimes parsing fails (timeout, or wrong parser ABI)
|
// Sometimes parsing fails (timeout, or wrong parser ABI)
|
||||||
// In those case, just return an error.
|
// In those case, just return an error.
|
||||||
if (!new_tree) {
|
if (!new_tree) {
|
||||||
return luaL_error(L, "An error occurred when parsing.");
|
if (ts_parser_timeout_micros(p) == 0) {
|
||||||
|
// No timeout set, must have had an error
|
||||||
|
return luaL_error(L, "An error occurred when parsing.");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The new tree will be pushed to the stack, without copy, ownership is now to the lua GC.
|
// The new tree will be pushed to the stack, without copy, ownership is now to the lua GC.
|
||||||
|
@ -6520,8 +6520,8 @@ return {
|
|||||||
defaults = { if_true = 2000 },
|
defaults = { if_true = 2000 },
|
||||||
desc = [=[
|
desc = [=[
|
||||||
Time in milliseconds for redrawing the display. Applies to
|
Time in milliseconds for redrawing the display. Applies to
|
||||||
'hlsearch', 'inccommand', |:match| highlighting and syntax
|
'hlsearch', 'inccommand', |:match| highlighting, syntax highlighting,
|
||||||
highlighting.
|
and async |LanguageTree:parse()|.
|
||||||
When redrawing takes more than this many milliseconds no further
|
When redrawing takes more than this many milliseconds no further
|
||||||
matches will be highlighted.
|
matches will be highlighted.
|
||||||
For syntax highlighting the time applies per window. When over the
|
For syntax highlighting the time applies per window. When over the
|
||||||
|
@ -10,6 +10,7 @@ local exec_lua = n.exec_lua
|
|||||||
local pcall_err = t.pcall_err
|
local pcall_err = t.pcall_err
|
||||||
local feed = n.feed
|
local feed = n.feed
|
||||||
local run_query = ts_t.run_query
|
local run_query = ts_t.run_query
|
||||||
|
local assert_alive = n.assert_alive
|
||||||
|
|
||||||
describe('treesitter parser API', function()
|
describe('treesitter parser API', function()
|
||||||
before_each(function()
|
before_each(function()
|
||||||
@ -90,6 +91,197 @@ describe('treesitter parser API', function()
|
|||||||
eq(true, exec_lua('return parser:parse()[1] == tree2'))
|
eq(true, exec_lua('return parser:parse()[1] == tree2'))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('parses buffer asynchronously', function()
|
||||||
|
insert([[
|
||||||
|
int main() {
|
||||||
|
int x = 3;
|
||||||
|
}]])
|
||||||
|
|
||||||
|
exec_lua(function()
|
||||||
|
_G.parser = vim.treesitter.get_parser(0, 'c')
|
||||||
|
_G.lang = vim.treesitter.language.inspect('c')
|
||||||
|
_G.parser:parse(nil, function(_, trees)
|
||||||
|
_G.tree = trees[1]
|
||||||
|
_G.root = _G.tree:root()
|
||||||
|
end)
|
||||||
|
vim.wait(100, function() end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
eq('<tree>', exec_lua('return tostring(tree)'))
|
||||||
|
eq('<node translation_unit>', exec_lua('return tostring(root)'))
|
||||||
|
eq({ 0, 0, 3, 0 }, exec_lua('return {root:range()}'))
|
||||||
|
|
||||||
|
eq(1, exec_lua('return root:child_count()'))
|
||||||
|
exec_lua('child = root:child(0)')
|
||||||
|
eq('<node function_definition>', exec_lua('return tostring(child)'))
|
||||||
|
eq({ 0, 0, 2, 1 }, exec_lua('return {child:range()}'))
|
||||||
|
|
||||||
|
eq('function_definition', exec_lua('return child:type()'))
|
||||||
|
eq(true, exec_lua('return child:named()'))
|
||||||
|
eq('number', type(exec_lua('return child:symbol()')))
|
||||||
|
eq(true, exec_lua('return lang.symbols[child:type()]'))
|
||||||
|
|
||||||
|
exec_lua('anon = root:descendant_for_range(0,8,0,9)')
|
||||||
|
eq('(', exec_lua('return anon:type()'))
|
||||||
|
eq(false, exec_lua('return anon:named()'))
|
||||||
|
eq('number', type(exec_lua('return anon:symbol()')))
|
||||||
|
eq(false, exec_lua([=[return lang.symbols[string.format('"%s"', anon:type())]]=]))
|
||||||
|
|
||||||
|
exec_lua('descendant = root:descendant_for_range(1,2,1,12)')
|
||||||
|
eq('<node declaration>', exec_lua('return tostring(descendant)'))
|
||||||
|
eq({ 1, 2, 1, 12 }, exec_lua('return {descendant:range()}'))
|
||||||
|
eq(
|
||||||
|
'(declaration type: (primitive_type) declarator: (init_declarator declarator: (identifier) value: (number_literal)))',
|
||||||
|
exec_lua('return descendant:sexpr()')
|
||||||
|
)
|
||||||
|
|
||||||
|
feed('2G7|ay')
|
||||||
|
exec_lua(function()
|
||||||
|
_G.parser:parse(nil, function(_, trees)
|
||||||
|
_G.tree2 = trees[1]
|
||||||
|
_G.root2 = _G.tree2:root()
|
||||||
|
_G.descendant2 = _G.root2:descendant_for_range(1, 2, 1, 13)
|
||||||
|
end)
|
||||||
|
vim.wait(100, function() end)
|
||||||
|
end)
|
||||||
|
eq(false, exec_lua('return tree2 == tree1'))
|
||||||
|
eq(false, exec_lua('return root2 == root'))
|
||||||
|
eq('<node declaration>', exec_lua('return tostring(descendant2)'))
|
||||||
|
eq({ 1, 2, 1, 13 }, exec_lua('return {descendant2:range()}'))
|
||||||
|
|
||||||
|
eq(true, exec_lua('return child == child'))
|
||||||
|
-- separate lua object, but represents same node
|
||||||
|
eq(true, exec_lua('return child == root:child(0)'))
|
||||||
|
eq(false, exec_lua('return child == descendant2'))
|
||||||
|
eq(false, exec_lua('return child == nil'))
|
||||||
|
eq(false, exec_lua('return child == tree'))
|
||||||
|
|
||||||
|
eq('string', exec_lua('return type(child:id())'))
|
||||||
|
eq(true, exec_lua('return child:id() == child:id()'))
|
||||||
|
-- separate lua object, but represents same node
|
||||||
|
eq(true, exec_lua('return child:id() == root:child(0):id()'))
|
||||||
|
eq(false, exec_lua('return child:id() == descendant2:id()'))
|
||||||
|
eq(false, exec_lua('return child:id() == nil'))
|
||||||
|
eq(false, exec_lua('return child:id() == tree'))
|
||||||
|
|
||||||
|
-- unchanged buffer: return the same tree
|
||||||
|
eq(true, exec_lua('return parser:parse()[1] == tree2'))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('does not crash when editing large files', function()
|
||||||
|
insert([[printf("%s", "some text");]])
|
||||||
|
feed('yy49999p')
|
||||||
|
|
||||||
|
exec_lua(function()
|
||||||
|
_G.parser = vim.treesitter.get_parser(0, 'c')
|
||||||
|
_G.done = false
|
||||||
|
vim.treesitter.start(0, 'c')
|
||||||
|
_G.parser:parse(nil, function()
|
||||||
|
_G.done = true
|
||||||
|
end)
|
||||||
|
while not _G.done do
|
||||||
|
-- Busy wait until async parsing has completed
|
||||||
|
vim.wait(100, function() end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
eq(true, exec_lua([[return done]]))
|
||||||
|
exec_lua(function()
|
||||||
|
vim.api.nvim_input('Lxj')
|
||||||
|
end)
|
||||||
|
exec_lua(function()
|
||||||
|
vim.api.nvim_input('xj')
|
||||||
|
end)
|
||||||
|
exec_lua(function()
|
||||||
|
vim.api.nvim_input('xj')
|
||||||
|
end)
|
||||||
|
assert_alive()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('resets parsing state on tree changes', function()
|
||||||
|
insert([[vim.api.nvim_set_hl(0, 'test2', { bg = 'green' })]])
|
||||||
|
feed('yy1000p')
|
||||||
|
|
||||||
|
exec_lua(function()
|
||||||
|
vim.cmd('set ft=lua')
|
||||||
|
|
||||||
|
vim.treesitter.start(0)
|
||||||
|
local parser = assert(vim.treesitter.get_parser(0))
|
||||||
|
|
||||||
|
parser:parse(true, function() end)
|
||||||
|
vim.api.nvim_buf_set_lines(0, 1, -1, false, {})
|
||||||
|
parser:parse(true)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('resets when buffer was editing during an async parse', function()
|
||||||
|
insert([[printf("%s", "some text");]])
|
||||||
|
feed('yy49999p')
|
||||||
|
feed('gg4jO// Comment<Esc>')
|
||||||
|
|
||||||
|
exec_lua(function()
|
||||||
|
_G.parser = vim.treesitter.get_parser(0, 'c')
|
||||||
|
_G.done = false
|
||||||
|
vim.treesitter.start(0, 'c')
|
||||||
|
_G.parser:parse(nil, function()
|
||||||
|
_G.done = true
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
exec_lua(function()
|
||||||
|
vim.api.nvim_input('ggdj')
|
||||||
|
end)
|
||||||
|
|
||||||
|
eq(false, exec_lua([[return done]]))
|
||||||
|
exec_lua(function()
|
||||||
|
while not _G.done do
|
||||||
|
-- Busy wait until async parsing finishes
|
||||||
|
vim.wait(100, function() end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
eq(true, exec_lua([[return done]]))
|
||||||
|
eq('comment', exec_lua([[return parser:parse()[1]:root():named_child(2):type()]]))
|
||||||
|
eq({ 2, 0, 2, 10 }, exec_lua([[return {parser:parse()[1]:root():named_child(2):range()}]]))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('handles multiple async parse calls', function()
|
||||||
|
insert([[printf("%s", "some text");]])
|
||||||
|
feed('yy49999p')
|
||||||
|
|
||||||
|
exec_lua(function()
|
||||||
|
-- Spy on vim.schedule
|
||||||
|
local schedule = vim.schedule
|
||||||
|
vim.schedule = function(fn)
|
||||||
|
_G.schedules = _G.schedules + 1
|
||||||
|
schedule(fn)
|
||||||
|
end
|
||||||
|
_G.schedules = 0
|
||||||
|
_G.parser = vim.treesitter.get_parser(0, 'c')
|
||||||
|
for i = 1, 5 do
|
||||||
|
_G['done' .. i] = false
|
||||||
|
_G.parser:parse(nil, function()
|
||||||
|
_G['done' .. i] = true
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
schedule(function()
|
||||||
|
_G.schedules_snapshot = _G.schedules
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
eq(2, exec_lua([[return schedules_snapshot]]))
|
||||||
|
eq(
|
||||||
|
{ false, false, false, false, false },
|
||||||
|
exec_lua([[return { done1, done2, done3, done4, done5 }]])
|
||||||
|
)
|
||||||
|
exec_lua(function()
|
||||||
|
while not _G.done1 do
|
||||||
|
-- Busy wait until async parsing finishes
|
||||||
|
vim.wait(100, function() end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
eq({ true, true, true, true, true }, exec_lua([[return { done1, done2, done3, done4, done5 }]]))
|
||||||
|
end)
|
||||||
|
|
||||||
local test_text = [[
|
local test_text = [[
|
||||||
void ui_refresh(void)
|
void ui_refresh(void)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user