diff options
30 files changed, 1234 insertions, 400 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fc8bc230fd..ae30f00f31 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,11 +9,16 @@ labels: bug <!-- Before reporting: search existing issues and check the FAQ. --> - `nvim --version`: -- `vim -u DEFAULTS` (version: ) behaves differently? - Operating system/version: - Terminal name/version: - `$TERM`: +<!-- +If this report is about different behaviour between Nvim and Vim, make sure to +read `:h vim-differences` first. Otherwise remove the next line. +--> +[ ] `vim -u DEFAULTS` (version: ) behaves differently + ### Steps to reproduce using `nvim -u NORC` ``` diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 6d007c0e44..be01966d42 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -841,10 +841,18 @@ vim.call({func}, {...}) *vim.call()* vim.fn[func]({...}) vim.cmd({cmd}) *vim.cmd()* - Invokes an Ex command (the ":" commands, Vimscript statements). + Executes multiple lines of Vimscript at once. It is an alias to + |nvim_exec()|, where `output` is set to false. Thus it works identical + to |:source|. See also |ex-cmd-index|. Example: > vim.cmd('echo 42') + vim.cmd([[ + augroup My_group + autocmd! + autocmd FileType c setlocal cindent + augroup END + ]]) vim.fn.{func}({...}) *vim.fn* Invokes |vim-function| or |user-function| {func} with arguments {...}. diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index 510585d0dd..1f4b5d3097 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -102,14 +102,14 @@ tsnode:field({name}) *tsnode:field()* tsnode:child_count() *tsnode:child_count()* Get the node's number of children. -tsnode:child({index}) *tsnode:child()* +tsnode:child({index}) *tsnode:child()* Get the node's child at the given {index}, where zero represents the first child. -tsnode:named_child_count() *tsnode:named_child_count()* +tsnode:named_child_count() *tsnode:named_child_count()* Get the node's number of named children. -tsnode:named_child({index}) *tsnode:named_child()* +tsnode:named_child({index}) *tsnode:named_child()* Get the node's named child at the given {index}, where zero represents the first named child. @@ -146,7 +146,7 @@ tsnode:has_error() *tsnode:has_error()* tsnode:sexpr() *tsnode:sexpr()* Get an S-expression representing the node as a string. -tsnode:id() *tsnode:id()* +tsnode:id() *tsnode:id()* Get an unique identier for the node inside its own tree. No guarantees are made about this identifer's internal representation, @@ -156,16 +156,16 @@ tsnode:id() *tsnode:id()* NB: the id is not guaranteed to be unique for nodes from different trees. tsnode:descendant_for_range({start_row}, {start_col}, {end_row}, {end_col}) - *tsnode:descendant_for_range()* + *tsnode:descendant_for_range()* Get the smallest node within this node that spans the given range of (row, column) positions tsnode:named_descendant_for_range({start_row}, {start_col}, {end_row}, {end_col}) - *tsnode:named_descendant_for_range()* + *tsnode:named_descendant_for_range()* Get the smallest named node within this node that spans the given range of (row, column) positions -Query methods *lua-treesitter-query* +Query *lua-treesitter-query* Tree-sitter queries are supported, with some limitations. Currently, the only supported match predicate is `eq?` (both comparing a capture against a string @@ -178,65 +178,6 @@ and predicates. A `capture` allows you to associate names with a specific node in a pattern. A `predicate` adds arbitrary metadata and conditional data to a match. -vim.treesitter.parse_query({lang}, {query}) - *vim.treesitter.parse_query()* - Parse {query} as a string. (If the query is in a file, the caller - should read the contents into a string before calling). - - Returns a `Query` (see |lua-treesitter-query|) object which can be used to - search nodes in the syntax tree for the patterns defined in {query} - using `iter_*` methods below. Exposes `info` and `captures` with - additional information about the {query}. - - `captures` contains the list of unique capture names defined in - {query}. - -` info.captures` also points to `captures`. - - `info.patterns` contains information about predicates. - - -query:iter_captures({node}, {bufnr}, {start_row}, {end_row}) - *query:iter_captures()* - Iterate over all captures from all matches inside {node}. - {bufnr} is needed if the query contains predicates, then the caller - must ensure to use a freshly parsed tree consistent with the current - text of the buffer. {start_row} and {end_row} can be used to limit - matches inside a row range (this is typically used with root node - as the node, i e to get syntax highlight matches in the current - viewport). When omitted the start and end row values are used from - the given node. - - The iterator returns three values, a numeric id identifying the capture, - the captured node, and metadata from any directives processing the match. - The following example shows how to get captures by name: -> - for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do - local name = query.captures[id] -- name of the capture in the query - -- typically useful info about the node: - local type = node:type() -- type of the captured node - local row1, col1, row2, col2 = node:range() -- range of the capture - ... use the info here ... - end -< -query:iter_matches({node}, {bufnr}, {start_row}, {end_row}) - *query:iter_matches()* - Iterate over all matches within a node. The arguments are the same as - for |query:iter_captures()| but the iterated values are different: - an (1-based) index of the pattern in the query, a table mapping - capture indices to nodes, and metadata from any directives processing the match. - If the query has more than one pattern the capture table might be sparse, - and e.g. `pairs()` method should be used over `ipairs`. - Here an example iterating over all captures in every match: -> - for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do - for id, node in pairs(match) do - local name = query.captures[id] - -- `node` was captured by the `name` capture in the match - - local node_data = metadata[id] -- Node level metadata - - ... use the info here ... - end - end - Treesitter Query Predicates *lua-treesitter-predicates* When writing queries for treesitter, one might use `predicates`, that is, @@ -298,28 +239,6 @@ Here is a list of built-in directives: `({capture_id}, {start_row}, {start_col}, {end_row}, {end_col}, {key?})` The default key is "offset". - *vim.treesitter.query.add_predicate()* -vim.treesitter.query.add_predicate({name}, {handler}) - -This adds a predicate with the name {name} to be used in queries. -{handler} should be a function whose signature will be : > - handler(match, pattern, bufnr, predicate) -< - *vim.treesitter.query.list_predicates()* -vim.treesitter.query.list_predicates() - -This lists the currently available predicates to use in queries. - - *vim.treesitter.query.add_directive()* -vim.treesitter.query.add_directive({name}, {handler}) - -This adds a directive with the name {name} to be used in queries. -{handler} should be a function whose signature will be : > - handler(match, pattern, bufnr, predicate, metadata) -Handlers can set match level data by setting directly on the metadata object `metadata.key = value` -Handlers can set node level data by using the capture id on the metadata table -`metadata[capture_id].key = value` - Treesitter syntax highlighting (WIP) *lua-treesitter-highlight* NOTE: This is a partially implemented feature, and not usable as a default @@ -364,92 +283,434 @@ identical identifiers, highlighting both as |hl-WarningMsg|: > ((binary_expression left: (identifier) @WarningMsg.left right: (identifier) @WarningMsg.right) (eq? @WarningMsg.left @WarningMsg.right)) +< -Treesitter language injection (WIP) *lua-treesitter-language-injection* +============================================================================== +Lua module: vim.treesitter *lua-treesitter-core* -NOTE: This is a partially implemented feature, and not usable as a default -solution yet. What is documented here is a temporary interface intended -for those who want to experiment with this feature and contribute to -its development. +get_parser({bufnr}, {lang}, {opts}) *get_parser()* + Gets the parser for this bufnr / ft combination. -Languages can have nested languages within them, for example javascript inside -HTML. We can "inject" a treesitter parser for a child language by configuring -injection queries. Here is an example of Javascript and CSS injected into -HTML. > + If needed this will create the parser. Unconditionnally attach + the provided callback - local query = [[ - (script_element (raw_text) @javascript) - (style_element (raw_text) @css) - ]]; + Parameters: ~ + {bufnr} The buffer the parser should be tied to + {lang} The filetype of this parser + {opts} Options object to pass to the created language + tree - local parser = vim.treesitter.get_parser(nil, nil, { - injections = {html = query} - }) + Return: ~ + The parser - parser:parse() +get_string_parser({str}, {lang}, {opts}) *get_string_parser()* + Gets a string parser -Any capture will be treated as the node treesitter will use for the injected -language. The capture name will be used as the language. There are a couple -reserved captures that do not have this behavior + Parameters: ~ + {str} The string to parse + {lang} The language of this string + {opts} Options to pass to the created language tree -`@language` -This will use a nodes text content as the language to be injected. -`@content` -This will use the captured nodes content as the injected content. +============================================================================== +Lua module: vim.treesitter.language *treesitter-language* -`@combined` -This will combine all matches of a pattern as one single block of content. -By default, each match of a pattern is treated as it's own block of content -and parsed independent of each other. +inspect_language({lang}) *inspect_language()* + Inspects the provided language. -`@<language>` -Any other capture name will be treated as both the language and the content. + Inspecting provides some useful informations on the language + like node names, ... -`@_<name>` -Any capture with a leading "_" will not be treated as a language and will have -no special processing and is useful for capturing nodes for directives. + Parameters: ~ + {lang} The language. -Injections can be configured using `directives` instead of using capture -names. Here is an example of a directive that resolves the language based on a -buffer variable instead of statically in the query. > +require_language({lang}, {path}, {silent}) *require_language()* + Asserts that the provided language is installed, and + optionally provide a path for the parser - local query = require("vim.treesitter.query") + Parsers are searched in the `parser` runtime directory. - query.add_directive("inject-preprocessor!", function(_, bufnr, _, _, data) - local success, lang = pcall(vim.api.nvim_buf_get_var, bufnr, "css_preprocessor") + Parameters: ~ + {lang} The language the parser should parse + {path} Optional path the parser is located at + {silent} Don't throw an error if language not found - data.language = success and lang or "css" - end) -Here is the same HTML query using this directive. > +============================================================================== +Lua module: vim.treesitter.query *treesitter-query* - local query = [[ - (script_element (raw_text) @javascript) - (style_element - ((raw_text) @content - (#inject-preprocessor!))) - ]]; +add_directive({name}, {handler}, {force}) *add_directive()* + Adds a new directive to be used in queries + + Parameters: ~ + {name} the name of the directive, without leading # + {handler} the handler function to be used signature will + be (match, pattern, bufnr, predicate) + +add_predicate({name}, {handler}, {force}) *add_predicate()* + Adds a new predicate to be used in queries + + Parameters: ~ + {name} the name of the predicate, without leading # + {handler} the handler function to be used signature will + be (match, pattern, bufnr, predicate) + +get_node_text({node}, {source}) *get_node_text()* + Gets the text corresponding to a given node + + Parameters: ~ + {node} the node + {bsource} The buffer or string from which the node is + extracted + +get_query({lang}, {query_name}) *get_query()* + Returns the runtime query {query_name} for {lang}. + + Parameters: ~ + {lang} The language to use for the query + {query_name} The name of the query (i.e. "highlights") + + Return: ~ + The corresponding query, parsed. + + *get_query_files()* +get_query_files({lang}, {query_name}, {is_included}) + Gets the list of files used to make up a query + + Parameters: ~ + {lang} The language + {query_name} The name of the query to load + {is_included} Internal parameter, most of the time left + as `nil` + +list_predicates() *list_predicates()* + TODO: Documentation + +parse_query({lang}, {query}) *parse_query()* + Parse {query} as a string. (If the query is in a file, the + caller should read the contents into a string before calling). + + Returns a `Query` (see |lua-treesitter-query|) object which + can be used to search nodes in the syntax tree for the + patterns defined in {query} using `iter_*` methods below. + + Exposes `info` and `captures` with additional information about the {query}. + • `captures` contains the list of unique capture names defined + in {query}. - `info.captures` also points to `captures` . + • `info.patterns` contains information about predicates. + + Parameters: ~ + {lang} The language + {query} A string containing the query (s-expr syntax) + + Return: ~ + The query + + *Query:iter_captures()* +Query:iter_captures({self}, {node}, {source}, {start}, {stop}) + Iterate over all captures from all matches inside {node} + + {source} is needed if the query contains predicates, then the + caller must ensure to use a freshly parsed tree consistent + with the current text of the buffer (if relevent). {start_row} + and {end_row} can be used to limit matches inside a row range + (this is typically used with root node as the node, i e to get + syntax highlight matches in the current viewport). When + omitted the start and end row values are used from the given + node. + + The iterator returns three values, a numeric id identifying + the capture, the captured node, and metadata from any + directives processing the match. The following example shows + how to get captures by name: +> + + for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do + local name = query.captures[id] -- name of the capture in the query + -- typically useful info about the node: + local type = node:type() -- type of the captured node + local row1, col1, row2, col2 = node:range() -- range of the capture + ... use the info here ... + end +< + + Parameters: ~ + {node} The node under which the search will occur + {source} The source buffer or string to exctract text + from + {start} The starting line of the search + {stop} The stopping line of the search (end-exclusive) + {self} + + Return: ~ + The matching capture id + The captured node + + *Query:iter_matches()* +Query:iter_matches({self}, {node}, {source}, {start}, {stop}) + Iterates the matches of self on a given range. + + Iterate over all matches within a node. The arguments are the + same as for |query:iter_captures()| but the iterated values + are different: an (1-based) index of the pattern in the query, + a table mapping capture indices to nodes, and metadata from + any directives processing the match. If the query has more + than one pattern the capture table might be sparse, and e.g. + `pairs()` method should be used over `ipairs` . Here an + example iterating over all captures in every match: +> + + for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do + for id, node in pairs(match) do + local name = query.captures[id] + -- `node` was captured by the `name` capture in the match + + local node_data = metadata[id] -- Node level metadata + + ... use the info here ... + end + end +< + + Parameters: ~ + {node} The node under which the search will occur + {source} The source buffer or string to search + {start} The starting line of the search + {stop} The stopping line of the search (end-exclusive) + {self} + + Return: ~ + The matching pattern id + The matching match + +set_query({lang}, {query_name}, {text}) *set_query()* + Sets the runtime query {query_name} for {lang} + + This allows users to override any runtime files and/or + configuration set by plugins. + + Parameters: ~ + {lang} string: The language to use for the query + {query_name} string: The name of the query (i.e. + "highlights") + {text} string: The query text (unparsed). + + +============================================================================== +Lua module: vim.treesitter.highlighter *treesitter-highlighter* + +new({tree}, {opts}) *highlighter.new()* + Creates a new highlighter using + + Parameters: ~ + {tree} The language tree to use for highlighting + {opts} Table used to configure the highlighter + • queries: Table to overwrite queries used by the + highlighter + +TSHighlighter:destroy({self}) *TSHighlighter:destroy()* + Removes all internal references to the highlighter + + Parameters: ~ + {self} + +TSHighlighter:get_query({self}, {lang}) *TSHighlighter:get_query()* + Gets the query used for + + Parameters: ~ + {lang} A language used by the highlighter. + {self} + + +============================================================================== +Lua module: vim.treesitter.languagetree *treesitter-languagetree* + +LanguageTree:add_child({self}, {lang}) *LanguageTree:add_child()* + Adds a child language to this tree. + + If the language already exists as a child, it will first be + removed. + + Parameters: ~ + {lang} The language to add. + {self} + +LanguageTree:children({self}) *LanguageTree:children()* + Returns a map of language to child tree. + + Parameters: ~ + {self} + +LanguageTree:contains({self}, {range}) *LanguageTree:contains()* + Determines wether This goes down the tree to recursively check childs. + + Parameters: ~ + {range} is contained in this language tree + + Parameters: ~ + {range} A range, that is a `{ start_line, start_col, + end_line, end_col }` table. + {self} + +LanguageTree:destroy({self}) *LanguageTree:destroy()* + Destroys this language tree and all its children. + + Any cleanup logic should be performed here. Note, this DOES + NOT remove this tree from a parent. `remove_child` must be called on the parent to remove it. + + Parameters: ~ + {self} + + *LanguageTree:for_each_child()* +LanguageTree:for_each_child({self}, {fn}, {include_self}) + Invokes the callback for each LanguageTree and it's children + recursively + + Parameters: ~ + {fn} The function to invoke. This is invoked + with arguments (tree: LanguageTree, lang: + string) + {include_self} Whether to include the invoking tree in + the results. + {self} + +LanguageTree:for_each_tree({self}, {fn}) *LanguageTree:for_each_tree()* + Invokes the callback for each treesitter trees recursively. + + Note, this includes the invoking language tree's trees as + well. + + Parameters: ~ + {fn} The callback to invoke. The callback is invoked + with arguments (tree: TSTree, languageTree: + LanguageTree) + {self} + +LanguageTree:included_regions({self}) *LanguageTree:included_regions()* + Gets the set of included regions + + Parameters: ~ + {self} + +LanguageTree:invalidate({self}, {reload}) *LanguageTree:invalidate()* + Invalidates this parser and all its children + + Parameters: ~ + {self} + +LanguageTree:is_valid({self}) *LanguageTree:is_valid()* + Determines whether this tree is valid. If the tree is invalid, `parse()` must be called to get the an updated tree. + + Parameters: ~ + {self} + +LanguageTree:lang({self}) *LanguageTree:lang()* + Gets the language of this tree node. + + Parameters: ~ + {self} + + *LanguageTree:language_for_range()* +LanguageTree:language_for_range({self}, {range}) + Gets the appropriate language that contains + + Parameters: ~ + {range} A text range, see |LanguageTree:contains| + {self} + +LanguageTree:parse({self}) *LanguageTree:parse()* + Parses all defined regions using a treesitter parser for the + language this tree represents. This will run the injection + query for this language to determine if any child languages + should be created. + + Parameters: ~ + {self} + +LanguageTree:register_cbs({self}, {cbs}) *LanguageTree:register_cbs()* + Registers callbacks for the parser + + Parameters: ~ + {cbs} An `nvim_buf_attach` -like table argument with the following keys : `on_bytes` : see `nvim_buf_attach` , but this will be called after the parsers callback. `on_changedtree` : a callback that will be called every time the + tree has syntactical changes. it will only be + passed one argument, that is a table of the ranges + (as node ranges) that changed. `on_child_added` : emitted when a child is added to the tree. `on_child_removed` : emitted when a child is removed from the tree. + {self} + +LanguageTree:remove_child({self}, {lang}) *LanguageTree:remove_child()* + Removes a child language from this tree. + + Parameters: ~ + {lang} The language to remove. + {self} + + *LanguageTree:set_included_regions()* +LanguageTree:set_included_regions({self}, {regions}) + Sets the included regions that should be parsed by this + parser. A region is a set of nodes and/or ranges that will be + parsed in the same context. + + For example, `{ { node1 }, { node2} }` is two separate + regions. This will be parsed by the parser in two different + contexts... thus resulting in two separate trees. + + `{ { node1, node2 } }` is a single region consisting of two + nodes. This will be parsed by the parser in a single + context... thus resulting in a single tree. + + This allows for embedded languages to be parsed together + across different nodes, which is useful for templating + languages like ERB and EJS. + + Note, this call invalidates the tree and requires it to be + parsed again. + + Parameters: ~ + {regions} A list of regions this tree should manage and + parse. + {self} + +LanguageTree:source({self}) *LanguageTree:source()* + Returns the source content of the language tree (bufnr or + string). + + Parameters: ~ + {self} + +LanguageTree:trees({self}) *LanguageTree:trees()* + Returns all trees this language tree contains. Does not + include child languages. + + Parameters: ~ + {self} + +new({source}, {lang}, {opts}) *languagetree.new()* + Represents a single treesitter parser for a language. The + language can contain child languages with in its range, hence + the tree. - local parser = vim.treesitter.get_parser(nil, nil, { - injections = {html = query} - }) + Parameters: ~ + {source} Can be a bufnr or a string of text to + parse + {lang} The language this tree represents + {opts} Options table + {opts.injections} A table of language to injection query + strings. This is useful for overriding + the built-in runtime file searching for + the injection language query per + language. - parser:parse() -The following properties can be attached to the metadata object provided to -the directive. +============================================================================== +Lua module: vim.treesitter.health *treesitter-health* -`language` -Same as the language capture. +check_health() *check_health()* + TODO: Documentation -`content` -A list of ranges or nodes to inject as content. These ranges and/or nodes will -be treated as combined source and will be parsed within the same context. This -differs from the `@content` capture which only captures a single node as -content. This can also be a single number that references a captured node. +list_parsers() *list_parsers()* + Lists the parsers currently installed -`combined` -Same as the combined capture. + Return: ~ + A list of parsers vim:tw=78:ts=8:ft=help:norl: diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 26700288af..93ec9ed624 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -483,6 +483,13 @@ end --- result. You can use this with `client.cancel_request(request_id)` --- to cancel the request. --- +--- - request_sync(method, params, timeout_ms, bufnr) +--- Sends a request to the server and synchronously waits for the response. +--- This is a wrapper around {client.request} +--- Returns: { err=err, result=result }, a dictionary, where `err` and `result` come from +--- the |lsp-handler|. On timeout, cancel or error, returns `(nil, err)` where `err` is a +--- string describing the failure reason. If the request was unsuccessful returns `nil`. +--- --- - notify(method, params) --- Sends a notification to an LSP server. --- Returns: a boolean to indicate if the notification was successful. If @@ -891,6 +898,42 @@ function lsp.start_client(config) end --@private + --- Sends a request to the server and synchronously waits for the response. + --- + --- This is a wrapper around {client.request} + --- + --@param method (string) LSP method name. + --@param params (table) LSP request params. + --@param timeout_ms (number, optional, default=1000) Maximum time in + ---milliseconds to wait for a result. + --@param bufnr (number) Buffer handle (0 for current). + --@returns { err=err, result=result }, a dictionary, where `err` and `result` come from the |lsp-handler|. + ---On timeout, cancel or error, returns `(nil, err)` where `err` is a + ---string describing the failure reason. If the request was unsuccessful + ---returns `nil`. + --@see |vim.lsp.buf_request_sync()| + function client.request_sync(method, params, timeout_ms, bufnr) + local request_result = nil + local function _sync_handler(err, _, result) + request_result = { err = err, result = result } + end + + local success, request_id = client.request(method, params, _sync_handler, + bufnr) + if not success then return nil end + + local wait_result, reason = vim.wait(timeout_ms or 1000, function() + return request_result ~= nil + end, 10) + + if not wait_result then + client.cancel_request(request_id) + return nil, wait_result_reason[reason] + end + return request_result + end + + --@private --- Sends a notification to an LSP server. --- --@param method (string) LSP method name. @@ -1289,12 +1332,12 @@ end --- --- Calls |vim.lsp.buf_request_all()| but blocks Nvim while awaiting the result. --- Parameters are the same as |vim.lsp.buf_request()| but the return result is ---- different. Wait maximum of {timeout_ms} (default 100) ms. +--- different. Wait maximum of {timeout_ms} (default 1000) ms. --- --@param bufnr (number) Buffer handle, or 0 for current. --@param method (string) LSP method name --@param params (optional, table) Parameters to send to the server ---@param timeout_ms (optional, number, default=100) Maximum time in +--@param timeout_ms (optional, number, default=1000) Maximum time in --- milliseconds to wait for a result. --- --@returns Map of client_id:request_result. On timeout, cancel or error, @@ -1307,7 +1350,7 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms) request_results = it end) - local wait_result, reason = vim.wait(timeout_ms or 100, function() + local wait_result, reason = vim.wait(timeout_ms or 1000, function() return request_results ~= nil end, 10) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 31116985e2..341a3e82fc 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -111,6 +111,39 @@ function M.completion(context) return request('textDocument/completion', params) end +--@private +--- If there is more than one client that supports the given method, +--- asks the user to select one. +-- +--@returns The client that the user selected or nil +local function select_client(method) + local clients = vim.tbl_values(vim.lsp.buf_get_clients()); + clients = vim.tbl_filter(function (client) + return client.supports_method(method) + end, clients) + -- better UX when choices are always in the same order (between restarts) + table.sort(clients, function (a, b) return a.name < b.name end) + + if #clients > 1 then + local choices = {} + for k,v in ipairs(clients) do + table.insert(choices, string.format("%d %s", k, v.name)) + end + local user_choice = vim.fn.confirm( + "Select a language server:", + table.concat(choices, "\n"), + 0, + "Question" + ) + if user_choice == 0 then return nil end + return clients[user_choice] + elseif #clients < 1 then + return nil + else + return clients[1] + end +end + --- Formats the current buffer. --- --@param options (optional, table) Can be used to specify FormattingOptions. @@ -119,8 +152,11 @@ end -- --@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting function M.formatting(options) + local client = select_client("textDocument/formatting") + if client == nil then return end + local params = util.make_formatting_params(options) - return request('textDocument/formatting', params) + return client.request("textDocument/formatting", params) end --- Performs |vim.lsp.buf.formatting()| synchronously. @@ -134,14 +170,62 @@ end --- --@param options Table with valid `FormattingOptions` entries --@param timeout_ms (number) Request timeout +--@see |vim.lsp.buf.formatting_seq_sync| function M.formatting_sync(options, timeout_ms) + local client = select_client("textDocument/formatting") + if client == nil then return end + local params = util.make_formatting_params(options) - local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout_ms) - if not result or vim.tbl_isempty(result) then return end - local _, formatting_result = next(result) - result = formatting_result.result - if not result then return end - vim.lsp.util.apply_text_edits(result) + local result, err = client.request_sync("textDocument/formatting", params, timeout_ms) + if result and result.result then + util.apply_text_edits(result.result) + elseif err then + vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN) + end +end + +--- Formats the current buffer by sequentially requesting formatting from attached clients. +--- +--- Useful when multiple clients with formatting capability are attached. +--- +--- Since it's synchronous, can be used for running on save, to make sure buffer is formatted +--- prior to being saved. {timeout_ms} is passed on to the |vim.lsp.client| `request_sync` method. +--- Example: +--- <pre> +--- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]] +--- </pre> +--- +--@param options (optional, table) `FormattingOptions` entries +--@param timeout_ms (optional, number) Request timeout +--@param order (optional, table) List of client names. Formatting is requested from clients +---in the following order: first all clients that are not in the `order` list, then +---the remaining clients in the order as they occur in the `order` list. +function M.formatting_seq_sync(options, timeout_ms, order) + local clients = vim.tbl_values(vim.lsp.buf_get_clients()); + + -- sort the clients according to `order` + for _, client_name in ipairs(order or {}) do + -- if the client exists, move to the end of the list + for i, client in ipairs(clients) do + if client.name == client_name then + table.insert(clients, table.remove(clients, i)) + break + end + end + end + + -- loop through the clients and make synchronous formatting requests + for _, client in ipairs(clients) do + if client.resolved_capabilities.document_formatting then + local params = util.make_formatting_params(options) + local result, err = client.request_sync("textDocument/formatting", params, timeout_ms) + if result and result.result then + util.apply_text_edits(result.result) + elseif err then + vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN) + end + end + end end --- Formats a given range. @@ -152,15 +236,12 @@ end --@param end_pos ({number, number}, optional) mark-indexed position. ---Defaults to the end of the last visual selection. function M.range_formatting(options, start_pos, end_pos) - validate { options = {options, 't', true} } - local sts = vim.bo.softtabstop; - options = vim.tbl_extend('keep', options or {}, { - tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop; - insertSpaces = vim.bo.expandtab; - }) + local client = select_client("textDocument/rangeFormatting") + if client == nil then return end + local params = util.make_given_range_params(start_pos, end_pos) - params.options = options - return request('textDocument/rangeFormatting', params) + params.options = util.make_formatting_params(options).options + return client.request("textDocument/rangeFormatting", params) end --- Renames all references to the symbol under the cursor. diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index 331e980e67..471a311c16 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -10,7 +10,7 @@ local log = {} -- Can be used to lookup the number from the name or the name from the number. -- Levels by name: 'trace', 'debug', 'info', 'warn', 'error' -- Level numbers begin with 'trace' at 0 -log.levels = vim.log.levels +log.levels = vim.deepcopy(vim.log.levels) -- Default log level is warn. local current_log_level = log.levels.WARN diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 1aa8326514..0cabd1a0d4 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -518,7 +518,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) send_response(decoded.id, err, result) end) -- This works because we are expecting vim.NIL here - elseif decoded.id and (decoded.result or decoded.error) then + elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then -- Server Result decoded.error = convert_NIL(decoded.error) decoded.result = convert_NIL(decoded.result) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 92ec447b55..ce8468aa8a 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -944,6 +944,9 @@ function M.jump_to_location(location) local from = {from_bufnr, vim.fn.line('.'), vim.fn.col('.'), 0} local item = {tagname=vim.fn.expand('<cword>'), from=from} + -- Save position in jumplist + vim.cmd("mark '") + --- Jump to new location (adjusting for UTF-16 encoding of characters) local bufnr = vim.uri_to_bufnr(uri) api.nvim_set_current_buf(bufnr) diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index f223c7b8c8..de997b2d86 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -25,12 +25,12 @@ setmetatable(M, { }) --- Creates a new parser. --- --- It is not recommended to use this, use vim.treesitter.get_parser() instead. --- --- @param bufnr The buffer the parser will be tied to --- @param lang The language of the parser --- @param opts Options to pass to the language tree +--- +--- It is not recommended to use this, use vim.treesitter.get_parser() instead. +--- +--- @param bufnr The buffer the parser will be tied to +--- @param lang The language of the parser +--- @param opts Options to pass to the created language tree function M._create_parser(bufnr, lang, opts) language.require_language(lang) if bufnr == 0 then @@ -41,10 +41,12 @@ function M._create_parser(bufnr, lang, opts) local self = LanguageTree.new(bufnr, lang, opts) + ---@private local function bytes_cb(_, ...) self:_on_bytes(...) end + ---@private local function detach_cb(_, ...) if parsers[bufnr] == self then parsers[bufnr] = nil @@ -52,6 +54,7 @@ function M._create_parser(bufnr, lang, opts) self:_on_detach(...) end + ---@private local function reload_cb(_, ...) self:_on_reload(...) end @@ -64,15 +67,15 @@ function M._create_parser(bufnr, lang, opts) end --- Gets the parser for this bufnr / ft combination. --- --- If needed this will create the parser. --- Unconditionnally attach the provided callback --- --- @param bufnr The buffer the parser should be tied to --- @param ft The filetype of this parser --- @param opts Options object to pass to the parser --- --- @returns The parser +--- +--- If needed this will create the parser. +--- Unconditionnally attach the provided callback +--- +--- @param bufnr The buffer the parser should be tied to +--- @param lang The filetype of this parser +--- @param opts Options object to pass to the created language tree +--- +--- @returns The parser function M.get_parser(bufnr, lang, opts) opts = opts or {} @@ -92,6 +95,11 @@ function M.get_parser(bufnr, lang, opts) return parsers[bufnr] end +--- Gets a string parser +--- +--- @param str The string to parse +--- @param lang The language of this string +--- @param opts Options to pass to the created language tree function M.get_string_parser(str, lang, opts) vim.validate { str = { str, 'string' }, diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua index dd0b11a6c7..e031ba1bd6 100644 --- a/runtime/lua/vim/treesitter/health.lua +++ b/runtime/lua/vim/treesitter/health.lua @@ -1,10 +1,14 @@ local M = {} local ts = vim.treesitter +--- Lists the parsers currently installed +--- +---@return A list of parsers function M.list_parsers() return vim.api.nvim_get_runtime_file('parser/*', true) end +--- Performs a healthcheck for treesitter integration function M.check_health() local report_info = vim.fn['health#report_info'] local report_ok = vim.fn['health#report_ok'] diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index fe7e1052c9..84b6a5f135 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -70,11 +70,13 @@ TSHighlighter.hl_map = { ["include"] = "Include", } +---@private local function is_highlight_name(capture_name) local firstc = string.sub(capture_name, 1, 1) return firstc ~= string.lower(firstc) end +---@private function TSHighlighterQuery.new(lang, query_string) local self = setmetatable({}, { __index = TSHighlighterQuery }) @@ -99,10 +101,12 @@ function TSHighlighterQuery.new(lang, query_string) return self end +---@private function TSHighlighterQuery:query() return self._query end +---@private --- Get the hl from capture. --- Returns a tuple { highlight_name: string, is_builtin: bool } function TSHighlighterQuery:_get_hl_from_capture(capture) @@ -116,6 +120,11 @@ function TSHighlighterQuery:_get_hl_from_capture(capture) end end +--- Creates a new highlighter using @param tree +--- +--- @param tree The language tree to use for highlighting +--- @param opts Table used to configure the highlighter +--- - queries: Table to overwrite queries used by the highlighter function TSHighlighter.new(tree, opts) local self = setmetatable({}, TSHighlighter) @@ -165,12 +174,14 @@ function TSHighlighter.new(tree, opts) return self end +--- Removes all internal references to the highlighter function TSHighlighter:destroy() if TSHighlighter.active[self.bufnr] then TSHighlighter.active[self.bufnr] = nil end end +---@private function TSHighlighter:get_highlight_state(tstree) if not self._highlight_states[tstree] then self._highlight_states[tstree] = { @@ -182,24 +193,31 @@ function TSHighlighter:get_highlight_state(tstree) return self._highlight_states[tstree] end +---@private function TSHighlighter:reset_highlight_state() self._highlight_states = {} end +---@private function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end) a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1) end +---@private function TSHighlighter:on_detach() self:destroy() end +---@private function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes or {}) do a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3]+1) end end +--- Gets the query used for @param lang +--- +--- @param lang A language used by the highlighter. function TSHighlighter:get_query(lang) if not self._queries[lang] then self._queries[lang] = TSHighlighterQuery.new(lang) @@ -208,6 +226,7 @@ function TSHighlighter:get_query(lang) return self._queries[lang] end +---@private local function on_line_impl(self, buf, line) self.tree:for_each_tree(function(tstree, tree) if not tstree then return end @@ -251,6 +270,7 @@ local function on_line_impl(self, buf, line) end, true) end +---@private function TSHighlighter._on_line(_, _win, buf, line, _) local self = TSHighlighter.active[buf] if not self then return end @@ -258,6 +278,7 @@ function TSHighlighter._on_line(_, _win, buf, line, _) on_line_impl(self, buf, line) end +---@private function TSHighlighter._on_buf(_, buf) local self = TSHighlighter.active[buf] if self then @@ -265,6 +286,7 @@ function TSHighlighter._on_buf(_, buf) end end +---@private function TSHighlighter._on_win(_, _win, buf, _topline) local self = TSHighlighter.active[buf] if not self then diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index eed28e0e41..6dc37c7848 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -3,12 +3,12 @@ local a = vim.api local M = {} --- Asserts that the provided language is installed, and optionally provide a path for the parser --- --- Parsers are searched in the `parser` runtime directory. --- --- @param lang The language the parser should parse --- @param path Optional path the parser is located at --- @param silent Don't throw an error if language not found +--- +--- Parsers are searched in the `parser` runtime directory. +--- +--- @param lang The language the parser should parse +--- @param path Optional path the parser is located at +--- @param silent Don't throw an error if language not found function M.require_language(lang, path, silent) if vim._ts_has_language(lang) then return true @@ -37,10 +37,10 @@ function M.require_language(lang, path, silent) end --- Inspects the provided language. --- --- Inspecting provides some useful informations on the language like node names, ... --- --- @param lang The language. +--- +--- Inspecting provides some useful informations on the language like node names, ... +--- +--- @param lang The language. function M.inspect_language(lang) M.require_language(lang) return vim._ts_inspect_language(lang) diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 2f5aeb0710..899d90e464 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -5,16 +5,16 @@ local language = require'vim.treesitter.language' local LanguageTree = {} LanguageTree.__index = LanguageTree --- Represents a single treesitter parser for a language. --- The language can contain child languages with in its range, --- hence the tree. --- --- @param source Can be a bufnr or a string of text to parse --- @param lang The language this tree represents --- @param opts Options table --- @param opts.injections A table of language to injection query strings. --- This is useful for overriding the built-in runtime file --- searching for the injection language query per language. +--- Represents a single treesitter parser for a language. +--- The language can contain child languages with in its range, +--- hence the tree. +--- +--- @param source Can be a bufnr or a string of text to parse +--- @param lang The language this tree represents +--- @param opts Options table +--- @param opts.injections A table of language to injection query strings. +--- This is useful for overriding the built-in runtime file +--- searching for the injection language query per language. function LanguageTree.new(source, lang, opts) language.require_language(lang) opts = opts or {} @@ -50,7 +50,7 @@ function LanguageTree.new(source, lang, opts) return self end --- Invalidates this parser and all its children +--- Invalidates this parser and all its children function LanguageTree:invalidate(reload) self._valid = false @@ -64,38 +64,38 @@ function LanguageTree:invalidate(reload) end end --- Returns all trees this language tree contains. --- Does not include child languages. +--- Returns all trees this language tree contains. +--- Does not include child languages. function LanguageTree:trees() return self._trees end --- Gets the language of this tree layer. +--- Gets the language of this tree node. function LanguageTree:lang() return self._lang end --- Determines whether this tree is valid. --- If the tree is invalid, `parse()` must be called --- to get the an updated tree. +--- Determines whether this tree is valid. +--- If the tree is invalid, `parse()` must be called +--- to get the an updated tree. function LanguageTree:is_valid() return self._valid end --- Returns a map of language to child tree. +--- Returns a map of language to child tree. function LanguageTree:children() return self._children end --- Returns the source content of the language tree (bufnr or string). +--- Returns the source content of the language tree (bufnr or string). function LanguageTree:source() return self._source end --- Parses all defined regions using a treesitter parser --- for the language this tree represents. --- This will run the injection query for this language to --- determine if any child languages should be created. +--- Parses all defined regions using a treesitter parser +--- for the language this tree represents. +--- This will run the injection query for this language to +--- determine if any child languages should be created. function LanguageTree:parse() if self._valid then return self._trees @@ -169,9 +169,10 @@ function LanguageTree:parse() return self._trees, changes end --- Invokes the callback for each LanguageTree and it's children recursively --- @param fn The function to invoke. This is invoked with arguments (tree: LanguageTree, lang: string) --- @param include_self Whether to include the invoking tree in the results. +--- Invokes the callback for each LanguageTree and it's children recursively +--- +--- @param fn The function to invoke. This is invoked with arguments (tree: LanguageTree, lang: string) +--- @param include_self Whether to include the invoking tree in the results. function LanguageTree:for_each_child(fn, include_self) if include_self then fn(self, self._lang) @@ -182,10 +183,12 @@ function LanguageTree:for_each_child(fn, include_self) end end --- Invokes the callback for each treesitter trees recursively. --- Note, this includes the invoking language tree's trees as well. --- @param fn The callback to invoke. The callback is invoked with arguments --- (tree: TSTree, languageTree: LanguageTree) +--- Invokes the callback for each treesitter trees recursively. +--- +--- Note, this includes the invoking language tree's trees as well. +--- +--- @param fn The callback to invoke. The callback is invoked with arguments +--- (tree: TSTree, languageTree: LanguageTree) function LanguageTree:for_each_tree(fn) for _, tree in ipairs(self._trees) do fn(tree, self) @@ -196,9 +199,11 @@ function LanguageTree:for_each_tree(fn) end end --- Adds a child language to this tree. --- If the language already exists as a child, it will first be removed. --- @param lang The language to add. +--- Adds a child language to this tree. +--- +--- If the language already exists as a child, it will first be removed. +--- +--- @param lang The language to add. function LanguageTree:add_child(lang) if self._children[lang] then self:remove_child(lang) @@ -212,8 +217,9 @@ function LanguageTree:add_child(lang) return self._children[lang] end --- Removes a child language from this tree. --- @param lang The language to remove. +--- Removes a child language from this tree. +--- +--- @param lang The language to remove. function LanguageTree:remove_child(lang) local child = self._children[lang] @@ -225,10 +231,11 @@ function LanguageTree:remove_child(lang) end end --- Destroys this language tree and all its children. --- Any cleanup logic should be performed here. --- Note, this DOES NOT remove this tree from a parent. --- `remove_child` must be called on the parent to remove it. +--- Destroys this language tree and all its children. +--- +--- Any cleanup logic should be performed here. +--- Note, this DOES NOT remove this tree from a parent. +--- `remove_child` must be called on the parent to remove it. function LanguageTree:destroy() -- Cleanup here for _, child in ipairs(self._children) do @@ -236,23 +243,23 @@ function LanguageTree:destroy() end end --- Sets the included regions that should be parsed by this parser. --- A region is a set of nodes and/or ranges that will be parsed in the same context. --- --- For example, `{ { node1 }, { node2} }` is two separate regions. --- This will be parsed by the parser in two different contexts... thus resulting --- in two separate trees. --- --- `{ { node1, node2 } }` is a single region consisting of two nodes. --- This will be parsed by the parser in a single context... thus resulting --- in a single tree. --- --- This allows for embedded languages to be parsed together across different --- nodes, which is useful for templating languages like ERB and EJS. --- --- Note, this call invalidates the tree and requires it to be parsed again. --- --- @param regions A list of regions this tree should manage and parse. +--- Sets the included regions that should be parsed by this parser. +--- A region is a set of nodes and/or ranges that will be parsed in the same context. +--- +--- For example, `{ { node1 }, { node2} }` is two separate regions. +--- This will be parsed by the parser in two different contexts... thus resulting +--- in two separate trees. +--- +--- `{ { node1, node2 } }` is a single region consisting of two nodes. +--- This will be parsed by the parser in a single context... thus resulting +--- in a single tree. +--- +--- This allows for embedded languages to be parsed together across different +--- nodes, which is useful for templating languages like ERB and EJS. +--- +--- Note, this call invalidates the tree and requires it to be parsed again. +--- +--- @param regions A list of regions this tree should manage and parse. function LanguageTree:set_included_regions(regions) -- TODO(vigoux): I don't think string parsers are useful for now if type(self._source) == "number" then @@ -281,16 +288,18 @@ function LanguageTree:set_included_regions(regions) self:invalidate() end --- Gets the set of included regions +--- Gets the set of included regions function LanguageTree:included_regions() return self._regions end --- Gets language injection points by language. --- This is where most of the injection processing occurs. --- TODO: Allow for an offset predicate to tailor the injection range --- instead of using the entire nodes range. --- @private +--- Gets language injection points by language. +--- +--- This is where most of the injection processing occurs. +--- +--- TODO: Allow for an offset predicate to tailor the injection range +--- instead of using the entire nodes range. +--- @private function LanguageTree:_get_injections() if not self._injection_query then return {} end @@ -395,12 +404,14 @@ function LanguageTree:_get_injections() return result end +---@private function LanguageTree:_do_callback(cb_name, ...) for _, cb in ipairs(self._callbacks[cb_name]) do cb(...) end end +---@private function LanguageTree:_on_bytes(bufnr, changed_tick, start_row, start_col, start_byte, old_row, old_col, old_byte, @@ -425,24 +436,26 @@ function LanguageTree:_on_bytes(bufnr, changed_tick, new_row, new_col, new_byte) end +---@private function LanguageTree:_on_reload() self:invalidate(true) end +---@private function LanguageTree:_on_detach(...) self:invalidate(true) self:_do_callback('detach', ...) end --- Registers callbacks for the parser --- @param cbs An `nvim_buf_attach`-like table argument with the following keys : --- `on_bytes` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback. --- `on_changedtree` : a callback that will be called every time the tree has syntactical changes. --- it will only be passed one argument, that is a table of the ranges (as node ranges) that --- changed. --- `on_child_added` : emitted when a child is added to the tree. --- `on_child_removed` : emitted when a child is removed from the tree. +--- @param cbs An `nvim_buf_attach`-like table argument with the following keys : +--- `on_bytes` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback. +--- `on_changedtree` : a callback that will be called every time the tree has syntactical changes. +--- it will only be passed one argument, that is a table of the ranges (as node ranges) that +--- changed. +--- `on_child_added` : emitted when a child is added to the tree. +--- `on_child_removed` : emitted when a child is removed from the tree. function LanguageTree:register_cbs(cbs) if not cbs then return end @@ -467,6 +480,7 @@ function LanguageTree:register_cbs(cbs) end end +---@private local function tree_contains(tree, range) local start_row, start_col, end_row, end_col = tree:root():range() local start_fits = start_row < range[1] or (start_row == range[1] and start_col <= range[2]) @@ -479,6 +493,11 @@ local function tree_contains(tree, range) return false end +--- Determines wether @param range is contained in this language tree +--- +--- This goes down the tree to recursively check childs. +--- +--- @param range A range, that is a `{ start_line, start_col, end_line, end_col }` table. function LanguageTree:contains(range) for _, tree in pairs(self._trees) do if tree_contains(tree, range) then @@ -489,6 +508,9 @@ function LanguageTree:contains(range) return false end +--- Gets the appropriate language that contains @param range +--- +--- @param range A text range, see |LanguageTree:contains| function LanguageTree:language_for_range(range) for _, child in pairs(self._children) do if child:contains(range) then diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index ed5146be44..9b4d28e09a 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -8,6 +8,7 @@ Query.__index = Query local M = {} +---@private local function dedupe_files(files) local result = {} local seen = {} @@ -22,6 +23,7 @@ local function dedupe_files(files) return result end +---@private local function safe_read(filename, read_quantifier) local file, err = io.open(filename, 'r') if not file then @@ -32,6 +34,11 @@ local function safe_read(filename, read_quantifier) return content end +--- Gets the list of files used to make up a query +--- +--- @param lang The language +--- @param query_name The name of the query to load +--- @param is_included Internal parameter, most of the time left as `nil` function M.get_query_files(lang, query_name, is_included) local query_path = string.format('queries/%s/%s.scm', lang, query_name) local lang_files = dedupe_files(a.nvim_get_runtime_file(query_path, true)) @@ -79,6 +86,7 @@ function M.get_query_files(lang, query_name, is_included) return query_files end +---@private local function read_query_files(filenames) local contents = {} @@ -103,19 +111,20 @@ local explicit_queries = setmetatable({}, { --- --- This allows users to override any runtime files and/or configuration --- set by plugins. ----@param lang string: The language to use for the query ----@param query_name string: The name of the query (i.e. "highlights") ----@param text string: The query text (unparsed). +--- +--- @param lang string: The language to use for the query +--- @param query_name string: The name of the query (i.e. "highlights") +--- @param text string: The query text (unparsed). function M.set_query(lang, query_name, text) explicit_queries[lang][query_name] = M.parse_query(lang, text) end --- Returns the runtime query {query_name} for {lang}. --- --- @param lang The language to use for the query --- @param query_name The name of the query (i.e. "highlights") --- --- @return The corresponding query, parsed. +--- +--- @param lang The language to use for the query +--- @param query_name The name of the query (i.e. "highlights") +--- +--- @return The corresponding query, parsed. function M.get_query(lang, query_name) if explicit_queries[lang][query_name] then return explicit_queries[lang][query_name] @@ -129,12 +138,23 @@ function M.get_query(lang, query_name) end end ---- Parses a query. --- --- @param language The language --- @param query A string containing the query (s-expr syntax) --- --- @returns The query +--- Parse {query} as a string. (If the query is in a file, the caller +--- should read the contents into a string before calling). +--- +--- Returns a `Query` (see |lua-treesitter-query|) object which can be used to +--- search nodes in the syntax tree for the patterns defined in {query} +--- using `iter_*` methods below. +--- +--- Exposes `info` and `captures` with additional information about the {query}. +--- - `captures` contains the list of unique capture names defined in +--- {query}. +--- -` info.captures` also points to `captures`. +--- - `info.patterns` contains information about predicates. +--- +--- @param lang The language +--- @param query A string containing the query (s-expr syntax) +--- +--- @returns The query function M.parse_query(lang, query) language.require_language(lang) local self = setmetatable({}, Query) @@ -147,8 +167,9 @@ end -- TODO(vigoux): support multiline nodes too --- Gets the text corresponding to a given node --- @param node the node --- @param bufnr the buffer from which the node is extracted. +--- +--- @param node the node +--- @param bsource The buffer or string from which the node is extracted function M.get_node_text(node, source) local start_row, start_col, start_byte = node:start() local end_row, end_col, end_byte = node:end_() @@ -200,6 +221,7 @@ local predicate_handlers = { ["match?"] = (function() local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true} + ---@private local function check_magic(str) if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then return str @@ -253,7 +275,11 @@ local directive_handlers = { ["set!"] = function(_, _, _, pred, metadata) if #pred == 4 then -- (#set! @capture "key" "value") - metadata[pred[2]][pred[3]] = pred[4] + local capture = pred[2] + if not metadata[capture] then + metadata[capture] = {} + end + metadata[capture][pred[3]] = pred[4] else -- (#set! "key" "value") metadata[pred[2]] = pred[3] @@ -282,10 +308,10 @@ local directive_handlers = { } --- Adds a new predicate to be used in queries --- --- @param name the name of the predicate, without leading # --- @param handler the handler function to be used --- signature will be (match, pattern, bufnr, predicate) +--- +--- @param name the name of the predicate, without leading # +--- @param handler the handler function to be used +--- signature will be (match, pattern, bufnr, predicate) function M.add_predicate(name, handler, force) if predicate_handlers[name] and not force then error(string.format("Overriding %s", name)) @@ -295,10 +321,10 @@ function M.add_predicate(name, handler, force) end --- Adds a new directive to be used in queries --- --- @param name the name of the directive, without leading # --- @param handler the handler function to be used --- signature will be (match, pattern, bufnr, predicate) +--- +--- @param name the name of the directive, without leading # +--- @param handler the handler function to be used +--- signature will be (match, pattern, bufnr, predicate) function M.add_directive(name, handler, force) if directive_handlers[name] and not force then error(string.format("Overriding %s", name)) @@ -312,14 +338,17 @@ function M.list_predicates() return vim.tbl_keys(predicate_handlers) end +---@private local function xor(x, y) return (x or y) and not (x and y) end +---@private local function is_directive(name) return string.sub(name, -1) == "!" end +---@private function Query:match_preds(match, pattern, source) local preds = self.info.patterns[pattern] @@ -358,7 +387,7 @@ function Query:match_preds(match, pattern, source) return true end ---- Applies directives against a match and pattern. +---@private function Query:apply_directives(match, pattern, source, metadata) local preds = self.info.patterns[pattern] @@ -380,6 +409,7 @@ end --- Returns the start and stop value if set else the node's range. -- When the node's range is used, the stop is incremented by 1 -- to make the search inclusive. +---@private local function value_or_node_range(start, stop, node) if start == nil and stop == nil then local node_start, _, node_stop, _ = node:range() @@ -389,15 +419,36 @@ local function value_or_node_range(start, stop, node) return start, stop end ---- Iterates of the captures of self on a given range. --- --- @param node The node under which the search will occur --- @param buffer The source buffer to search --- @param start The starting line of the search --- @param stop The stopping line of the search (end-exclusive) --- --- @returns The matching capture id --- @returns The captured node +--- Iterate over all captures from all matches inside {node} +--- +--- {source} is needed if the query contains predicates, then the caller +--- must ensure to use a freshly parsed tree consistent with the current +--- text of the buffer (if relevent). {start_row} and {end_row} can be used to limit +--- matches inside a row range (this is typically used with root node +--- as the node, i e to get syntax highlight matches in the current +--- viewport). When omitted the start and end row values are used from the given node. +--- +--- The iterator returns three values, a numeric id identifying the capture, +--- the captured node, and metadata from any directives processing the match. +--- The following example shows how to get captures by name: +--- +--- <pre> +--- for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do +--- local name = query.captures[id] -- name of the capture in the query +--- -- typically useful info about the node: +--- local type = node:type() -- type of the captured node +--- local row1, col1, row2, col2 = node:range() -- range of the capture +--- ... use the info here ... +--- end +--- </pre> +--- +--- @param node The node under which the search will occur +--- @param source The source buffer or string to exctract text from +--- @param start The starting line of the search +--- @param stop The stopping line of the search (end-exclusive) +--- +--- @returns The matching capture id +--- @returns The captured node function Query:iter_captures(node, source, start, stop) if type(source) == "number" and source == 0 then source = vim.api.nvim_get_current_buf() @@ -406,6 +457,7 @@ function Query:iter_captures(node, source, start, stop) start, stop = value_or_node_range(start, stop, node) local raw_iter = node:_rawquery(self.query, true, start, stop) + ---@private local function iter() local capture, captured_node, match = raw_iter() local metadata = {} @@ -425,14 +477,35 @@ function Query:iter_captures(node, source, start, stop) end --- Iterates the matches of self on a given range. --- --- @param node The node under which the search will occur --- @param buffer The source buffer to search --- @param start The starting line of the search --- @param stop The stopping line of the search (end-exclusive) --- --- @returns The matching pattern id --- @returns The matching match +--- +--- Iterate over all matches within a node. The arguments are the same as +--- for |query:iter_captures()| but the iterated values are different: +--- an (1-based) index of the pattern in the query, a table mapping +--- capture indices to nodes, and metadata from any directives processing the match. +--- If the query has more than one pattern the capture table might be sparse, +--- and e.g. `pairs()` method should be used over `ipairs`. +--- Here an example iterating over all captures in every match: +--- +--- <pre> +--- for pattern, match, metadata in cquery:iter_matches(tree:root(), bufnr, first, last) do +--- for id, node in pairs(match) do +--- local name = query.captures[id] +--- -- `node` was captured by the `name` capture in the match +--- +--- local node_data = metadata[id] -- Node level metadata +--- +--- ... use the info here ... +--- end +--- end +--- </pre> +--- +--- @param node The node under which the search will occur +--- @param source The source buffer or string to search +--- @param start The starting line of the search +--- @param stop The stopping line of the search (end-exclusive) +--- +--- @returns The matching pattern id +--- @returns The matching match function Query:iter_matches(node, source, start, stop) if type(source) == "number" and source == 0 then source = vim.api.nvim_get_current_buf() diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py index b4d896fecc..d46306d41a 100755 --- a/scripts/gen_vimdoc.py +++ b/scripts/gen_vimdoc.py @@ -186,6 +186,48 @@ CONFIG = { 'module_override': {}, 'append_only': [], }, + 'treesitter': { + 'mode': 'lua', + 'filename': 'treesitter.txt', + 'section_start_token': '*lua-treesitter-core*', + 'section_order': [ + 'treesitter.lua', + 'language.lua', + 'query.lua', + 'highlighter.lua', + 'languagetree.lua', + 'health.lua', + ], + 'files': ' '.join([ + os.path.join(base_dir, 'runtime/lua/vim/treesitter.lua'), + os.path.join(base_dir, 'runtime/lua/vim/treesitter/'), + ]), + 'file_patterns': '*.lua', + 'fn_name_prefix': '', + 'section_name': {}, + 'section_fmt': lambda name: ( + 'Lua module: vim.treesitter' + if name.lower() == 'treesitter' + else f'Lua module: vim.treesitter.{name.lower()}'), + 'helptag_fmt': lambda name: ( + '*lua-treesitter-core*' + if name.lower() == 'treesitter' + else f'*treesitter-{name.lower()}*'), + 'fn_helptag_fmt': lambda fstem, name: ( + f'*{name}()*' + if name != 'new' + else f'*{fstem}.{name}()*'), + # 'fn_helptag_fmt': lambda fstem, name: ( + # f'*vim.treesitter.{name}()*' + # if fstem == 'treesitter' + # else ( + # '*vim.lsp.client*' + # # HACK. TODO(justinmk): class/structure support in lua2dox + # if 'lsp.client' == f'{fstem}.{name}' + # else f'*vim.lsp.{fstem}.{name}()*')), + 'module_override': {}, + 'append_only': [], + } } param_exclude = ( @@ -666,15 +708,6 @@ def extract_from_xml(filename, target, width): annotations = filter(None, map(lambda x: annotation_map.get(x), annotations.split())) - if not fmt_vimhelp: - pass - else: - fstem = '?' - if '.' in compoundname: - fstem = compoundname.split('.')[0] - fstem = CONFIG[target]['module_override'].get(fstem, fstem) - vimtag = CONFIG[target]['fn_helptag_fmt'](fstem, name) - params = [] type_length = 0 @@ -695,17 +728,37 @@ def extract_from_xml(filename, target, width): if fmt_vimhelp and param_type.endswith('*'): param_type = param_type.strip('* ') param_name = '*' + param_name + type_length = max(type_length, len(param_type)) params.append((param_type, param_name)) + # Handle Object Oriented style functions here. + # We make sure they have "self" in the parameters, + # and a parent function + if return_type.startswith('function') \ + and len(return_type.split(' ')) >= 2 \ + and any(x[1] == 'self' for x in params): + split_return = return_type.split(' ') + name = f'{split_return[1]}:{name}' + c_args = [] for param_type, param_name in params: c_args.append((' ' if fmt_vimhelp else '') + ( '%s %s' % (param_type.ljust(type_length), param_name)).strip()) + if not fmt_vimhelp: + pass + else: + fstem = '?' + if '.' in compoundname: + fstem = compoundname.split('.')[0] + fstem = CONFIG[target]['module_override'].get(fstem, fstem) + vimtag = CONFIG[target]['fn_helptag_fmt'](fstem, name) + prefix = '%s(' % name suffix = '%s)' % ', '.join('{%s}' % a[1] for a in params if a[0] not in ('void', 'Error')) + if not fmt_vimhelp: c_decl = '%s %s(%s);' % (return_type, name, ', '.join(c_args)) signature = prefix + suffix @@ -774,7 +827,9 @@ def extract_from_xml(filename, target, width): xrefs.clear() - fns = collections.OrderedDict(sorted(fns.items())) + fns = collections.OrderedDict(sorted( + fns.items(), + key=lambda key_item_tuple: key_item_tuple[0].lower())) deprecated_fns = collections.OrderedDict(sorted(deprecated_fns.items())) return (fns, deprecated_fns) @@ -1002,6 +1057,7 @@ def main(config, args): title, helptag, section_doc = sections.pop(filename) except KeyError: msg(f'warning: empty docs, skipping (target={target}): {filename}') + msg(f' existing docs: {sections.keys()}') continue i += 1 if filename not in CONFIG[target]['append_only']: diff --git a/scripts/lua2dox.lua b/scripts/lua2dox.lua index 1dc4c0a5a0..0b36a1e061 100644 --- a/scripts/lua2dox.lua +++ b/scripts/lua2dox.lua @@ -491,6 +491,27 @@ function TLua2DoX_filter.readfile(this,AppStamp,Filename) end end + -- Big hax + if string.find(fn, ":") then + -- TODO: We need to add a first parameter of "SELF" here + -- local colon_place = string.find(fn, ":") + -- local name = string.sub(fn, 1, colon_place) + fn = fn:gsub(":", ".", 1) + outStream:writeln("/// @param self") + + local paren_start = string.find(fn, "(", 1, true) + local paren_finish = string.find(fn, ")", 1, true) + + -- Nothing in between the parens + local comma + if paren_finish == paren_start + 1 then + comma = "" + else + comma = ", " + end + fn = string.sub(fn, 1, paren_start) .. "self" .. comma .. string.sub(fn, paren_start + 1) + end + -- add vanilla function outStream:writeln(fn_type .. 'function ' .. fn .. '{}') end diff --git a/scripts/pvscheck.sh b/scripts/pvscheck.sh index f054f6e6fe..f3371b485e 100755 --- a/scripts/pvscheck.sh +++ b/scripts/pvscheck.sh @@ -389,7 +389,7 @@ run_analysis() {( detect_url() { local url="${1:-detect}" if test "$url" = detect ; then - curl --silent -L 'https://www.viva64.com/en/pvs-studio-download/' \ + curl --silent -L 'https://pvs-studio.com/en/pvs-studio/download-all/' \ | grep -o 'https\{0,1\}://[^"<>]\{1,\}/pvs-studio[^/"<>]*-x86_64\.tgz' \ || echo FAILED else diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index e79a7a2de2..cc5a62a170 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -1426,6 +1426,10 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, /// - "eol": right after eol character (default) /// - "overlay": display over the specified column, without /// shifting the underlying text. +/// - "right_align": display right aligned in the window. +/// - virt_text_win_col : position the virtual text at a fixed +/// window column (starting from the first +/// text column) /// - virt_text_hide : hide the virtual text when the background /// text is selected or hidden due to /// horizontal scroll 'nowrap' @@ -1574,11 +1578,22 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, decor.virt_text_pos = kVTEndOfLine; } else if (strequal("overlay", str.data)) { decor.virt_text_pos = kVTOverlay; + } else if (strequal("right_align", str.data)) { + decor.virt_text_pos = kVTRightAlign; } else { api_set_error(err, kErrorTypeValidation, "virt_text_pos: invalid value"); goto error; } + } else if (strequal("virt_text_win_col", k.data)) { + if (v->type != kObjectTypeInteger) { + api_set_error(err, kErrorTypeValidation, + "virt_text_win_col is not a Number of the correct size"); + goto error; + } + + decor.col = (int)v->data.integer; + decor.virt_text_pos = kVTWinCol; } else if (strequal("virt_text_hide", k.data)) { decor.virt_text_hide = api_object_to_bool(*v, "virt_text_hide", false, err); @@ -1673,6 +1688,14 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, col2 = 0; } + if (decor.virt_text_pos == kVTRightAlign) { + decor.col = 0; + for (size_t i = 0; i < kv_size(decor.virt_text); i++) { + decor.col += mb_string2cells((char_u *)kv_A(decor.virt_text, i).text); + } + } + + Decoration *d = NULL; if (ephemeral) { diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c index 89fa2f86fb..f942d6b19f 100644 --- a/src/nvim/api/window.c +++ b/src/nvim/api/window.c @@ -381,7 +381,7 @@ Integer nvim_win_get_number(Window window, Error *err) } int tabnr; - win_get_tabwin(window, &tabnr, &rv); + win_get_tabwin(win->handle, &tabnr, &rv); return rv; } diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index e39d2328f5..ca1d141dd8 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -230,6 +230,10 @@ static void decor_add(DecorState *state, int start_row, int start_col, *decor, attr_id, kv_size(decor->virt_text) && owned, -1 }; + if (decor->virt_text_pos == kVTEndOfLine) { + range.win_col = -2; // handled separately + } + kv_pushp(state->active); size_t index; for (index = kv_size(state->active)-1; index > 0; index--) { @@ -242,7 +246,7 @@ static void decor_add(DecorState *state, int start_row, int start_col, kv_A(state->active, index) = range; } -int decor_redraw_col(buf_T *buf, int col, int virt_col, bool hidden, +int decor_redraw_col(buf_T *buf, int col, int win_col, bool hidden, DecorState *state) { if (col <= state->col_until) { @@ -321,8 +325,9 @@ next_mark: attr = hl_combine_attr(attr, item.attr_id); } if ((item.start_row == state->row && item.start_col <= col) - && kv_size(item.decor.virt_text) && item.virt_col == -1) { - item.virt_col = (item.decor.virt_text_hide && hidden) ? -2 : virt_col; + && kv_size(item.decor.virt_text) + && item.decor.virt_text_pos == kVTOverlay && item.win_col == -1) { + item.win_col = (item.decor.virt_text_hide && hidden) ? -2 : win_col; } if (keep) { kv_A(state->active, j++) = item; @@ -340,18 +345,23 @@ void decor_redraw_end(DecorState *state) state->buf = NULL; } -VirtText decor_redraw_eol(buf_T *buf, DecorState *state, int *eol_attr) +VirtText decor_redraw_eol(buf_T *buf, DecorState *state, int *eol_attr, + bool *aligned) { decor_redraw_col(buf, MAXCOL, MAXCOL, false, state); VirtText text = VIRTTEXT_EMPTY; for (size_t i = 0; i < kv_size(state->active); i++) { DecorRange item = kv_A(state->active, i); - if (!kv_size(text) - && item.start_row == state->row && kv_size(item.decor.virt_text) - && item.decor.virt_text_pos == kVTEndOfLine) { - text = item.decor.virt_text; + if (item.start_row == state->row && kv_size(item.decor.virt_text)) { + if (!kv_size(text) && item.decor.virt_text_pos == kVTEndOfLine) { + text = item.decor.virt_text; + } else if (item.decor.virt_text_pos == kVTRightAlign + || item.decor.virt_text_pos == kVTWinCol) { + *aligned = true; + } } + if (item.decor.hl_eol && item.start_row <= state->row) { *eol_attr = hl_combine_attr(*eol_attr, item.attr_id); } diff --git a/src/nvim/decoration.h b/src/nvim/decoration.h index 08d69060f0..4cebc0b731 100644 --- a/src/nvim/decoration.h +++ b/src/nvim/decoration.h @@ -21,6 +21,8 @@ typedef uint16_t DecorPriority; typedef enum { kVTEndOfLine, kVTOverlay, + kVTWinCol, + kVTRightAlign, } VirtTextPos; typedef enum { @@ -41,9 +43,10 @@ struct Decoration // TODO(bfredl): style, signs, etc DecorPriority priority; bool shared; // shared decoration, don't free + int col; // fixed col value, like win_col }; #define DECORATION_INIT { 0, KV_INITIAL_VALUE, kVTEndOfLine, false, \ - kHlModeUnknown, false, DECOR_PRIORITY_BASE, false } + kHlModeUnknown, false, DECOR_PRIORITY_BASE, false, 0 } typedef struct { int start_row; @@ -53,7 +56,7 @@ typedef struct { Decoration decor; int attr_id; // cached lookup of decor.hl_id bool virt_text_owned; - int virt_col; + int win_col; } DecorRange; typedef struct { diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index 950a1a436f..7f28c001f9 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -2719,16 +2719,10 @@ static char_u *get_str_line(int c, void *cookie, int indent, bool do_concat) while (!(p->buf[i] == '\n' || p->buf[i] == '\0')) { i++; } - char buf[2046]; - char *dst; - dst = xstpncpy(buf, (char *)p->buf + p->offset, i - p->offset); - if ((uint32_t)(dst - buf) != i - p->offset) { - smsg(_(":source error parsing command %s"), p->buf); - return NULL; - } - buf[i - p->offset] = '\0'; + size_t line_length = i - p->offset; + char_u *buf = xmemdupz(p->buf + p->offset, line_length); p->offset = i + 1; - return (char_u *)xstrdup(buf); + return buf; } static int source_using_linegetter(void *cookie, diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 75e759094f..3994c5bc5b 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -400,7 +400,10 @@ do wfw = true; winbl = true; winblend = true; winfixheight = true; winfixwidth = true; winhighlight = true; winhl = true; wrap = true; } + + --@private local function new_buf_opt_accessor(bufnr) + --@private local function get(k) if window_options[k] then return a.nvim_err_writeln(k.." is a window option, not a buffer option") @@ -410,23 +413,34 @@ do end return a.nvim_buf_get_option(bufnr or 0, k) end + + --@private local function set(k, v) if window_options[k] then return a.nvim_err_writeln(k.." is a window option, not a buffer option") end return a.nvim_buf_set_option(bufnr or 0, k, v) end + return make_meta_accessor(get, set) end vim.bo = new_buf_opt_accessor(nil) + + --@private local function new_win_opt_accessor(winnr) + + --@private local function get(k) if winnr == nil and type(k) == "number" then return new_win_opt_accessor(k) end return a.nvim_win_get_option(winnr or 0, k) end - local function set(k, v) return a.nvim_win_set_option(winnr or 0, k, v) end + + --@private + local function set(k, v) + return a.nvim_win_set_option(winnr or 0, k, v) + end return make_meta_accessor(get, set) end vim.wo = new_win_opt_accessor(nil) diff --git a/src/nvim/main.c b/src/nvim/main.c index 7064f2a068..56cd97f133 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -375,7 +375,7 @@ int main(int argc, char **argv) // Does ":filetype plugin indent on". filetype_maybe_enable(); // Sources syntax/syntax.vim, which calls `:filetype on`. - syn_maybe_on(); + syn_maybe_enable(); } // Read all the plugin files. diff --git a/src/nvim/message.c b/src/nvim/message.c index 7c98d3c6b5..1783f62247 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -2265,12 +2265,14 @@ void msg_scroll_up(bool may_throttle) /// per screen update. /// /// NB: The bookkeeping is quite messy, and rests on a bunch of poorly -/// documented assumtions. For instance that the message area always grows while -/// being throttled, messages are only being output on the last line etc. +/// documented assumptions. For instance that the message area always grows +/// while being throttled, messages are only being output on the last line +/// etc. /// -/// Probably message scrollback storage should reimplented as a file_buffer, and -/// message scrolling in TUI be reimplemented as a modal floating window. Then -/// we get throttling "for free" using standard redraw_later code paths. +/// Probably message scrollback storage should be reimplemented as a +/// file_buffer, and message scrolling in TUI be reimplemented as a modal +/// floating window. Then we get throttling "for free" using standard +/// redraw_later code paths. void msg_scroll_flush(void) { if (msg_grid.throttled) { diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 6be3b6fb60..5151d82c1b 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -2101,6 +2101,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool search_attr_from_match = false; // if search_attr is from :match bool has_decor = false; // this buffer has decoration bool do_virttext = false; // draw virtual text for this line + int win_col_offset; // offsett for window columns char_u buf_fold[FOLD_TEXT_LEN + 1]; // Hold value returned by get_foldtext @@ -2790,6 +2791,10 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, } } + if (draw_state == WL_NR && n_extra == 0) { + win_col_offset = off; + } + if (wp->w_briopt_sbr && draw_state == WL_BRI - 1 && n_extra == 0 && *p_sbr != NUL) { // draw indent after showbreak value @@ -2904,7 +2909,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, && vcol >= (long)wp->w_virtcol) || (number_only && draw_state > WL_NR)) && filler_todo <= 0) { - draw_virt_text(buf, &col, grid->Columns); + draw_virt_text(buf, win_col_offset, &col, grid->Columns); grid_put_linebuf(grid, row, 0, col, -grid->Columns, wp->w_p_rl, wp, wp->w_hl_attr_normal, false); // Pretend we have finished updating the window. Except when @@ -3945,13 +3950,15 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, draw_color_col = advance_color_col(VCOL_HLC, &color_cols); VirtText virt_text = KV_INITIAL_VALUE; + bool has_aligned = false; if (err_text) { int hl_err = syn_check_group((char_u *)S_LEN("ErrorMsg")); kv_push(virt_text, ((VirtTextChunk){ .text = err_text, .hl_id = hl_err })); do_virttext = true; } else if (has_decor) { - virt_text = decor_redraw_eol(wp->w_buffer, &decor_state, &line_attr); + virt_text = decor_redraw_eol(wp->w_buffer, &decor_state, &line_attr, + &has_aligned); if (kv_size(virt_text)) { do_virttext = true; } @@ -3963,7 +3970,8 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, grid->Columns * (row - startrow + 1) + v && lnum != wp->w_cursor.lnum) || draw_color_col || line_attr_lowprio || line_attr - || diff_hlf != (hlf_T)0 || do_virttext)) { + || diff_hlf != (hlf_T)0 || do_virttext + || has_aligned)) { int rightmost_vcol = 0; int i; @@ -4001,7 +4009,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, } int base_attr = hl_combine_attr(line_attr_lowprio, diff_attr); - if (base_attr || line_attr) { + if (base_attr || line_attr || has_aligned) { rightmost_vcol = INT_MAX; } @@ -4079,7 +4087,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, } } - draw_virt_text(buf, &col, grid->Columns); + draw_virt_text(buf, win_col_offset, &col, grid->Columns); grid_put_linebuf(grid, row, 0, col, grid->Columns, wp->w_p_rl, wp, wp->w_hl_attr_normal, false); row++; @@ -4300,7 +4308,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, && !wp->w_p_rl; // Not right-to-left. int draw_col = col - boguscols; - draw_virt_text(buf, &draw_col, grid->Columns); + draw_virt_text(buf, win_col_offset, &draw_col, grid->Columns); grid_put_linebuf(grid, row, 0, draw_col, grid->Columns, wp->w_p_rl, wp, wp->w_hl_attr_normal, wrap); if (wrap) { @@ -4377,52 +4385,62 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, return row; } -void draw_virt_text(buf_T *buf, int *end_col, int max_col) +void draw_virt_text(buf_T *buf, int col_off, int *end_col, int max_col) { DecorState *state = &decor_state; + int right_pos = max_col; for (size_t i = 0; i < kv_size(state->active); i++) { DecorRange *item = &kv_A(state->active, i); - if (item->start_row == state->row && kv_size(item->decor.virt_text) - && item->decor.virt_text_pos == kVTOverlay - && item->virt_col >= 0) { - VirtText vt = item->decor.virt_text; - HlMode hl_mode = item->decor.hl_mode; - LineState s = LINE_STATE(""); - int virt_attr = 0; - int col = item->virt_col; - size_t virt_pos = 0; - item->virt_col = -2; // deactivate + if (item->start_row == state->row && kv_size(item->decor.virt_text)) { + if (item->win_col == -1) { + if (item->decor.virt_text_pos == kVTRightAlign) { + right_pos -= item->decor.col; + item->win_col = right_pos; + } else if (item->decor.virt_text_pos == kVTWinCol) { + item->win_col = MAX(item->decor.col+col_off, 0); + } + } + if (item->win_col < 0) { + continue; + } + VirtText vt = item->decor.virt_text; + HlMode hl_mode = item->decor.hl_mode; + LineState s = LINE_STATE(""); + int virt_attr = 0; + int col = item->win_col; + size_t virt_pos = 0; + item->win_col = -2; // deactivate - while (col < max_col) { - if (!*s.p) { - if (virt_pos == kv_size(vt)) { - break; - } - s.p = kv_A(vt, virt_pos).text; - int hl_id = kv_A(vt, virt_pos).hl_id; - virt_attr = hl_id > 0 ? syn_id2attr(hl_id) : 0; - virt_pos++; - continue; - } - int attr; - bool through = false; - if (hl_mode == kHlModeCombine) { - attr = hl_combine_attr(linebuf_attr[col], virt_attr); - } else if (hl_mode == kHlModeBlend) { - through = (*s.p == ' '); - attr = hl_blend_attrs(linebuf_attr[col], virt_attr, &through); - } else { - attr = virt_attr; + while (col < max_col) { + if (!*s.p) { + if (virt_pos == kv_size(vt)) { + break; } - schar_T dummy[2]; - int cells = line_putchar(&s, through ? dummy : &linebuf_char[col], - max_col-col, false); + s.p = kv_A(vt, virt_pos).text; + int hl_id = kv_A(vt, virt_pos).hl_id; + virt_attr = hl_id > 0 ? syn_id2attr(hl_id) : 0; + virt_pos++; + continue; + } + int attr; + bool through = false; + if (hl_mode == kHlModeCombine) { + attr = hl_combine_attr(linebuf_attr[col], virt_attr); + } else if (hl_mode == kHlModeBlend) { + through = (*s.p == ' '); + attr = hl_blend_attrs(linebuf_attr[col], virt_attr, &through); + } else { + attr = virt_attr; + } + schar_T dummy[2]; + int cells = line_putchar(&s, through ? dummy : &linebuf_char[col], + max_col-col, false); + linebuf_attr[col++] = attr; + if (cells > 1) { linebuf_attr[col++] = attr; - if (cells > 1) { - linebuf_attr[col++] = attr; - } } - *end_col = MAX(*end_col, col); + } + *end_col = MAX(*end_col, col); } } } diff --git a/src/nvim/sign.c b/src/nvim/sign.c index 5c7b497a19..97e64c6c4c 100644 --- a/src/nvim/sign.c +++ b/src/nvim/sign.c @@ -742,15 +742,15 @@ void sign_mark_adjust( next = sign->se_next; new_lnum = sign->se_lnum; if (sign->se_lnum >= line1 && sign->se_lnum <= line2) { - if (amount == MAXLNUM && (!is_fixed || signcol >= 2)) { + if (amount != MAXLNUM) { + new_lnum += amount; + } else if (!is_fixed || signcol >= 2) { *lastp = next; if (next) { next->se_prev = last; } xfree(sign); continue; - } else { - new_lnum += amount; } } else if (sign->se_lnum > line2) { new_lnum += amount_after; diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index 77a751e5ad..ed886ab7f9 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -3469,13 +3469,13 @@ static void syn_cmd_onoff(exarg_T *eap, char *name) } } -void syn_maybe_on(void) +void syn_maybe_enable(void) { if (!did_syntax_onoff) { exarg_T ea; ea.arg = (char_u *)""; ea.skip = false; - syn_cmd_onoff(&ea, "syntax"); + syn_cmd_enable(&ea, false); } } diff --git a/test/functional/treesitter/parser_spec.lua b/test/functional/treesitter/parser_spec.lua index 72ff6f2fb6..f267f9fb5d 100644 --- a/test/functional/treesitter/parser_spec.lua +++ b/test/functional/treesitter/parser_spec.lua @@ -599,6 +599,56 @@ int x = INT_MAX; eq(result, "value") end) + + describe("when setting a key on a capture", function() + it("it should create the nested table", function() + insert([[ + int x = 3; + ]]) + + local result = exec_lua([[ + local query = require("vim.treesitter.query") + local value + + query = vim.treesitter.parse_query("c", '((number_literal) @number (#set! @number "key" "value"))') + parser = vim.treesitter.get_parser(0, "c") + + for pattern, match, metadata in query:iter_matches(parser:parse()[1]:root(), 0) do + for _, nested_tbl in pairs(metadata) do + return nested_tbl.key + end + end + ]]) + + eq(result, "value") + end) + + it("it should not overwrite the nested table", function() + insert([[ + int x = 3; + ]]) + + local result = exec_lua([[ + local query = require("vim.treesitter.query") + local result + + query = vim.treesitter.parse_query("c", '((number_literal) @number (#set! @number "key" "value") (#set! @number "key2" "value2"))') + parser = vim.treesitter.get_parser(0, "c") + + for pattern, match, metadata in query:iter_matches(parser:parse()[1]:root(), 0) do + for _, nested_tbl in pairs(metadata) do + return nested_tbl + end + end + ]]) + local expected = { + ["key"] = "value", + ["key2"] = "value2", + } + + eq(expected, result) + end) + end) end) end) end) diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua index 82d3075be2..09638df6c5 100644 --- a/test/functional/ui/decorations_spec.lua +++ b/test/functional/ui/decorations_spec.lua @@ -333,6 +333,35 @@ describe('decorations providers', function() ]]} end) + it('can have virtual text of the style: right_align', function() + insert(mulholland) + setup_provider [[ + local hl = a.nvim_get_hl_id_by_name "ErrorMsg" + local test_ns = a.nvim_create_namespace "mulholland" + function on_do(event, ...) + if event == "line" then + local win, buf, line = ... + a.nvim_buf_set_extmark(buf, test_ns, line, 0, { + virt_text = {{'+'}, {string.rep(' ', line+1), 'ErrorMsg'}}; + virt_text_pos='right_align'; + ephemeral = true; + }) + end + end + ]] + + screen:expect{grid=[[ + // just to see if there was an acciden+{2: }| + // on Mulholland Drive +{2: }| + try_start(); +{2: }| + bufref_T save_buf; +{2: }| + switch_buffer(&save_buf, buf); +{2: }| + posp = getmark(mark, false); +{2: }| + restore_buffer(&save_buf);^ +{2: }| + | + ]]} + end) + it('can highlight beyond EOL', function() insert(mulholland) setup_provider [[ @@ -366,7 +395,7 @@ describe('decorations providers', function() end) describe('extmark decorations', function() - local screen + local screen, ns before_each( function() clear() screen = Screen.new(50, 15) @@ -397,6 +426,8 @@ describe('extmark decorations', function() [23] = {foreground = Screen.colors.Magenta1, background = Screen.colors.LightGrey}; [24] = {bold = true}; } + + ns = meths.create_namespace 'test' end) local example_text = [[ @@ -417,7 +448,6 @@ end]] insert(example_text) feed 'gg' - local ns = meths.create_namespace 'test' for i = 1,9 do meths.buf_set_extmark(0, ns, i, 0, { virt_text={{'|', 'LineNr'}}, virt_text_pos='overlay'}) if i == 3 or (i >= 6 and i <= 9) then @@ -484,7 +514,6 @@ end]] it('can have virtual text of overlay position and styling', function() insert(example_text) feed 'gg' - local ns = meths.create_namespace 'test' command 'set ft=lua' command 'syntax on' @@ -572,4 +601,88 @@ end]] {24:-- VISUAL LINE --} | ]]} end) + + it('can have virtual text of fixed win_col position', function() + insert(example_text) + feed 'gg' + meths.buf_set_extmark(0, ns, 1, 0, { virt_text={{'Very', 'ErrorMsg'}}, virt_text_win_col=31, hl_mode='blend'}) + meths.buf_set_extmark(0, ns, 2, 10, { virt_text={{'Much', 'ErrorMsg'}}, virt_text_win_col=31, hl_mode='blend'}) + meths.buf_set_extmark(0, ns, 3, 15, { virt_text={{'Error', 'ErrorMsg'}}, virt_text_win_col=31, hl_mode='blend'}) + meths.buf_set_extmark(0, ns, 7, 21, { virt_text={{'-', 'NonText'}}, virt_text_win_col=4, hl_mode='blend'}) + + screen:expect{grid=[[ + ^for _,item in ipairs(items) do | + local text, hl_id_cell, cou{4:Very} unpack(item) | + if hl_id_cell ~= nil then {4:Much} | + hl_id = hl_id_cell {4:Error} | + end | + for _ = 1, (count or 1) do | + local cell = line[colpos] | + {1:-} cell.text = text | + cell.hl_id = hl_id | + colpos = colpos+1 | + end | + end | + {1:~ }| + {1:~ }| + | + ]]} + + feed '3G12|i<cr><esc>' + screen:expect{grid=[[ + for _,item in ipairs(items) do | + local text, hl_id_cell, cou{4:Very} unpack(item) | + if hl_i {4:Much} | + ^d_cell ~= nil then | + hl_id = hl_id_cell {4:Error} | + end | + for _ = 1, (count or 1) do | + local cell = line[colpos] | + {1:-} cell.text = text | + cell.hl_id = hl_id | + colpos = colpos+1 | + end | + end | + {1:~ }| + | + ]]} + + feed 'u:<cr>' + screen:expect{grid=[[ + for _,item in ipairs(items) do | + local text, hl_id_cell, cou{4:Very} unpack(item) | + if hl_i^d_cell ~= nil then {4:Much} | + hl_id = hl_id_cell {4:Error} | + end | + for _ = 1, (count or 1) do | + local cell = line[colpos] | + {1:-} cell.text = text | + cell.hl_id = hl_id | + colpos = colpos+1 | + end | + end | + {1:~ }| + {1:~ }| + : | + ]]} + + feed '8|i<cr><esc>' + screen:expect{grid=[[ + for _,item in ipairs(items) do | + local text, hl_id_cell, cou{4:Very} unpack(item) | + if | + ^hl_id_cell ~= nil then {4:Much} | + hl_id = hl_id_cell {4:Error} | + end | + for _ = 1, (count or 1) do | + local cell = line[colpos] | + {1:-} cell.text = text | + cell.hl_id = hl_id | + colpos = colpos+1 | + end | + end | + {1:~ }| + | + ]]} + end) end) |