diff options
-rw-r--r-- | runtime/doc/lua.txt | 10 | ||||
-rw-r--r-- | runtime/doc/treesitter.txt | 563 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 3 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 38 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/health.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 22 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 20 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 160 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 153 | ||||
-rwxr-xr-x | scripts/gen_vimdoc.py | 76 | ||||
-rw-r--r-- | scripts/lua2dox.lua | 21 | ||||
-rw-r--r-- | src/nvim/api/buffer.c | 23 | ||||
-rw-r--r-- | src/nvim/api/window.c | 2 | ||||
-rw-r--r-- | src/nvim/decoration.c | 26 | ||||
-rw-r--r-- | src/nvim/decoration.h | 7 | ||||
-rw-r--r-- | src/nvim/lua/vim.lua | 16 | ||||
-rw-r--r-- | src/nvim/main.c | 2 | ||||
-rw-r--r-- | src/nvim/screen.c | 106 | ||||
-rw-r--r-- | src/nvim/sign.c | 6 | ||||
-rw-r--r-- | src/nvim/syntax.c | 4 | ||||
-rw-r--r-- | test/functional/ui/decorations_spec.lua | 119 |
22 files changed, 1019 insertions, 364 deletions
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/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..db6d7e4dc0 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 @@ -282,10 +304,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 +317,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 +334,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 +383,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 +405,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 +415,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 +453,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 +473,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/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/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/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/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) |