diff options
Diffstat (limited to 'runtime')
-rw-r--r-- | runtime/doc/lsp.txt | 120 | ||||
-rw-r--r-- | runtime/doc/lua.txt | 67 | ||||
-rw-r--r-- | runtime/doc/news.txt | 5 | ||||
-rw-r--r-- | runtime/doc/treesitter.txt | 78 | ||||
-rw-r--r-- | runtime/lua/vim/_init_packages.lua | 5 | ||||
-rw-r--r-- | runtime/lua/vim/_inspector.lua | 97 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/semantic_tokens.lua | 183 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 26 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_range.lua | 20 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 34 | ||||
-rw-r--r-- | runtime/lua/vim/version.lua | 277 |
11 files changed, 749 insertions, 163 deletions
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 7e46698614..0d7e8e7ab4 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -482,6 +482,71 @@ LspSignatureActiveParameter Used to highlight the active parameter in the signature help. See |vim.lsp.handlers.signature_help()|. +------------------------------------------------------------------------------ +LSP SEMANTIC HIGHLIGHTS *lsp-semantic-highlight* + +When available, the LSP client highlights code using |lsp-semantic_tokens|, +which are another way that LSP servers can provide information about source +code. Note that this is in addition to treesitter syntax highlighting; +semantic highlighting does not replace syntax highlighting. + +The server will typically provide one token per identifier in the source code. +The token will have a `type` such as "function" or "variable", and 0 or more +`modifier`s such as "readonly" or "deprecated." The standard types and +modifiers are described here: +https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens +LSP servers may also use off-spec types and modifiers. + +The LSP client adds one or more highlights for each token. The highlight +groups are derived from the token's type and modifiers: + • `@lsp.type.<type>.<ft>` for the type + • `@lsp.mod.<mod>.<ft>` for each modifier + • `@lsp.typemod.<type>.<mod>.<ft>` for each modifier +Use |:Inspect| to view the higlights for a specific token. Use |:hi| or +|nvim_set_hl()| to change the appearance of semantic highlights: >vim + + hi @lsp.type.function guifg=Yellow " function names are yellow + hi @lsp.type.variable.lua guifg=Green " variables in lua are green + hi @lsp.mod.deprecated gui=strikethrough " deprecated is crossed out + hi @lsp.typemod.function.async guifg=Blue " async functions are blue +< +The value |vim.highlight.priorities|`.semantic_tokens` is the priority of the +`@lsp.type.*` highlights. The `@lsp.mod.*` and `@lsp.typemod.*` highlights +have priorities one and two higher, respectively. + +You can disable semantic highlights by clearing the highlight groups: >lua + + -- Hide semantic highlights for functions + vim.api.nvim_set_hl(0, '@lsp.type.function', {}) + + -- Hide all semantic highlights + for _, group in ipairs(vim.fn.getcompletion("@lsp", "highlight")) do + vim.api.nvim_set_hl(0, group, {}) + end +< +You probably want these inside a |ColorScheme| autocommand. + +Use |LspTokenUpdate| and |vim.lsp.semantic_tokens.highlight_token()| for more +complex highlighting. + +The following groups are linked by default to standard |group-name|s: +> + @lsp.type.class Structure + @lsp.type.decorator Function + @lsp.type.enum Structure + @lsp.type.enumMember Constant + @lsp.type.function Function + @lsp.type.interface Structure + @lsp.type.macro Macro + @lsp.type.method Function + @lsp.type.namespace Structure + @lsp.type.parameter Identifier + @lsp.type.property Identifier + @lsp.type.struct Structure + @lsp.type.type Type + @lsp.type.typeParameter TypeDef + @lsp.type.variable Identifier +< ============================================================================== EVENTS *lsp-events* @@ -516,6 +581,29 @@ callback in the "data" table. Example: >lua end, }) < + +LspTokenUpdate *LspTokenUpdate* + +When a visible semantic token is sent or updated by the LSP server, or when an +existing token becomes visible for the first time. The |autocmd-pattern| is +the name of the buffer. When used from Lua, the token and client ID are passed +to the callback in the "data" table. The token fields are documented in +|vim.lsp.semantic_tokens.get_at_pos()|. Example: >lua + + vim.api.nvim_create_autocmd('LspTokenUpdate', { + callback = function(args) + local token = args.data.token + if token.type == 'variable' and not token.modifiers.readonly then + vim.lsp.semantic_tokens.highlight_token( + token, args.buf, args.data.client_id, 'MyMutableVariableHighlight' + ) + end + end, + }) +< +Note: doing anything other than calling +|vim.lsp.semantic_tokens.highlight_token()| is considered experimental. + Also the following |User| |autocommand|s are provided: LspProgressUpdate *LspProgressUpdate* @@ -1332,7 +1420,8 @@ force_refresh({bufnr}) *vim.lsp.semantic_tokens.force_refresh()* highlighting (|vim.lsp.semantic_tokens.start()| has been called for it) Parameters: ~ - • {bufnr} (nil|number) default: current buffer + • {bufnr} (number|nil) filter by buffer. All buffers if nil, current + buffer if 0 *vim.lsp.semantic_tokens.get_at_pos()* get_at_pos({bufnr}, {row}, {col}) @@ -1345,7 +1434,34 @@ get_at_pos({bufnr}, {row}, {col}) • {col} (number|nil) Position column (default cursor position) Return: ~ - (table|nil) List of tokens at position + (table|nil) List of tokens at position. Each token has the following + fields: + • line (number) line number, 0-based + • start_col (number) start column, 0-based + • end_col (number) end column, 0-based + • type (string) token type as string, e.g. "variable" + • modifiers (table) token modifiers as a set. E.g., { static = true, + readonly = true } + + *vim.lsp.semantic_tokens.highlight_token()* +highlight_token({token}, {bufnr}, {client_id}, {hl_group}, {opts}) + Highlight a semantic token. + + Apply an extmark with a given highlight group for a semantic token. The + mark will be deleted by the semantic token engine when appropriate; for + example, when the LSP sends updated tokens. This function is intended for + use inside |LspTokenUpdate| callbacks. + + Parameters: ~ + • {token} (table) a semantic token, found as `args.data.token` in + |LspTokenUpdate|. + • {bufnr} (number) the buffer to highlight + • {client_id} (number) The ID of the |vim.lsp.client| + • {hl_group} (string) Highlight group name + • {opts} (table|nil) Optional parameters. + • priority: (number|nil) Priority for the applied + extmark. Defaults to + `vim.highlight.priorities.semantic_tokens + 3` start({bufnr}, {client_id}, {opts}) *vim.lsp.semantic_tokens.start()* Start the semantic token highlighting engine for the given buffer with the diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index cb309eaf1a..bcd68b7608 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2503,4 +2503,71 @@ trust({opts}) *vim.secure.trust()* • true and full path of target file if operation was successful • false and error message on failure + +============================================================================== +Lua module: version *lua-version* + +cmp({v1}, {v2}, {opts}) *vim.version.cmp()* + Compares two strings ( `v1` and `v2` ) in semver format. + + Parameters: ~ + • {v1} (string) Version. + • {v2} (string) Version to compare with v1. + • {opts} (table|nil) Optional keyword arguments: + • strict (boolean): see `semver.parse` for details. Defaults + to false. + + Return: ~ + (integer) `-1` if `v1 < v2`, `0` if `v1 == v2`, `1` if `v1 > v2`. + +eq({v1}, {v2}) *vim.version.eq()* + Returns `true` if `v1` are `v2` are equal versions. + + Parameters: ~ + • {v1} (string) + • {v2} (string) + + Return: ~ + (boolean) + +gt({v1}, {v2}) *vim.version.gt()* + Returns `true` if `v1` is greater than `v2` . + + Parameters: ~ + • {v1} (string) + • {v2} (string) + + Return: ~ + (boolean) + +lt({v1}, {v2}) *vim.version.lt()* + Returns `true` if `v1` is less than `v2` . + + Parameters: ~ + • {v1} (string) + • {v2} (string) + + Return: ~ + (boolean) + +parse({version}, {opts}) *vim.version.parse()* + Parses a semantic version string. + + Ignores leading "v" and surrounding whitespace, e.g. " + v1.0.1-rc1+build.2", "1.0.1-rc1+build.2", "v1.0.1-rc1+build.2" and + "v1.0.1-rc1+build.2 " are all parsed as: > + + { major = 1, minor = 0, patch = 1, prerelease = "rc1", build = "build.2" } +< + + Parameters: ~ + • {version} (string) Version string to be parsed. + • {opts} (table|nil) Optional keyword arguments: + • strict (boolean): Default false. If `true` , no coercion is attempted on input not strictly + conforming to semver v2.0.0 ( https://semver.org/spec/v2.0.0.html ). E.g. `parse("v1.2")` returns nil. + + Return: ~ + (table|nil) parsed_version Parsed version table or `nil` if `version` + is invalid. + vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl: diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 41b59681ae..2db1e75bf7 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -55,6 +55,9 @@ NEW FEATURES *news-features* The following new APIs or features were added. +• Added |vim.version| for parsing and comparing version strings conforming to + the semver specification, see |lua-version|. + • A new environment variable named NVIM_APPNAME enables configuring the directories where Neovim should find its configuration and state files. See `:help $NVIM_APPNAME` . @@ -87,7 +90,7 @@ The following new APIs or features were added. `semanticTokensProvider` from the LSP client's {server_capabilities} in the `LspAttach` callback. - See |lsp-semantic_tokens| for more information. + See |lsp-semantic-highlight| for more information. • |vim.treesitter.inspect_tree()| and |:InspectTree| opens a split window showing a text representation of the nodes in a language tree for the current diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index 855fc6bdc6..1f78e4d5d9 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -29,42 +29,6 @@ A parser can also be loaded manually using a full path: >lua vim.treesitter.require_language("python", "/path/to/python.so") < ============================================================================== -LANGUAGE TREES *treesitter-languagetree* - *LanguageTree* - -As buffers can contain multiple languages (e.g., Vimscript commands in a Lua -file), multiple parsers may be needed to parse the full buffer. These are -combined in a |LanguageTree| object. - -To create a LanguageTree (parser object) for a buffer and a given language, -use >lua - - tsparser = vim.treesitter.get_parser(bufnr, lang) -< -`bufnr=0` can be used for current buffer. `lang` will default to 'filetype'. -Currently, the parser will be retained for the lifetime of a buffer but this -is subject to change. A plugin should keep a reference to the parser object as -long as it wants incremental updates. - -Whenever you need to access the current syntax tree, parse the buffer: >lua - - tstree = tsparser:parse() -< -This will return a table of immutable |treesitter-tree|s that represent the -current state of the buffer. When the plugin wants to access the state after a -(possible) edit it should call `parse()` again. If the buffer wasn't edited, -the same tree will be returned again without extra work. If the buffer was -parsed before, incremental parsing will be done of the changed parts. - -Note: To use the parser directly inside a |nvim_buf_attach()| Lua callback, -you must call |vim.treesitter.get_parser()| before you register your callback. -But preferably parsing shouldn't be done directly in the change callback -anyway as they will be very frequent. Rather a plugin that does any kind of -analysis on a tree should use a timer to throttle too frequent updates. - -See |lua-treesitter-languagetree| for the list of available methods. - -============================================================================== TREESITTER TREES *treesitter-tree* *TSTree* @@ -221,7 +185,7 @@ Nvim looks for queries as `*.scm` files in a `queries` directory under purpose, e.g., `queries/lua/highlights.scm` for highlighting Lua files. By default, the first query on `runtimepath` is used (which usually implies that user config takes precedence over plugins, which take precedence over -queries bundled with Neovim). If a query should extend other queries instead +queries bundled with Nvim). If a query should extend other queries instead of replacing them, use |treesitter-query-modeline-extends|. See |lua-treesitter-query| for the list of available methods for working with @@ -321,7 +285,7 @@ Use |vim.treesitter.list_directives()| to list all available directives. TREESITTER QUERY MODELINES *treesitter-query-modeline* -Neovim supports to customize the behavior of the queries using a set of +Nvim supports to customize the behavior of the queries using a set of "modelines", that is comments in the queries starting with `;`. Here are the currently supported modeline alternatives: @@ -938,6 +902,44 @@ TSHighlighter:destroy({self}) *TSHighlighter:destroy()* ============================================================================== Lua module: vim.treesitter.languagetree *lua-treesitter-languagetree* + +A *LanguageTree* contains a tree of parsers: the root treesitter parser +for {lang} and any "injected" language parsers, which themselves may +inject other languages, recursively. For example a Lua buffer containing +some Vimscript commands needs multiple parsers to fully understand its +contents. + +To create a LanguageTree (parser object) for a given buffer and language, use: + +>lua + local parser = vim.treesitter.get_parser(bufnr, lang) +< + +(where `bufnr=0` means current buffer). `lang` defaults to 'filetype'. +Note: currently the parser is retained for the lifetime of a buffer but +this may change; a plugin should keep a reference to the parser object if +it wants incremental updates. + +Whenever you need to access the current syntax tree, parse the buffer: + +>lua + local tree = parser:parse() +< + +This returns a table of immutable |treesitter-tree| objects representing +the current state of the buffer. When the plugin wants to access the state +after a (possible) edit it must call `parse()` again. If the buffer wasn't +edited, the same tree will be returned again without extra work. If the +buffer was parsed before, incremental parsing will be done of the changed +parts. + +Note: To use the parser directly inside a |nvim_buf_attach()| Lua +callback, you must call |vim.treesitter.get_parser()| before you register +your callback. But preferably parsing shouldn't be done directly in the +change callback anyway as they will be very frequent. Rather a plugin that +does any kind of analysis on a tree should use a timer to throttle too +frequent updates. + LanguageTree:children({self}) *LanguageTree:children()* Returns a map of language to child tree. diff --git a/runtime/lua/vim/_init_packages.lua b/runtime/lua/vim/_init_packages.lua index e3a442af5e..57c0fc9122 100644 --- a/runtime/lua/vim/_init_packages.lua +++ b/runtime/lua/vim/_init_packages.lua @@ -51,7 +51,10 @@ end -- builtin functions which always should be available require('vim.shared') -vim._submodules = { inspect = true } +vim._submodules = { + inspect = true, + version = true, +} -- These are for loading runtime modules in the vim namespace lazily. setmetatable(vim, { diff --git a/runtime/lua/vim/_inspector.lua b/runtime/lua/vim/_inspector.lua index 9e91597192..92d380b08c 100644 --- a/runtime/lua/vim/_inspector.lua +++ b/runtime/lua/vim/_inspector.lua @@ -2,7 +2,7 @@ ---@field syntax boolean include syntax based highlight groups (defaults to true) ---@field treesitter boolean include treesitter based highlight groups (defaults to true) ---@field extmarks boolean|"all" include extmarks. When `all`, then extmarks without a `hl_group` will also be included (defaults to true) ----@field semantic_tokens boolean include semantic tokens (defaults to true) +---@field semantic_tokens boolean include semantic token highlights (defaults to true) local defaults = { syntax = true, treesitter = true, @@ -81,47 +81,54 @@ function vim.inspect_pos(bufnr, row, col, filter) end end - -- semantic tokens - if filter.semantic_tokens then - for _, token in ipairs(vim.lsp.semantic_tokens.get_at_pos(bufnr, row, col) or {}) do - token.hl_groups = { - type = resolve_hl({ hl_group = '@' .. token.type }), - modifiers = vim.tbl_map(function(modifier) - return resolve_hl({ hl_group = '@' .. modifier }) - end, token.modifiers or {}), - } - table.insert(results.semantic_tokens, token) + --- Convert an extmark tuple into a map-like table + --- @private + local function to_map(extmark) + extmark = { + id = extmark[1], + row = extmark[2], + col = extmark[3], + opts = resolve_hl(extmark[4]), + } + extmark.end_row = extmark.opts.end_row or extmark.row -- inclusive + extmark.end_col = extmark.opts.end_col or (extmark.col + 1) -- exclusive + return extmark + end + + --- Check if an extmark overlaps this position + --- @private + local function is_here(extmark) + return (row >= extmark.row and row <= extmark.end_row) -- within the rows of the extmark + and (row > extmark.row or col >= extmark.col) -- either not the first row, or in range of the col + and (row < extmark.end_row or col < extmark.end_col) -- either not in the last row or in range of the col + end + + -- all extmarks at this position + local extmarks = {} + for ns, nsid in pairs(vim.api.nvim_get_namespaces()) do + local ns_marks = vim.api.nvim_buf_get_extmarks(bufnr, nsid, 0, -1, { details = true }) + ns_marks = vim.tbl_map(to_map, ns_marks) + ns_marks = vim.tbl_filter(is_here, ns_marks) + for _, mark in ipairs(ns_marks) do + mark.ns_id = nsid + mark.ns = ns end + vim.list_extend(extmarks, ns_marks) + end + + if filter.semantic_tokens then + results.semantic_tokens = vim.tbl_filter(function(extmark) + return extmark.ns:find('vim_lsp_semantic_tokens') == 1 + end, extmarks) end - -- extmarks if filter.extmarks then - for ns, nsid in pairs(vim.api.nvim_get_namespaces()) do - if ns:find('vim_lsp_semantic_tokens') ~= 1 then - local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, nsid, 0, -1, { details = true }) - for _, extmark in ipairs(extmarks) do - extmark = { - ns_id = nsid, - ns = ns, - id = extmark[1], - row = extmark[2], - col = extmark[3], - opts = resolve_hl(extmark[4]), - } - local end_row = extmark.opts.end_row or extmark.row -- inclusive - local end_col = extmark.opts.end_col or (extmark.col + 1) -- exclusive - if - (filter.extmarks == 'all' or extmark.opts.hl_group) -- filter hl_group - and (row >= extmark.row and row <= end_row) -- within the rows of the extmark - and (row > extmark.row or col >= extmark.col) -- either not the first row, or in range of the col - and (row < end_row or col < end_col) -- either not in the last row or in range of the col - then - table.insert(results.extmarks, extmark) - end - end - end - end + results.extmarks = vim.tbl_filter(function(extmark) + return extmark.ns:find('vim_lsp_semantic_tokens') ~= 1 + and (filter.extmarks == 'all' or extmark.opts.hl_group) + end, extmarks) end + return results end @@ -174,16 +181,17 @@ function vim.show_pos(bufnr, row, col, filter) nl() end + -- semantic tokens if #items.semantic_tokens > 0 then append('Semantic Tokens', 'Title') nl() - for _, token in ipairs(items.semantic_tokens) do - local client = vim.lsp.get_client_by_id(token.client_id) - client = client and (' (' .. client.name .. ')') or '' - item(token.hl_groups.type, 'type' .. client) - for _, modifier in ipairs(token.hl_groups.modifiers) do - item(modifier, 'modifier' .. client) - end + local sorted_marks = vim.fn.sort(items.semantic_tokens, function(left, right) + local left_first = left.opts.priority < right.opts.priority + or left.opts.priority == right.opts.priority and left.opts.hl_group < right.opts.hl_group + return left_first and -1 or 1 + end) + for _, extmark in ipairs(sorted_marks) do + item(extmark.opts, 'priority: ' .. extmark.opts.priority) end nl() end @@ -197,6 +205,7 @@ function vim.show_pos(bufnr, row, col, filter) end nl() end + -- extmarks if #items.extmarks > 0 then append('Extmarks', 'Title') diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 24b5c6c24e..7983d066b8 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -8,8 +8,8 @@ local bit = require('bit') --- @field start_col number start column 0-based --- @field end_col number end column 0-based --- @field type string token type as string ---- @field modifiers string[] token modifiers as strings ---- @field extmark_added boolean whether this extmark has been added to the buffer yet +--- @field modifiers table token modifiers as a set. E.g., { static = true, readonly = true } +--- @field marked boolean whether this token has had extmarks applied --- --- @class STCurrentResult --- @field version number document version associated with this result @@ -36,10 +36,13 @@ local bit = require('bit') ---@field client_state table<number, STClientState> local STHighlighter = { active = {} } +--- Do a binary search of the tokens in the half-open range [lo, hi). +--- +--- Return the index i in range such that tokens[j].line < line for all j < i, and +--- tokens[j].line >= line for all j >= i, or return hi if no such index is found. +--- ---@private -local function binary_search(tokens, line) - local lo = 1 - local hi = #tokens +local function lower_bound(tokens, line, lo, hi) while lo < hi do local mid = math.floor((lo + hi) / 2) if tokens[mid].line < line then @@ -51,16 +54,34 @@ local function binary_search(tokens, line) return lo end +--- Do a binary search of the tokens in the half-open range [lo, hi). +--- +--- Return the index i in range such that tokens[j].line <= line for all j < i, and +--- tokens[j].line > line for all j >= i, or return hi if no such index is found. +--- +---@private +local function upper_bound(tokens, line, lo, hi) + while lo < hi do + local mid = math.floor((lo + hi) / 2) + if line < tokens[mid].line then + hi = mid + else + lo = mid + 1 + end + end + return lo +end + --- Extracts modifier strings from the encoded number in the token array --- ---@private ----@return string[] +---@return table<string, boolean> local function modifiers_from_number(x, modifiers_table) local modifiers = {} local idx = 1 while x > 0 do if bit.band(x, 1) == 1 then - modifiers[#modifiers + 1] = modifiers_table[idx] + modifiers[modifiers_table[idx]] = true end x = bit.rshift(x, 1) idx = idx + 1 @@ -109,7 +130,7 @@ local function tokens_to_ranges(data, bufnr, client) end_col = end_col, type = token_type, modifiers = modifiers, - extmark_added = false, + marked = false, } end end @@ -355,7 +376,7 @@ end --- ---@private function STHighlighter:on_win(topline, botline) - for _, state in pairs(self.client_state) do + for client_id, state in pairs(self.client_state) do local current_result = state.current_result if current_result.version and current_result.version == util.buf_versions[self.bufnr] then if not current_result.namespace_cleared then @@ -372,52 +393,55 @@ function STHighlighter:on_win(topline, botline) -- -- Instead, we have to use normal extmarks that can attach to locations -- in the buffer and are persisted between redraws. + -- + -- `strict = false` is necessary here for the 1% of cases where the + -- current result doesn't actually match the buffer contents. Some + -- LSP servers can respond with stale tokens on requests if they are + -- still processing changes from a didChange notification. + -- + -- LSP servers that do this _should_ follow up known stale responses + -- with a refresh notification once they've finished processing the + -- didChange notification, which would re-synchronize the tokens from + -- our end. + -- + -- The server I know of that does this is clangd when the preamble of + -- a file changes and the token request is processed with a stale + -- preamble while the new one is still being built. Once the preamble + -- finishes, clangd sends a refresh request which lets the client + -- re-synchronize the tokens. + + local set_mark = function(token, hl_group, delta) + vim.api.nvim_buf_set_extmark(self.bufnr, state.namespace, token.line, token.start_col, { + hl_group = hl_group, + end_col = token.end_col, + priority = vim.highlight.priorities.semantic_tokens + delta, + strict = false, + }) + end + + local ft = vim.bo[self.bufnr].filetype local highlights = current_result.highlights - local idx = binary_search(highlights, topline) + local first = lower_bound(highlights, topline, 1, #highlights + 1) + local last = upper_bound(highlights, botline, first, #highlights + 1) - 1 - for i = idx, #highlights do + for i = first, last do local token = highlights[i] - - if token.line > botline then - break - end - - if not token.extmark_added then - -- `strict = false` is necessary here for the 1% of cases where the - -- current result doesn't actually match the buffer contents. Some - -- LSP servers can respond with stale tokens on requests if they are - -- still processing changes from a didChange notification. - -- - -- LSP servers that do this _should_ follow up known stale responses - -- with a refresh notification once they've finished processing the - -- didChange notification, which would re-synchronize the tokens from - -- our end. - -- - -- The server I know of that does this is clangd when the preamble of - -- a file changes and the token request is processed with a stale - -- preamble while the new one is still being built. Once the preamble - -- finishes, clangd sends a refresh request which lets the client - -- re-synchronize the tokens. - api.nvim_buf_set_extmark(self.bufnr, state.namespace, token.line, token.start_col, { - hl_group = '@' .. token.type, - end_col = token.end_col, - priority = vim.highlight.priorities.semantic_tokens, - strict = false, - }) - - -- TODO(bfredl) use single extmark when hl_group supports table - if #token.modifiers > 0 then - for _, modifier in pairs(token.modifiers) do - api.nvim_buf_set_extmark(self.bufnr, state.namespace, token.line, token.start_col, { - hl_group = '@' .. modifier, - end_col = token.end_col, - priority = vim.highlight.priorities.semantic_tokens + 1, - strict = false, - }) - end + if not token.marked then + set_mark(token, string.format('@lsp.type.%s.%s', token.type, ft), 0) + for modifier, _ in pairs(token.modifiers) do + set_mark(token, string.format('@lsp.mod.%s.%s', modifier, ft), 1) + set_mark(token, string.format('@lsp.typemod.%s.%s.%s', token.type, modifier, ft), 2) end - - token.extmark_added = true + token.marked = true + + api.nvim_exec_autocmds('LspTokenUpdate', { + pattern = vim.api.nvim_buf_get_name(self.bufnr), + modeline = false, + data = { + token = token, + client_id = client_id, + }, + }) end end end @@ -588,7 +612,13 @@ end ---@param row number|nil Position row (default cursor position) ---@param col number|nil Position column (default cursor position) --- ----@return table|nil (table|nil) List of tokens at position +---@return table|nil (table|nil) List of tokens at position. Each token has +--- the following fields: +--- - line (number) line number, 0-based +--- - start_col (number) start column, 0-based +--- - end_col (number) end column, 0-based +--- - type (string) token type as string, e.g. "variable" +--- - modifiers (table) token modifiers as a set. E.g., { static = true, readonly = true } function M.get_at_pos(bufnr, row, col) if bufnr == nil or bufnr == 0 then bufnr = api.nvim_get_current_buf() @@ -608,7 +638,7 @@ function M.get_at_pos(bufnr, row, col) for client_id, client in pairs(highlighter.client_state) do local highlights = client.current_result.highlights if highlights then - local idx = binary_search(highlights, row) + local idx = lower_bound(highlights, row, 1, #highlights + 1) for i = idx, #highlights do local token = highlights[i] @@ -631,23 +661,60 @@ end --- Only has an effect if the buffer is currently active for semantic token --- highlighting (|vim.lsp.semantic_tokens.start()| has been called for it) --- ----@param bufnr (nil|number) default: current buffer +---@param bufnr (number|nil) filter by buffer. All buffers if nil, current +--- buffer if 0 function M.force_refresh(bufnr) vim.validate({ bufnr = { bufnr, 'n', true }, }) - if bufnr == nil or bufnr == 0 then - bufnr = api.nvim_get_current_buf() + local buffers = bufnr == nil and vim.tbl_keys(STHighlighter.active) + or bufnr == 0 and { api.nvim_get_current_buf() } + or { bufnr } + + for _, buffer in ipairs(buffers) do + local highlighter = STHighlighter.active[buffer] + if highlighter then + highlighter:reset() + highlighter:send_request() + end end +end +--- Highlight a semantic token. +--- +--- Apply an extmark with a given highlight group for a semantic token. The +--- mark will be deleted by the semantic token engine when appropriate; for +--- example, when the LSP sends updated tokens. This function is intended for +--- use inside |LspTokenUpdate| callbacks. +---@param token (table) a semantic token, found as `args.data.token` in +--- |LspTokenUpdate|. +---@param bufnr (number) the buffer to highlight +---@param client_id (number) The ID of the |vim.lsp.client| +---@param hl_group (string) Highlight group name +---@param opts (table|nil) Optional parameters. +--- - priority: (number|nil) Priority for the applied extmark. Defaults +--- to `vim.highlight.priorities.semantic_tokens + 3` +function M.highlight_token(token, bufnr, client_id, hl_group, opts) local highlighter = STHighlighter.active[bufnr] if not highlighter then return end - highlighter:reset() - highlighter:send_request() + local state = highlighter.client_state[client_id] + if not state then + return + end + + opts = opts or {} + local priority = opts.priority or vim.highlight.priorities.semantic_tokens + 3 + + vim.api.nvim_buf_set_extmark(bufnr, state.namespace, token.line, token.start_col, { + hl_group = hl_group, + end_col = token.end_col, + priority = priority, + strict = false, + }) end --- |lsp-handler| for the method `workspace/semanticTokens/refresh` diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index ee66ba9f9b..d13824076e 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -2,6 +2,7 @@ local a = vim.api local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') local LanguageTree = require('vim.treesitter.languagetree') +local Range = require('vim.treesitter._range') ---@type table<integer,LanguageTree> local parsers = setmetatable({}, { __mode = 'v' }) @@ -190,20 +191,7 @@ end --- ---@return boolean True if the position is in node range function M.is_in_node_range(node, line, col) - local start_line, start_col, end_line, end_col = M.get_node_range(node) - if line >= start_line and line <= end_line then - if line == start_line and line == end_line then - return col >= start_col and col < end_col - elseif line == start_line then - return col >= start_col - elseif line == end_line then - return col < end_col - else - return true - end - else - return false - end + return M.node_contains(node, { line, col, line, col }) end --- Determines if a node contains a range @@ -213,11 +201,11 @@ end --- ---@return boolean True if the {node} contains the {range} function M.node_contains(node, range) - local start_row, start_col, end_row, end_col = node:range() - local start_fits = start_row < range[1] or (start_row == range[1] and start_col <= range[2]) - local end_fits = end_row > range[3] or (end_row == range[3] and end_col >= range[4]) - - return start_fits and end_fits + vim.validate({ + node = { node, 'userdata' }, + range = { range, Range.validate, 'integer list with 4 or 6 elements' }, + }) + return Range.contains({ node:range() }, range) end --- Returns a list of highlight captures at the given position diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index 8decd3a1fd..bec24a23a5 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -54,6 +54,26 @@ M.cmp_pos = { setmetatable(M.cmp_pos, { __call = cmp_pos }) ---@private +---Check if a variable is a valid range object +---@param r any +---@return boolean +function M.validate(r) + if type(r) ~= 'table' or #r ~= 6 and #r ~= 4 then + return false + end + + for _, e in + ipairs(r --[[@as any[] ]]) + do + if type(e) ~= 'number' then + return false + end + end + + return true +end + +---@private ---@param r1 Range4|Range6 ---@param r2 Range4|Range6 ---@return boolean diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 1bc7971eba..b8b0dd867e 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -1,3 +1,37 @@ +--- @defgroup lua-treesitter-languagetree +--- +--- @brief A \*LanguageTree\* contains a tree of parsers: the root treesitter parser for {lang} and +--- any "injected" language parsers, which themselves may inject other languages, recursively. +--- For example a Lua buffer containing some Vimscript commands needs multiple parsers to fully +--- understand its contents. +--- +--- To create a LanguageTree (parser object) for a given buffer and language, use: +--- +--- <pre>lua +--- local parser = vim.treesitter.get_parser(bufnr, lang) +--- </pre> +--- +--- (where `bufnr=0` means current buffer). `lang` defaults to 'filetype'. +--- Note: currently the parser is retained for the lifetime of a buffer but this may change; +--- a plugin should keep a reference to the parser object if it wants incremental updates. +--- +--- Whenever you need to access the current syntax tree, parse the buffer: +--- +--- <pre>lua +--- local tree = parser:parse() +--- </pre> +--- +--- This returns a table of immutable |treesitter-tree| objects representing the current state of +--- the buffer. When the plugin wants to access the state after a (possible) edit it must call +--- `parse()` again. If the buffer wasn't edited, the same tree will be returned again without extra +--- work. If the buffer was parsed before, incremental parsing will be done of the changed parts. +--- +--- Note: To use the parser directly inside a |nvim_buf_attach()| Lua callback, you must call +--- |vim.treesitter.get_parser()| before you register your callback. But preferably parsing +--- shouldn't be done directly in the change callback anyway as they will be very frequent. Rather +--- a plugin that does any kind of analysis on a tree should use a timer to throttle too frequent +--- updates. + local a = vim.api local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') diff --git a/runtime/lua/vim/version.lua b/runtime/lua/vim/version.lua new file mode 100644 index 0000000000..35629c461f --- /dev/null +++ b/runtime/lua/vim/version.lua @@ -0,0 +1,277 @@ +local M = {} + +---@private +---@param version string +---@return string +local function create_err_msg(v) + if type(v) == 'string' then + return string.format('invalid version: "%s"', tostring(v)) + end + return string.format('invalid version: %s (%s)', tostring(v), type(v)) +end + +---@private +--- Throws an error if `version` cannot be parsed. +---@param version string +local function assert_version(version, opt) + local rv = M.parse(version, opt) + if rv == nil then + error(create_err_msg(version)) + end + return rv +end + +---@private +--- Compares the prerelease component of the two versions. +local function cmp_prerelease(v1, v2) + if v1.prerelease and not v2.prerelease then + return -1 + end + if not v1.prerelease and v2.prerelease then + return 1 + end + if not v1.prerelease and not v2.prerelease then + return 0 + end + + local v1_identifiers = vim.split(v1.prerelease, '.', { plain = true }) + local v2_identifiers = vim.split(v2.prerelease, '.', { plain = true }) + local i = 1 + local max = math.max(vim.tbl_count(v1_identifiers), vim.tbl_count(v2_identifiers)) + while i <= max do + local v1_identifier = v1_identifiers[i] + local v2_identifier = v2_identifiers[i] + if v1_identifier ~= v2_identifier then + local v1_num = tonumber(v1_identifier) + local v2_num = tonumber(v2_identifier) + local is_number = v1_num and v2_num + if is_number then + -- Number comparisons + if not v1_num and v2_num then + return -1 + end + if v1_num and not v2_num then + return 1 + end + if v1_num == v2_num then + return 0 + end + if v1_num > v2_num then + return 1 + end + if v1_num < v2_num then + return -1 + end + else + -- String comparisons + if v1_identifier and not v2_identifier then + return 1 + end + if not v1_identifier and v2_identifier then + return -1 + end + if v1_identifier < v2_identifier then + return -1 + end + if v1_identifier > v2_identifier then + return 1 + end + if v1_identifier == v2_identifier then + return 0 + end + end + end + i = i + 1 + end + + return 0 +end + +---@private +local function cmp_version_core(v1, v2) + if v1.major == v2.major and v1.minor == v2.minor and v1.patch == v2.patch then + return 0 + end + if + v1.major > v2.major + or (v1.major == v2.major and v1.minor > v2.minor) + or (v1.major == v2.major and v1.minor == v2.minor and v1.patch > v2.patch) + then + return 1 + end + return -1 +end + +--- Compares two strings (`v1` and `v2`) in semver format. +---@param v1 string Version. +---@param v2 string Version to compare with v1. +---@param opts table|nil Optional keyword arguments: +--- - strict (boolean): see `semver.parse` for details. Defaults to false. +---@return integer `-1` if `v1 < v2`, `0` if `v1 == v2`, `1` if `v1 > v2`. +function M.cmp(v1, v2, opts) + opts = opts or { strict = false } + local v1_parsed = assert_version(v1, opts) + local v2_parsed = assert_version(v2, opts) + + local result = cmp_version_core(v1_parsed, v2_parsed) + if result == 0 then + result = cmp_prerelease(v1_parsed, v2_parsed) + end + return result +end + +---@private +---@param labels string Prerelease and build component of semantic version string e.g. "-rc1+build.0". +---@return string|nil +local function parse_prerelease(labels) + -- This pattern matches "-(alpha)+build.15". + -- '^%-[%w%.]+$' + local result = labels:match('^%-([%w%.]+)+.+$') + if result then + return result + end + -- This pattern matches "-(alpha)". + result = labels:match('^%-([%w%.]+)') + if result then + return result + end + + return nil +end + +---@private +---@param labels string Prerelease and build component of semantic version string e.g. "-rc1+build.0". +---@return string|nil +local function parse_build(labels) + -- Pattern matches "-alpha+(build.15)". + local result = labels:match('^%-[%w%.]+%+([%w%.]+)$') + if result then + return result + end + + -- Pattern matches "+(build.15)". + result = labels:match('^%+([%w%.]+)$') + if result then + return result + end + + return nil +end + +---@private +--- Extracts the major, minor, patch and preprelease and build components from +--- `version`. +---@param version string Version string +local function extract_components_strict(version) + local major, minor, patch, prerelease_and_build = version:match('^v?(%d+)%.(%d+)%.(%d+)(.*)$') + return tonumber(major), tonumber(minor), tonumber(patch), prerelease_and_build +end + +---@private +--- Extracts the major, minor, patch and preprelease and build components from +--- `version`. When `minor` and `patch` components are not found (nil), coerce +--- them to 0. +---@param version string Version string +local function extract_components_loose(version) + local major, minor, patch, prerelease_and_build = version:match('^v?(%d+)%.?(%d*)%.?(%d*)(.*)$') + major = tonumber(major) + minor = tonumber(minor) or 0 + patch = tonumber(patch) or 0 + return major, minor, patch, prerelease_and_build +end + +---@private +--- Validates the prerelease and build string e.g. "-rc1+build.0". If the +--- prerelease, build or both are valid forms then it will return true, if it +--- is not of any valid form, it will return false. +---@param prerelease_and_build string +---@return boolean +local function is_prerelease_and_build_valid(prerelease_and_build) + if prerelease_and_build == '' then + return true + end + local has_build = parse_build(prerelease_and_build) ~= nil + local has_prerelease = parse_prerelease(prerelease_and_build) ~= nil + local has_prerelease_and_build = has_prerelease and has_build + return has_build or has_prerelease or has_prerelease_and_build +end + +--- Parses a semantic version string. +--- +--- Ignores leading "v" and surrounding whitespace, e.g. " v1.0.1-rc1+build.2", +--- "1.0.1-rc1+build.2", "v1.0.1-rc1+build.2" and "v1.0.1-rc1+build.2 " are all parsed as: +--- <pre> +--- { major = 1, minor = 0, patch = 1, prerelease = "rc1", build = "build.2" } +--- </pre> +--- +---@param version string Version string to be parsed. +---@param opts table|nil Optional keyword arguments: +--- - strict (boolean): Default false. If `true`, no coercion is attempted on +--- input not strictly conforming to semver v2.0.0 +--- (https://semver.org/spec/v2.0.0.html). E.g. `parse("v1.2")` returns nil. +---@return table|nil parsed_version Parsed version table or `nil` if `version` is invalid. +function M.parse(version, opts) + if type(version) ~= 'string' then + error(create_err_msg(version)) + end + + opts = opts or { strict = false } + + version = vim.trim(version) + + local extract_components = opts.strict and extract_components_strict or extract_components_loose + local major, minor, patch, prerelease_and_build = extract_components(version) + + -- If major is nil then that means that the version does not begin with a + -- digit with or without a "v" prefix. + if major == nil or not is_prerelease_and_build_valid(prerelease_and_build) then + return nil + end + + local prerelease = nil + local build = nil + if prerelease_and_build ~= nil then + prerelease = parse_prerelease(prerelease_and_build) + build = parse_build(prerelease_and_build) + end + + return { + major = major, + minor = minor, + patch = patch, + prerelease = prerelease, + build = build, + } +end + +---Returns `true` if `v1` are `v2` are equal versions. +---@param v1 string +---@param v2 string +---@return boolean +function M.eq(v1, v2) + return M.cmp(v1, v2) == 0 +end + +---Returns `true` if `v1` is less than `v2`. +---@param v1 string +---@param v2 string +---@return boolean +function M.lt(v1, v2) + return M.cmp(v1, v2) == -1 +end + +---Returns `true` if `v1` is greater than `v2`. +---@param v1 string +---@param v2 string +---@return boolean +function M.gt(v1, v2) + return M.cmp(v1, v2) == 1 +end + +setmetatable(M, { + __call = function() + return vim.fn.api_info().version + end, +}) + +return M |