diff --git a/.emmyrc.json b/.emmyrc.json index 70c8faa3f4..dd49324bbf 100644 --- a/.emmyrc.json +++ b/.emmyrc.json @@ -27,8 +27,6 @@ }, "diagnostics": { "disable": [ - "annotation-usage-error", - "code-style-check", "incomplete-signature-doc", "iter-variable-reassign", "unnecessary-if" @@ -42,6 +40,7 @@ "unused": "warning" }, "enables": [ + "code-style-check", "missing-global-doc", "non-literal-expressions-in-assert", "unknown-doc-tag" diff --git a/runtime/doc/dev.txt b/runtime/doc/dev.txt index 128d4a4ece..250d8d17e0 100644 --- a/runtime/doc/dev.txt +++ b/runtime/doc/dev.txt @@ -248,6 +248,7 @@ Docstring format: 0), it is marked as "experimental". - See scripts/util.lua for the mapping of api-level to Nvim version. - Use `@deprecated` to mark deprecated functions. +- For callable module tables, document `__call` in `setmetatable(M, { ... })`. - Use `@nodoc` to prevent documentation generation. - Use `@private` for private functions. - Naming a function with an underscore prefix ("_foo", not "foo") implies diff --git a/runtime/doc/lua-async.txt b/runtime/doc/lua-async.txt index 5a0cb8ca64..e39b616799 100644 --- a/runtime/doc/lua-async.txt +++ b/runtime/doc/lua-async.txt @@ -380,6 +380,20 @@ run({func}, {...}) *vim.async.run()* Return: ~ (`vim.async.Task`) +semaphore({permits}) *vim.async.semaphore()* + Create an async semaphore that allows up to a given number of + acquisitions. + + Prefer |Semaphore:with()| for most uses so permits are released reliably. + Use |Semaphore:acquire()| and |Semaphore:release()| directly only when the + acquire and release points cannot be expressed as one function call. + + Parameters: ~ + • {permits} (`integer?`) (default: 1) + + Return: ~ + (`vim.async.Semaphore`) + Semaphore:acquire() *Semaphore:acquire()* Acquire a semaphore permit. diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index c7bad0efc2..b610842133 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1182,6 +1182,36 @@ vim.wo[{winid}][{bufnr}] *vim.wo* ============================================================================== Lua module: vim *lua-vim* +*vim.Ringbuf* + + Fields: ~ + • {clear} (`fun()`) See |Ringbuf:clear()|. + • {peek} (`fun(): T?`) See |Ringbuf:peek()|. + • {pop} (`fun(): T?`) See |Ringbuf:pop()|. + • {push} (`fun(item: T)`) See |Ringbuf:push()|. + + +Ringbuf:clear() *Ringbuf:clear()* + Clear all items + +Ringbuf:peek() *Ringbuf:peek()* + Returns the first unread item without removing it + + Return: ~ + (`T?`) + +Ringbuf:pop() *Ringbuf:pop()* + Removes and returns the first unread item + + Return: ~ + (`T?`) + +Ringbuf:push({item}) *Ringbuf:push()* + Adds an item, overriding the oldest item if the buffer is full. + + Parameters: ~ + • {item} (`T`) + vim.cmd({cmd}) *vim.cmd()* Executes Vimscript (|Ex-command|s). @@ -1228,443 +1258,6 @@ vim.cmd({cmd}) *vim.cmd()* See also: ~ • |ex-cmd-index| -vim.defer_fn({fn}, {timeout}) *vim.defer_fn()* - Defers calling {fn} until {timeout} ms passes. - - Use to do a one-shot timer that calls {fn}. Note: The {fn} is |schedule|d - automatically, so API functions are safe to call. - - Parameters: ~ - • {fn} (`function`) Callback to call once `timeout` expires - • {timeout} (`integer`) Number of milliseconds to wait before calling - `fn` - - Return: ~ - (`uv.uv_timer_t`) timer luv timer object - - *vim.deprecate()* -vim.deprecate({name}, {alternative}, {version}, {plugin}, {backtrace}) - Shows a deprecation message to the user. - - Parameters: ~ - • {name} (`string`) Deprecated feature (function, API, etc.). - • {alternative} (`string?`) Suggested alternative feature. - • {version} (`string`) Version when the deprecated function will be - removed. - • {plugin} (`string?`) Name of the plugin that owns the deprecated - feature. Defaults to "Nvim". - • {backtrace} (`boolean?`) Prints backtrace. Defaults to true. - - Return: ~ - (`string?`) Deprecation message, or nil if no message was shown. - -vim.inspect() *vim.inspect()* - Gets a human-readable representation of the given object. - - Overloads: ~ - • `fun(x: any, opts?: vim.inspect.Opts): string` - - Return: ~ - (`string`) - - See also: ~ - • |vim.print()| - • https://github.com/kikito/inspect.lua - • https://github.com/mpeterv/vinspect - -vim.keycode({keys}, {info}) *vim.keycode()* - Converts keys from |key-notation| to the internal encoding. Optionally - returns structured *key-chord* info as retval 2. - - Inverse of |keytrans()|, which converts the internal encoding back to - |key-notation|. - - Examples: >lua - local k = vim.keycode - vim.g.mapleader = k'' - - -- Split a key sequence into chords, e.g. to inspect modifiers. - local _, chords = vim.keycode('v', true) - assert(chords[1].key == 'w' and chords[1].mod[1] == 'C') - - -- keytrans() is the inverse: internal encoding => key-notation. - assert(vim.fn.keytrans(vim.keycode('')) == '') -< - - Parameters: ~ - • {keys} (`string`) Keys in |key-notation|. - • {info} (`boolean?`) Also return key-chord info. - - Return (multiple): ~ - (`string`) Internal representation of the given `keys`. - (`table?`) List of parsed key-chords, each with fields: - • {key} (`string`) Key without modifiers. Example: `` has - `key="a"`. - • {key_alt} (`string?`) Alternative spelling of `key`, or nil if There - Is No Alternative (TINA). Example: `"<"` has `key_alt="lt"`. - • {keys} (`string`) Full key-chord in canonical key-notation (as - produced by |keytrans()|), including modifiers. Example: `` has - `keys=""`. - • {mod} (`('M'|'T'|'C'|'S'|'2'|'3'|'4'|'D')[]`) List of key-chord - modifiers. Example: `` has `mod={ 'C', 'S' }`. - - See also: ~ - • |nvim_replace_termcodes()| - • |keytrans()| - -vim.lua_omnifunc({find_start}) *vim.lua_omnifunc()* - Omnifunc for completing Lua values from the runtime Lua interpreter, - similar to the builtin completion for the `:lua` command. - - Activate using `vim.bo.omnifunc = vim.lua_omnifunc` in a Lua buffer. - - Parameters: ~ - • {find_start} (`1|0`) - -vim.notify({msg}, {level}, {opts}) *vim.notify()* - Displays a notification to the user. - - This function can be overridden by plugins to display notifications using - a custom provider (such as the system notification provider). By default, - writes to |:messages|. - - Parameters: ~ - • {msg} (`string`) Content of the notification to show to the user. - • {level} (`integer?`) One of the values from |vim.log.levels|. - • {opts} (`table?`) Optional parameters. Unused by default. - -vim.notify_once({msg}, {level}, {opts}) *vim.notify_once()* - Displays a notification only one time. - - Like |vim.notify()|, but subsequent calls with the same message will not - display a notification. - - Parameters: ~ - • {msg} (`string`) Content of the notification to show to the user. - • {level} (`integer?`) One of the values from |vim.log.levels|. - • {opts} (`table?`) Optional parameters. Unused by default. - - Return: ~ - (`boolean`) true if message was displayed, else false - -vim.on_key({fn}, {ns_id}, {opts}) *vim.on_key()* - Registers function {fn} with |namespace| {ns_id} as a listener to EVERY - input key (except keys consumed by |getchar()|, by {fn} itself, or by a - payload). - - To parse |key-chord|s, see |vim.keycode()|. Example: >lua - local keychords = vim.keycode(vim.fn.keytrans(key), true) -< - - Note: ~ - • If {fn} returns an empty string, {key} is discarded/ignored; if {key} - is then the "…" sequence is discarded as a whole. - • Non-recursive: if {fn} itself consumes input, it won't be invoked for - those keys. - • Not invoked for keys consumed by getchar()/getcharstr(): they never - reach the main loop. - • To UNregister a given {ns_id}, pass `nil` {fn}. - • {fn} will be removed on error. - • {fn} will not be cleared by |nvim_buf_clear_namespace()| - • The |-w| command-line option is related but does not support callbacks - and cannot be toggled dynamically. - - Parameters: ~ - • {fn} (`fun(key: string, typed: string): string??`) Function - invoked for every input key, after mappings have been applied - but before further processing. Arguments {key} and {typed} - are raw input (use |keytrans()| to get |keycodes|). - • {key} is the key after mappings are applied. - • {typed} is the input before mappings are applied; may be - empty if {key} was produced by non-typed key(s) or by the - same typed key(s) that produced a previous {key}. - • {ns_id} (`integer?`) Namespace ID. If nil or 0, returns a new - |namespace| id. - • {opts} (`table?`) Optional parameters - - Return: ~ - (`integer`) Namespace id associated with {fn}. Or count of all - callbacks if on_key() is called without arguments. - - See also: ~ - • |keytrans()| - • |vim.keycode()| - -vim.paste({lines}, {phase}) *vim.paste()* - Paste handler, invoked by |nvim_paste()|. - - Note: This is provided only as a "hook", don't call it directly; call - |nvim_paste()| instead, which arranges redo (dot-repeat) and invokes - `vim.paste`. - - Example: To remove ANSI color codes when pasting: >lua - vim.paste = (function(overridden) - return function(lines, phase) - for i,line in ipairs(lines) do - -- Scrub ANSI color codes from paste input. - lines[i] = line:gsub('\27%[[0-9;mK]+', '') - end - return overridden(lines, phase) - end - end)(vim.paste) -< - - Parameters: ~ - • {lines} (`string[]`) |readfile()|-style list of lines to paste. - |channel-lines| - • {phase} (`-1|1|2|3`) -1: "non-streaming" paste: the call contains all - lines. If paste is "streamed", `phase` indicates the stream - state: - • 1: starts the paste (exactly once) - • 2: continues the paste (zero or more times) - • 3: ends the paste (exactly once) - - Return: ~ - (`boolean`) result false if client should cancel the paste. - - See also: ~ - • |paste| - -vim.print({...}) *vim.print()* - "Pretty prints" the given arguments and returns them unmodified. - - Example: >lua - local hl_normal = vim.print(vim.api.nvim_get_hl(0, { name = 'Normal' })) -< - - Parameters: ~ - • {...} (`any`) - - Return: ~ - (`any`) given arguments. - - See also: ~ - • |vim.inspect()| - • |:=| - -vim.schedule_wrap({fn}) *vim.schedule_wrap()* - Returns a function which calls {fn} via |vim.schedule()|. - - The returned function passes all arguments to {fn}. - - Example: >lua - function notify_readable(_err, readable) - vim.notify("readable? " .. tostring(readable)) - end - vim.uv.fs_access(vim.fn.stdpath("config"), "R", vim.schedule_wrap(notify_readable)) -< - - Parameters: ~ - • {fn} (`function`) - - Return: ~ - (`function`) - - See also: ~ - • |lua-loop-callbacks| - • |vim.schedule()| - • |vim.in_fast_event()| - - *vim.str_byteindex()* -vim.str_byteindex({s}, {encoding}, {index}, {strict_indexing}) - Convert UTF-32, UTF-16 or UTF-8 {index} to byte index. If - {strict_indexing} is false then an out of range index will return byte - length instead of throwing an error. - - Invalid UTF-8 and NUL is treated like in |vim.str_utfindex()|. An {index} - in the middle of a UTF-16 sequence is rounded upwards to the end of that - sequence. - - Parameters: ~ - • {s} (`string`) - • {encoding} (`"utf-8"|"utf-16"|"utf-32"`) - • {index} (`integer`) - • {strict_indexing} (`boolean?`) default: true - - Return: ~ - (`integer`) - - *vim.str_utfindex()* -vim.str_utfindex({s}, {encoding}, {index}, {strict_indexing}) - Convert byte index to UTF-32, UTF-16 or UTF-8 indices. If {index} is not - supplied, the length of the string is used. All indices are zero-based. - - If {strict_indexing} is false then an out of range index will return - string length instead of throwing an error. Invalid UTF-8 bytes, and - embedded surrogates are counted as one code point each. An {index} in the - middle of a UTF-8 sequence is rounded upwards to the end of that sequence. - - Parameters: ~ - • {s} (`string`) - • {encoding} (`"utf-8"|"utf-16"|"utf-32"`) - • {index} (`integer?`) - • {strict_indexing} (`boolean?`) default: true - - Return: ~ - (`integer`) - -vim.wait({time}, {callback}, {interval}, {fast_only}) *vim.wait()* - Waits up to `time` milliseconds, until `callback` returns `true` - (success). Executes `callback` immediately, then on user events, internal - events, and approximately every `interval` milliseconds (default 200). - Returns `true` plus any remaining callback results on success. - - Nvim processes other events while waiting. Cannot be called during an - |api-fast| event. - - Examples: >lua - -- Wait for 100 ms, allowing other events to process. - vim.wait(100) - - -- Wait up to 1000 ms or until `vim.g.foo` is true, at intervals of ~500 ms. - vim.wait(1000, function() return vim.g.foo end, 500) - - -- Wait indefinitely until `vim.g.foo` is true, and get the callback results. - local ok, rv1, rv2, rv3 = vim.wait(math.huge, function() - return vim.g.foo, 'a', 42, { ok = { 'yes' } } - end) - - -- Schedule a function to set a value in 100ms. This would wait 10s if blocked, but actually - -- only waits 100ms because `vim.wait` processes other events while waiting. - vim.defer_fn(function() vim.g.timer_result = true end, 100) - if vim.wait(10000, function() return vim.g.timer_result end) then - print('Only waiting a little bit of time!') - end - - -- Yield via vim.wait() to allow CTRL-C to interrupt Lua code. - while true do - -- ...work... - local _, code = vim.wait(0, nil, 0) - if code == -2 then -- CTRL-C. - break - end - end -< - - Parameters: ~ - • {time} (`number`) Number of milliseconds to wait. Must be - non-negative number, any fractional part is truncated. - • {callback} (`fun(): boolean, ...?`) Optional callback. Waits until - {callback} returns true - • {interval} (`integer?`) (Approximate) number of milliseconds to wait - between polls - • {fast_only} (`boolean?`) If true, only |api-fast| events will be - processed. - - Return (multiple): ~ - (`boolean`) - (`-1|-2?`) - • If callback returns `true` before timeout: `true, ...` (remaining - callback results). - • On timeout: `false, -1` - • On interrupt: `false, -2` - • On error: the error is raised. - - -============================================================================== -Lua module: vim.inspector *vim.inspector* - -vim.inspect_pos({buf}, {row}, {col}, {filter}) *vim.inspect_pos()* - Get all the items at a given buffer position. - - Can also be pretty-printed with `:Inspect!`. *:Inspect!* - - Attributes: ~ - Since: 0.9.0 - - Parameters: ~ - • {buf} (`integer?`) defaults to the current buffer - • {row} (`integer?`) row to inspect, 0-based. Defaults to the row of - the current cursor - • {col} (`integer?`) col to inspect, 0-based. Defaults to the col of - the current cursor - • {filter} (`table?`) Table with key-value pairs to filter the items - • {extmarks} (`boolean|"all"`, default: true) Include - extmarks. When `all`, then extmarks without a `hl_group` - will also be included. - • {semantic_tokens} (`boolean`, default: true) Include - semantic token highlights. - • {syntax} (`boolean`, default: `true`) Include syntax based - highlight groups. - • {treesitter} (`boolean`, default: `true`) Include - treesitter based highlight groups. - - Return: ~ - (`table`) a table with the following key-value pairs. Items are in - "traversal order": - • treesitter: a list of treesitter captures - • syntax: a list of syntax groups - • semantic_tokens: a list of semantic tokens - • extmarks: a list of extmarks - • buffer: the buffer used to get the items - • row: the row used to get the items - • col: the col used to get the items - -vim.show_pos({buf}, {row}, {col}, {filter}) *vim.show_pos()* - Show all the items at a given buffer position. - - Can also be shown with `:Inspect`. *:Inspect* - - See also |:marks| to list all extmarks. - - Example: To bind this function to the vim-scriptease inspired `zS` in - Normal mode: >lua - vim.keymap.set('n', 'zS', vim.show_pos) -< - - Attributes: ~ - Since: 0.9.0 - - Parameters: ~ - • {buf} (`integer?`) defaults to the current buffer - • {row} (`integer?`) row to inspect, 0-based. Defaults to the row of - the current cursor - • {col} (`integer?`) col to inspect, 0-based. Defaults to the col of - the current cursor - • {filter} (`table?`) A table with the following fields: - • {extmarks} (`boolean|"all"`, default: true) Include - extmarks. When `all`, then extmarks without a `hl_group` - will also be included. - • {semantic_tokens} (`boolean`, default: true) Include - semantic token highlights. - • {syntax} (`boolean`, default: `true`) Include syntax based - highlight groups. - • {treesitter} (`boolean`, default: `true`) Include - treesitter based highlight groups. - - - - -*vim.Ringbuf* - - Fields: ~ - • {clear} (`fun()`) See |Ringbuf:clear()|. - • {peek} (`fun(): T?`) See |Ringbuf:peek()|. - • {pop} (`fun(): T?`) See |Ringbuf:pop()|. - • {push} (`fun(item: T)`) See |Ringbuf:push()|. - - -Ringbuf:clear() *Ringbuf:clear()* - Clear all items - -Ringbuf:peek() *Ringbuf:peek()* - Returns the first unread item without removing it - - Return: ~ - (`T?`) - -Ringbuf:pop() *Ringbuf:pop()* - Removes and returns the first unread item - - Return: ~ - (`T?`) - -Ringbuf:push({item}) *Ringbuf:push()* - Adds an item, overriding the oldest item if the buffer is full. - - Parameters: ~ - • {item} (`T`) - vim.deep_equal({a}, {b}) *vim.deep_equal()* Deep compare values for equality @@ -1718,6 +1311,36 @@ vim.defaulttable({createfn}) *vim.defaulttable()* Return: ~ (`table`) Empty table with `__index` metamethod. +vim.defer_fn({fn}, {timeout}) *vim.defer_fn()* + Defers calling {fn} until {timeout} ms passes. + + Use to do a one-shot timer that calls {fn}. Note: The {fn} is |schedule|d + automatically, so API functions are safe to call. + + Parameters: ~ + • {fn} (`function`) Callback to call once `timeout` expires + • {timeout} (`integer`) Number of milliseconds to wait before calling + `fn` + + Return: ~ + (`uv.uv_timer_t`) timer luv timer object + + *vim.deprecate()* +vim.deprecate({name}, {alternative}, {version}, {plugin}, {backtrace}) + Shows a deprecation message to the user. + + Parameters: ~ + • {name} (`string`) Deprecated feature (function, API, etc.). + • {alternative} (`string?`) Suggested alternative feature. + • {version} (`string`) Version when the deprecated function will be + removed. + • {plugin} (`string?`) Name of the plugin that owns the deprecated + feature. Defaults to "Nvim". + • {backtrace} (`boolean?`) Prints backtrace. Defaults to true. + + Return: ~ + (`string?`) Deprecation message, or nil if no message was shown. + vim.endswith({s}, {suffix}) *vim.endswith()* Tests if `s` ends with `suffix`. @@ -1764,6 +1387,34 @@ vim.gsplit({s}, {sep}, {opts}) *vim.gsplit()* • https://www.lua.org/pil/20.2.html • http://lua-users.org/wiki/StringLibraryTutorial +vim.inspect({root}, {options}) *vim.inspect()* + Gets a human-readable representation of the given object. + + Parameters: ~ + • {root} (`any`) + • {options} (`table?`) A table with the following fields: + • {depth}? (`integer`, default: unlimited) Maximum table + nesting depth. Deeper tables are shown as `{...}`. + • {indent}? (`string`, default: `" "`) String repeated for + each level of indentation. + • {newline}? (`string`, default: `"\n"`) String used for + line breaks. + • {process}? (`fun(item: any, path: any[]): any`) + Transforms values, keys, and metatables before + formatting. Return a replacement value, or `nil` to omit + a table entry. {path} lists the keys leading to {item}, + with `vim.inspect.KEY` appended for keys or + `vim.inspect.METATABLE` for metatables. The root has an + empty path. + + Return: ~ + (`string`) + + See also: ~ + • |vim.print()| + • https://github.com/kikito/inspect.lua + • https://github.com/mpeterv/vinspect + vim.is_callable({f}) *vim.is_callable()* Returns true if object `f` can be called as a function. @@ -1822,6 +1473,46 @@ vim.isnil({t}) *vim.isnil()* Return: ~ (`boolean`) `true` if `nil` or |vim.NIL|, else `false`. +vim.keycode({keys}, {info}) *vim.keycode()* + Converts keys from |key-notation| to the internal encoding. Optionally + returns structured *key-chord* info as retval 2. + + Inverse of |keytrans()|, which converts the internal encoding back to + |key-notation|. + + Examples: >lua + local k = vim.keycode + vim.g.mapleader = k'' + + -- Split a key sequence into chords, e.g. to inspect modifiers. + local _, chords = vim.keycode('v', true) + assert(chords[1].key == 'w' and chords[1].mod[1] == 'C') + + -- keytrans() is the inverse: internal encoding => key-notation. + assert(vim.fn.keytrans(vim.keycode('')) == '') +< + + Parameters: ~ + • {keys} (`string`) Keys in |key-notation|. + • {info} (`boolean?`) Also return key-chord info. + + Return (multiple): ~ + (`string`) Internal representation of the given `keys`. + (`table?`) List of parsed key-chords, each with fields: + • {key} (`string`) Key without modifiers. Example: `` has + `key="a"`. + • {key_alt} (`string?`) Alternative spelling of `key`, or nil if There + Is No Alternative (TINA). Example: `"<"` has `key_alt="lt"`. + • {keys} (`string`) Full key-chord in canonical key-notation (as + produced by |keytrans()|), including modifiers. Example: `` has + `keys=""`. + • {mod} (`('M'|'T'|'C'|'S'|'2'|'3'|'4'|'D')[]`) List of key-chord + modifiers. Example: `` has `mod={ 'C', 'S' }`. + + See also: ~ + • |nvim_replace_termcodes()| + • |keytrans()| + vim.list.bisect({t}, {val}, {opts}) *vim.list.bisect()* Search for a position in a sorted |lua-list| {t} where {val} can be inserted while keeping the list sorted. @@ -1970,6 +1661,15 @@ vim.list_slice({list}, {start}, {finish}) *vim.list_slice()* Return: ~ (`any[]`) Copy of table sliced from start to finish (inclusive) +vim.lua_omnifunc({find_start}) *vim.lua_omnifunc()* + Omnifunc for completing Lua values from the runtime Lua interpreter, + similar to the builtin completion for the `:lua` command. + + Activate using `vim.bo.omnifunc = vim.lua_omnifunc` in a Lua buffer. + + Parameters: ~ + • {find_start} (`1|0`) + vim.nonnil({...}) *vim.nonnil()* Returns the first argument which is not nil. @@ -1992,6 +1692,32 @@ vim.nonnil({...}) *vim.nonnil()* Return: ~ (`any`) +vim.notify({msg}, {level}, {opts}) *vim.notify()* + Displays a notification to the user. + + This function can be overridden by plugins to display notifications using + a custom provider (such as the system notification provider). By default, + writes to |:messages|. + + Parameters: ~ + • {msg} (`string`) Content of the notification to show to the user. + • {level} (`integer?`) One of the values from |vim.log.levels|. + • {opts} (`table?`) Optional parameters. Unused by default. + +vim.notify_once({msg}, {level}, {opts}) *vim.notify_once()* + Displays a notification only one time. + + Like |vim.notify()|, but subsequent calls with the same message will not + display a notification. + + Parameters: ~ + • {msg} (`string`) Content of the notification to show to the user. + • {level} (`integer?`) One of the values from |vim.log.levels|. + • {opts} (`table?`) Optional parameters. Unused by default. + + Return: ~ + (`boolean`) true if message was displayed, else false + vim.npcall({fn}, {...}) *vim.npcall()* Calls the function `fn` in `protected mode` like |pcall()|, but returns `nil` on error. @@ -2006,6 +1732,84 @@ vim.npcall({fn}, {...}) *vim.npcall()* Return: ~ (`any`) ... +vim.on_key({fn}, {ns_id}, {opts}) *vim.on_key()* + Registers function {fn} with |namespace| {ns_id} as a listener to EVERY + input key (except keys consumed by |getchar()|, by {fn} itself, or by a + payload). + + To parse |key-chord|s, see |vim.keycode()|. Example: >lua + local keychords = vim.keycode(vim.fn.keytrans(key), true) +< + + Note: ~ + • If {fn} returns an empty string, {key} is discarded/ignored; if {key} + is then the "…" sequence is discarded as a whole. + • Non-recursive: if {fn} itself consumes input, it won't be invoked for + those keys. + • Not invoked for keys consumed by getchar()/getcharstr(): they never + reach the main loop. + • To UNregister a given {ns_id}, pass `nil` {fn}. + • {fn} will be removed on error. + • {fn} will not be cleared by |nvim_buf_clear_namespace()| + • The |-w| command-line option is related but does not support callbacks + and cannot be toggled dynamically. + + Parameters: ~ + • {fn} (`fun(key: string, typed: string): string??`) Function + invoked for every input key, after mappings have been applied + but before further processing. Arguments {key} and {typed} + are raw input (use |keytrans()| to get |keycodes|). + • {key} is the key after mappings are applied. + • {typed} is the input before mappings are applied; may be + empty if {key} was produced by non-typed key(s) or by the + same typed key(s) that produced a previous {key}. + • {ns_id} (`integer?`) Namespace ID. If nil or 0, returns a new + |namespace| id. + • {opts} (`table?`) Optional parameters + + Return: ~ + (`integer`) Namespace id associated with {fn}. Or count of all + callbacks if on_key() is called without arguments. + + See also: ~ + • |keytrans()| + • |vim.keycode()| + +vim.paste({lines}, {phase}) *vim.paste()* + Paste handler, invoked by |nvim_paste()|. + + Note: This is provided only as a "hook", don't call it directly; call + |nvim_paste()| instead, which arranges redo (dot-repeat) and invokes + `vim.paste`. + + Example: To remove ANSI color codes when pasting: >lua + vim.paste = (function(overridden) + return function(lines, phase) + for i,line in ipairs(lines) do + -- Scrub ANSI color codes from paste input. + lines[i] = line:gsub('\27%[[0-9;mK]+', '') + end + return overridden(lines, phase) + end + end)(vim.paste) +< + + Parameters: ~ + • {lines} (`string[]`) |readfile()|-style list of lines to paste. + |channel-lines| + • {phase} (`-1|1|2|3`) -1: "non-streaming" paste: the call contains all + lines. If paste is "streamed", `phase` indicates the stream + state: + • 1: starts the paste (exactly once) + • 2: continues the paste (zero or more times) + • 3: ends the paste (exactly once) + + Return: ~ + (`boolean`) result false if client should cancel the paste. + + See also: ~ + • |paste| + vim.pesc({s}) *vim.pesc()* Escapes magic chars in |lua-pattern|s. @@ -2018,6 +1822,23 @@ vim.pesc({s}) *vim.pesc()* See also: ~ • https://github.com/rxi/lume +vim.print({...}) *vim.print()* + "Pretty prints" the given arguments and returns them unmodified. + + Example: >lua + local hl_normal = vim.print(vim.api.nvim_get_hl(0, { name = 'Normal' })) +< + + Parameters: ~ + • {...} (`any`) + + Return: ~ + (`any`) given arguments. + + See also: ~ + • |vim.inspect()| + • |:=| + vim.ringbuf({size}) *vim.ringbuf()* Create a ring buffer limited to a maximal number of items. Once the buffer is full, adding a new entry overrides the oldest entry. >lua @@ -2048,6 +1869,29 @@ vim.ringbuf({size}) *vim.ringbuf()* Return: ~ (`vim.Ringbuf`) ringbuf +vim.schedule_wrap({fn}) *vim.schedule_wrap()* + Returns a function which calls {fn} via |vim.schedule()|. + + The returned function passes all arguments to {fn}. + + Example: >lua + function notify_readable(_err, readable) + vim.notify("readable? " .. tostring(readable)) + end + vim.uv.fs_access(vim.fn.stdpath("config"), "R", vim.schedule_wrap(notify_readable)) +< + + Parameters: ~ + • {fn} (`function`) + + Return: ~ + (`function`) + + See also: ~ + • |lua-loop-callbacks| + • |vim.schedule()| + • |vim.in_fast_event()| + vim.spairs({t}) *vim.spairs()* Enumerates key-value pairs of a table, ordered by key. @@ -2100,6 +1944,44 @@ vim.startswith({s}, {prefix}) *vim.startswith()* Return: ~ (`boolean`) `true` if `prefix` is a prefix of `s` + *vim.str_byteindex()* +vim.str_byteindex({s}, {encoding}, {index}, {strict_indexing}) + Convert UTF-32, UTF-16 or UTF-8 {index} to byte index. If + {strict_indexing} is false then an out of range index will return byte + length instead of throwing an error. + + Invalid UTF-8 and NUL is treated like in |vim.str_utfindex()|. An {index} + in the middle of a UTF-16 sequence is rounded upwards to the end of that + sequence. + + Parameters: ~ + • {s} (`string`) + • {encoding} (`"utf-8"|"utf-16"|"utf-32"`) + • {index} (`integer`) + • {strict_indexing} (`boolean?`) default: true + + Return: ~ + (`integer`) + + *vim.str_utfindex()* +vim.str_utfindex({s}, {encoding}, {index}, {strict_indexing}) + Convert byte index to UTF-32, UTF-16 or UTF-8 indices. If {index} is not + supplied, the length of the string is used. All indices are zero-based. + + If {strict_indexing} is false then an out of range index will return + string length instead of throwing an error. Invalid UTF-8 bytes, and + embedded surrogates are counted as one code point each. An {index} in the + middle of a UTF-8 sequence is rounded upwards to the end of that sequence. + + Parameters: ~ + • {s} (`string`) + • {encoding} (`"utf-8"|"utf-16"|"utf-32"`) + • {index} (`integer?`) + • {strict_indexing} (`boolean?`) default: true + + Return: ~ + (`integer`) + vim.tbl_contains({t}, {value}, {opts}) *vim.tbl_contains()* Checks if a table contains a given value, specified either directly or via a predicate that is checked for each value. @@ -2358,6 +2240,135 @@ vim.validate({name}, {value}, {validator}, {optional}, {message}) • `fun(name: string, val: any, validator: vim.validate.Validator, message: string)` • `fun(spec: table)` +vim.wait({time}, {callback}, {interval}, {fast_only}) *vim.wait()* + Waits up to `time` milliseconds, until `callback` returns `true` + (success). Executes `callback` immediately, then on user events, internal + events, and approximately every `interval` milliseconds (default 200). + Returns `true` plus any remaining callback results on success. + + Nvim processes other events while waiting. Cannot be called during an + |api-fast| event. + + Examples: >lua + -- Wait for 100 ms, allowing other events to process. + vim.wait(100) + + -- Wait up to 1000 ms or until `vim.g.foo` is true, at intervals of ~500 ms. + vim.wait(1000, function() return vim.g.foo end, 500) + + -- Wait indefinitely until `vim.g.foo` is true, and get the callback results. + local ok, rv1, rv2, rv3 = vim.wait(math.huge, function() + return vim.g.foo, 'a', 42, { ok = { 'yes' } } + end) + + -- Schedule a function to set a value in 100ms. This would wait 10s if blocked, but actually + -- only waits 100ms because `vim.wait` processes other events while waiting. + vim.defer_fn(function() vim.g.timer_result = true end, 100) + if vim.wait(10000, function() return vim.g.timer_result end) then + print('Only waiting a little bit of time!') + end + + -- Yield via vim.wait() to allow CTRL-C to interrupt Lua code. + while true do + -- ...work... + local _, code = vim.wait(0, nil, 0) + if code == -2 then -- CTRL-C. + break + end + end +< + + Parameters: ~ + • {time} (`number`) Number of milliseconds to wait. Must be + non-negative number, any fractional part is truncated. + • {callback} (`fun(): boolean, ...?`) Optional callback. Waits until + {callback} returns true + • {interval} (`integer?`) (Approximate) number of milliseconds to wait + between polls + • {fast_only} (`boolean?`) If true, only |api-fast| events will be + processed. + + Return (multiple): ~ + (`boolean`) + (`-1|-2?`) + • If callback returns `true` before timeout: `true, ...` (remaining + callback results). + • On timeout: `false, -1` + • On interrupt: `false, -2` + • On error: the error is raised. + + +============================================================================== +Lua module: vim.inspector *vim.inspector* + +vim.inspect_pos({buf}, {row}, {col}, {filter}) *vim.inspect_pos()* + Get all the items at a given buffer position. + + Can also be pretty-printed with `:Inspect!`. *:Inspect!* + + Attributes: ~ + Since: 0.9.0 + + Parameters: ~ + • {buf} (`integer?`) defaults to the current buffer + • {row} (`integer?`) row to inspect, 0-based. Defaults to the row of + the current cursor + • {col} (`integer?`) col to inspect, 0-based. Defaults to the col of + the current cursor + • {filter} (`table?`) Table with key-value pairs to filter the items + • {extmarks} (`boolean|"all"`, default: true) Include + extmarks. When `all`, then extmarks without a `hl_group` + will also be included. + • {semantic_tokens} (`boolean`, default: true) Include + semantic token highlights. + • {syntax} (`boolean`, default: `true`) Include syntax based + highlight groups. + • {treesitter} (`boolean`, default: `true`) Include + treesitter based highlight groups. + + Return: ~ + (`table`) a table with the following key-value pairs. Items are in + "traversal order": + • treesitter: a list of treesitter captures + • syntax: a list of syntax groups + • semantic_tokens: a list of semantic tokens + • extmarks: a list of extmarks + • buffer: the buffer used to get the items + • row: the row used to get the items + • col: the col used to get the items + +vim.show_pos({buf}, {row}, {col}, {filter}) *vim.show_pos()* + Show all the items at a given buffer position. + + Can also be shown with `:Inspect`. *:Inspect* + + See also |:marks| to list all extmarks. + + Example: To bind this function to the vim-scriptease inspired `zS` in + Normal mode: >lua + vim.keymap.set('n', 'zS', vim.show_pos) +< + + Attributes: ~ + Since: 0.9.0 + + Parameters: ~ + • {buf} (`integer?`) defaults to the current buffer + • {row} (`integer?`) row to inspect, 0-based. Defaults to the row of + the current cursor + • {col} (`integer?`) col to inspect, 0-based. Defaults to the col of + the current cursor + • {filter} (`table?`) A table with the following fields: + • {extmarks} (`boolean|"all"`, default: true) Include + extmarks. When `all`, then extmarks without a `hl_group` + will also be included. + • {semantic_tokens} (`boolean`, default: true) Include + semantic token highlights. + • {syntax} (`boolean`, default: `true`) Include syntax based + highlight groups. + • {treesitter} (`boolean`, default: `true`) Include + treesitter based highlight groups. + ============================================================================== Lua module: vim.base64 *vim.base64* @@ -3306,10 +3317,7 @@ Iter:find({f}) *Iter:find()* Since: 0.10.0 Parameters: ~ - • {f} (`(fun(v: V1, ...: V...): boolean)|any`) - - Overloads: ~ - • `fun(self: vim.IterArray, f: V1|fun(v: V1, ...: V...): boolean): V1?, V...` + • {f} (`V1|fun(v: V1, ...: V...): any`) Return (multiple): ~ (`V1?`) @@ -3584,10 +3592,6 @@ Iter:totable() *Iter:totable()* Attributes: ~ Since: 0.10.0 - Overloads: ~ - • `fun(self: vim.Iter): T[]` - • `fun(self: vim.Iter): [V1, V2, V...][]` - Return: ~ (`any[]`) @@ -5871,7 +5875,7 @@ Example: >lua end < -*vim.version()* returns the version of the current Nvim process. +|vim.version()| returns the version of the current Nvim process. VERSION RANGE SPEC *version-range* @@ -5948,6 +5952,12 @@ VersionRange:has({version}) *VersionRange:has()* See also: ~ • https://github.com/npm/node-semver#ranges +vim.version() *vim.version()* + Returns the current Nvim version. + + Return: ~ + (`vim.Version`) + vim.version.cmp({v1}, {v2}) *vim.version.cmp()* Parses and compares two version objects (the result of |vim.version.parse()|, or specified literally as a `{major, minor, patch}` diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index c7bbc958d1..a5fce60b72 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -1577,6 +1577,21 @@ This Lua |treesitter-query| interface allows you to create queries and use them to parse text. See |vim.treesitter.query.parse()| for a working example. +*TSQuery* + Extends: |userdata| + + Reference to an object held by the treesitter library that is used as a + component of the |vim.treesitter.Query| for language feature support. See + |treesitter-query| for more about queries or + |vim.treesitter.query.parse()| for an example of how to obtain a query + object. + + Fields: ~ + • {disable_capture} (`fun(self: TSQuery, capture_name: string)`) See + |TSQuery:disable_capture()|. + • {disable_pattern} (`fun(self: TSQuery, pattern_index: integer)`) See + |TSQuery:disable_pattern()|. + *vim.treesitter.Query* Parsed query, see |vim.treesitter.query.parse()| @@ -1871,25 +1886,6 @@ set({lang}, {query_name}, {text}) *vim.treesitter.query.set()* • {query_name} (`string`) Name of the query (e.g., "highlights") • {text} (`string`) Query text (unparsed). - - - -*TSQuery* - Extends: |userdata| - - Reference to an object held by the treesitter library that is used as a - component of the |vim.treesitter.Query| for language feature support. See - |treesitter-query| for more about queries or - |vim.treesitter.query.parse()| for an example of how to obtain a query - object. - - Fields: ~ - • {disable_capture} (`fun(self: TSQuery, capture_name: string)`) See - |TSQuery:disable_capture()|. - • {disable_pattern} (`fun(self: TSQuery, pattern_index: integer)`) See - |TSQuery:disable_pattern()|. - - TSQuery:disable_capture({capture_name}) *TSQuery:disable_capture()* Disable a specific capture in this query; once disabled the capture cannot be re-enabled. {capture_name} should not include a leading "@". diff --git a/runtime/lua/nvim/tutor.lua b/runtime/lua/nvim/tutor.lua index 8fdcbfe397..310c3c87c1 100644 --- a/runtime/lua/nvim/tutor.lua +++ b/runtime/lua/nvim/tutor.lua @@ -55,7 +55,7 @@ local function check_line(line) { details = true } )) :filter(function(extmark) - return not extmark[4].invalid + return not assert(extmark[4]).invalid end) :totable() diff --git a/runtime/lua/vim/_core/editor.lua b/runtime/lua/vim/_core/editor.lua index 9134d74775..215b47b9b8 100644 --- a/runtime/lua/vim/_core/editor.lua +++ b/runtime/lua/vim/_core/editor.lua @@ -243,22 +243,6 @@ function vim._os_proc_children(ppid) return children end ---- @nodoc ---- @class vim.inspect.Opts ---- @field depth? integer ---- @field newline? string ---- @field indent? string ---- @field process? fun(item: any, path: any[]): any - ---- Gets a human-readable representation of the given object. ---- ----@see |vim.print()| ----@see https://github.com/kikito/inspect.lua ----@see https://github.com/mpeterv/vinspect ----@return string ----@overload fun(x: any, opts?: vim.inspect.Opts): string -vim.inspect = vim.inspect - do local startpos --- @type integer[]? local tdots = 0 @@ -1119,7 +1103,7 @@ function vim._expand_pat(pat, env) local vars = vim.fn.getcompletion(prefix .. match_part, type) --- @type string[] insert_keys(vim .iter(vars) - :map(function(s) ---@param s string + :map(function(s) s = s:gsub('[()]+$', '') -- strip '(' and ')' for function completions return s:sub(#prefix + 1) -- strip the prefix, e.g., 'g:foo' => 'foo' end) diff --git a/runtime/lua/vim/_core/ex_cmd.lua b/runtime/lua/vim/_core/ex_cmd.lua index db4feaa3ae..9c65c688c0 100644 --- a/runtime/lua/vim/_core/ex_cmd.lua +++ b/runtime/lua/vim/_core/ex_cmd.lua @@ -189,7 +189,6 @@ function M.lsp_complete(_, line) local subcmd = split[2] return vim .iter(complete_args[subcmd]()) - --- @param n string :map(function(n) return vim.fn.escape(n, ' \t') end) diff --git a/runtime/lua/vim/_core/server.lua b/runtime/lua/vim/_core/server.lua index 6a92085014..408749c9e5 100644 --- a/runtime/lua/vim/_core/server.lua +++ b/runtime/lua/vim/_core/server.lua @@ -37,7 +37,7 @@ function M.serverlist(opts, addrs) if want_info then local self_pid = vim.fn.getpid() local self_active = vim.v.useractive - peers = vim.tbl_map(function(addr) ---@param addr vim.ServerInfo + peers = vim.tbl_map(function(addr) return { addr = addr, pid = self_pid, diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua index 1a1fc71c8d..d001bba000 100644 --- a/runtime/lua/vim/_core/ui2/messages.lua +++ b/runtime/lua/vim/_core/ui2/messages.lua @@ -329,6 +329,8 @@ function M.show_msg(tgt, kind, content, replace_last, append, id) or (api.nvim_buf_line_count(buf) - ((replace_last or cr or append) and 1 or 0)) local curline = (cr or append) and api.nvim_buf_get_lines(buf, row, row + 1, false)[1] col = mark[2] or (append and not cr and math.min(col, #curline) or 0) + --- EmmyLuaLs/emmylua-analyzer-rust#1269 + --- @type integer, integer, integer local start_row, start_col, lines = row, col, o.lines local function set_target_pos() diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua index e285cd5a12..6ae800e958 100644 --- a/runtime/lua/vim/_meta.lua +++ b/runtime/lua/vim/_meta.lua @@ -7,61 +7,6 @@ error('Cannot require a meta file') ---@type uv vim.uv = ... ---- LuaLS fallback surface for the richer iterator annotations in `vim.iter`. ---- EmmyLua reads the precise generics from `runtime/lua/vim/iter.lua`; LuaLS uses ---- these broader shapes for downstream type-checking. ---- @class vim.Iter ---- @field all fun(self: vim.Iter, pred: fun(...): boolean): boolean ---- @field any fun(self: vim.Iter, pred: fun(...): boolean): boolean ---- @field count fun(self: vim.Iter): integer ---- @field each fun(self: vim.Iter, f: fun(...)): nil ---- @field enumerate fun(self: vim.Iter): vim.Iter ---- @field filter fun(self: vim.Iter, f: fun(...): boolean): vim.Iter ---- @field find fun(self: vim.Iter, f: any): any ---- @field flatten fun(self: vim.Iter, depth?: integer): vim.IterArray ---- @field fold fun(self: vim.Iter, init: any, f: fun(acc: any, ...): any): any ---- @field join fun(self: vim.Iter, delim: string): string ---- @field last fun(self: vim.Iter): any ---- @field map fun(self: vim.Iter, f: fun(...): ...): vim.Iter ---- @field next fun(self: vim.Iter): any ---- @field nth fun(self: vim.Iter, n: integer): any ---- @field peek fun(self: vim.Iter): any ---- @field pop fun(self: vim.Iter): any ---- @field rev fun(self: vim.Iter): vim.IterArray ---- @field rfind fun(self: vim.Iter, f: any): any ---- @field rpeek fun(self: vim.Iter): any ---- @field rskip fun(self: vim.Iter, n: integer): vim.IterArray ---- @field skip fun(self: vim.Iter, n: integer|fun(...): boolean): vim.Iter ---- @field slice fun(self: vim.Iter, first: integer, last: integer): vim.IterArray ---- @field take fun(self: vim.Iter, n: integer|fun(...): boolean): vim.Iter ---- @field totable fun(self: vim.Iter): table ---- @field unique fun(self: vim.Iter, key?: fun(...): any): vim.Iter - ---- @class vim.IterArray : vim.Iter ---- @field count fun(self: vim.IterArray): integer ---- @field enumerate fun(self: vim.IterArray): vim.IterArray ---- @field filter fun(self: vim.IterArray, f: fun(...): boolean): vim.IterArray ---- @field find fun(self: vim.IterArray, f: any): any ---- @field flatten fun(self: vim.IterArray, depth?: integer): vim.IterArray ---- @field fold fun(self: vim.IterArray, init: any, f: fun(acc: any, ...): any): any ---- @field last fun(self: vim.IterArray): any ---- @field map fun(self: vim.IterArray, f: fun(...): ...): vim.IterArray ---- @field next fun(self: vim.IterArray): any ---- @field peek fun(self: vim.IterArray): any ---- @field pop fun(self: vim.IterArray): any ---- @field rev fun(self: vim.IterArray): vim.IterArray ---- @field rfind fun(self: vim.IterArray, f: any): any ---- @field rpeek fun(self: vim.IterArray): any ---- @field rskip fun(self: vim.IterArray, n: integer): vim.IterArray ---- @field skip fun(self: vim.IterArray, n: integer|fun(...): boolean): vim.IterArray ---- @field slice fun(self: vim.IterArray, first: integer, last: integer): vim.IterArray ---- @field take fun(self: vim.IterArray, n: integer|fun(...): boolean): vim.IterArray ---- @field totable fun(self: vim.IterArray): table ---- @field unique fun(self: vim.IterArray, key?: fun(...): any): vim.IterArray - ---- @class vim.IterModule ---- @operator call: fun(src: any, ...): vim.Iter - --- The following modules are loaded specially in _init_packages.lua vim.F = require('vim.F') @@ -75,11 +20,8 @@ vim.func = require('vim.func') vim.glob = require('vim.glob') vim.health = require('vim.health') vim.hl = require('vim.hl') -local iter = require('vim.iter') --- `require('vim.iter')` carries the richer EmmyLua generic surface. Force --- LuaLS onto the fallback module shape above so `make luals` stays clean. ----@cast iter vim.IterModule -vim.iter = iter +vim.inspect = require('vim.inspect') +vim.iter = require('vim.iter') vim.keymap = require('vim.keymap') vim.loader = require('vim.loader') vim.log = require('vim.log') diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua index d6d062be81..a78bba4888 100644 --- a/runtime/lua/vim/filetype/detect.lua +++ b/runtime/lua/vim/filetype/detect.lua @@ -1,3 +1,5 @@ +--- @diagnostic disable: annotation-usage-error + -- Contains filetype detection functions for use in filetype.lua that are either: -- * used more than once or -- * complex (e.g. check more than one line or use conditionals). diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua index a02dd730e8..150d8090e6 100644 --- a/runtime/lua/vim/health.lua +++ b/runtime/lua/vim/health.lua @@ -144,6 +144,8 @@ local function filepath_to_healthcheck(path) :find(function(dir) return vim.fs.relpath(dir, path) end) + -- The healthcheck path came from nvim_get_runtime_file(), so a runtime directory must match. + assert(rtp_lua) -- "/path/to/rtp/lua/foo/bar/health.lua" => "foo/bar/health.lua" -- "/another/rtp/lua/baz/health/init.lua" => "baz/health/init.lua" local subpath = path:gsub('^' .. vim.pesc(rtp_lua), ''):gsub('^/+', '') diff --git a/runtime/lua/vim/health/health.lua b/runtime/lua/vim/health/health.lua index 44c4dd39ae..eaab33cf34 100644 --- a/runtime/lua/vim/health/health.lua +++ b/runtime/lua/vim/health/health.lua @@ -762,9 +762,9 @@ local function check_sysinfo() vim.health.start('System Info') -- Use :version because `vim.version().build` returns "Homebrew" for brew installs. - local version_out = vim.api.nvim_exec2('version', { output = true }).output + local version_out = vim.api.nvim_exec2('version', { output = true }).output --[[@as string]] local nvim_version = version_out:match('NVIM (v[^\n]+)') or 'unknown' - local commit --[[@type string]] = (version_out:match('%+g(%x+)') or ''):sub(1, 12) + local commit = (version_out:match('%+g(%x+)') or ''):sub(1, 12) if vim.fn.executable('git') ~= 1 then vim.health.warn('Cannot check for updates: git not found') diff --git a/runtime/lua/vim/hl.lua b/runtime/lua/vim/hl.lua index 304c73a4b2..54c167c765 100644 --- a/runtime/lua/vim/hl.lua +++ b/runtime/lua/vim/hl.lua @@ -156,7 +156,6 @@ function M.range(buf, ns, hlgroup, start, finish, opts) end end ----@private ---@class (private) vim.hl.OnEventState ---@field timer? uv.uv_timer_t Timer to clear the highlight. ---@field clear? fun() Function to clear the highlight immediately. diff --git a/runtime/lua/vim/inspect.lua b/runtime/lua/vim/inspect.lua index 1c1dbf4ebe..ed4bc798dc 100644 --- a/runtime/lua/vim/inspect.lua +++ b/runtime/lua/vim/inspect.lua @@ -1,4 +1,5 @@ ---- @diagnostic disable: no-unknown +--- @class (private) vim.InspectModule +--- @overload fun(root: any, options?: vim.inspect.Opts): string local inspect = { _VERSION = 'inspect.lua 3.1.0', _URL = 'http://github.com/kikito/inspect.lua', @@ -311,6 +312,7 @@ local function tabify(inspector) puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level)) end +--- @nodoc --- @param v any function Inspector:getId(v) local id = self.ids[v] @@ -323,6 +325,7 @@ function Inspector:getId(v) return tostring(id) end +--- @nodoc --- @param v any function Inspector:putValue(v) local buf = self.buf @@ -407,8 +410,31 @@ function Inspector:putValue(v) end end +--- @inlinedoc +--- @class vim.inspect.Opts +--- +--- Maximum table nesting depth. Deeper tables are shown as `{...}`. +--- (default: unlimited) +--- @field depth? integer +--- +--- String used for line breaks. +--- (default: `"\n"`) +--- @field newline? string +--- +--- String repeated for each level of indentation. +--- (default: `" "`) +--- @field indent? string +--- +--- Transforms values, keys, and metatables before formatting. Return a replacement value, +--- or `nil` to omit a table entry. {path} lists the keys leading to {item}, with +--- `vim.inspect.KEY` appended for keys or `vim.inspect.METATABLE` for metatables. +--- The root has an empty path. +--- @field process? fun(item: any, path: any[]): any + +--- @nodoc --- @param root any --- @param options? vim.inspect.Opts +--- @return string function inspect.inspect(root, options) options = options or {} @@ -440,6 +466,14 @@ function inspect.inspect(root, options) end setmetatable(inspect, { + --- Gets a human-readable representation of the given object. + --- + --- @see |vim.print()| + --- @see https://github.com/kikito/inspect.lua + --- @see https://github.com/mpeterv/vinspect + --- @param root any + --- @param options? vim.inspect.Opts + --- @return string __call = function(_, root, options) return inspect.inspect(root, options) end, diff --git a/runtime/lua/vim/iter.lua b/runtime/lua/vim/iter.lua index 056a3fb438..40a4db815d 100644 --- a/runtime/lua/vim/iter.lua +++ b/runtime/lua/vim/iter.lua @@ -71,7 +71,7 @@ --- @overload fun(src: T[]): vim.IterArray --- @overload fun(src: table): vim.Iter --- @overload fun(src: table, ...): vim.Iter ---- @overload fun(src: function, ...): vim.Iter +--- @overload fun(src: (fun(state: S, control: C): R1, R...), state?: S, control?: C): vim.Iter local M = {} --- @nodoc @@ -440,7 +440,7 @@ function Iter:each(f) end end ---- @private +--- @nodoc --- @param f fun(v: V1, ...: V...) function IterArray:each(f) local inc = self._head < self._tail and 1 or -1 @@ -475,8 +475,6 @@ end --- --- --- @since 12 ---- @overload fun(self: vim.Iter): T[] ---- @overload fun(self: vim.Iter): [V1, V2, V...][] --- @return any[] function Iter:totable() local t = {} @@ -493,8 +491,6 @@ function Iter:totable() end --- @nodoc ---- @overload fun(self: vim.IterArray): T[] ---- @overload fun(self: vim.IterArray): [V1, V2, V...][] --- @return any[] function IterArray:totable() if self.next ~= IterArray.next or self._head >= self._tail then @@ -627,7 +623,7 @@ function Iter:next() return self._next() end ---- @package +--- @nodoc --- @return V1?, V... function IterArray:next() if self._head ~= self._tail then @@ -693,7 +689,7 @@ function Iter:peek() return unpack(self._peeked) end ---- @private +--- @nodoc --- @return V1?, V... function IterArray:peek() if self._head ~= self._tail then @@ -723,8 +719,7 @@ end --- --- ``` --- @since 12 ---- @param f (fun(v: V1, ...: V...): boolean)|any ---- @overload fun(self: vim.IterArray, f: V1|fun(v: V1, ...: V...): boolean): V1?, V... +--- @param f V1|fun(v: V1, ...: V...): any --- @return V1?, V... function Iter:find(f) if type(f) ~= 'function' then @@ -862,7 +857,7 @@ function Iter:take(n) return self end ---- @private +--- @nodoc --- @param n integer|fun(v: V1, ...: V...): boolean --- @return vim.IterArray function IterArray:take(n) @@ -997,7 +992,7 @@ function Iter:skip(n) return self end ---- @private +--- @nodoc --- @param n integer|fun(v: V1, ...: V...): boolean --- @return vim.IterArray function IterArray:skip(n) @@ -1185,7 +1180,7 @@ function Iter:last() return last end ---- @private +--- @nodoc --- @return V1?, V... function IterArray:last() if self._head == self._tail then @@ -1236,7 +1231,7 @@ function Iter:enumerate() end) end ---- @private +--- @nodoc --- @return vim.IterArray function IterArray:enumerate() local inc = self._head < self._tail and 1 or -1 diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index cdccf43190..6122e72ea0 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -7,7 +7,6 @@ local lsp = vim._defer_require('vim.lsp', { _changetracking = ..., --- @module 'vim.lsp._changetracking' _folding_range = ..., --- @module 'vim.lsp._folding_range' _snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar' - _tagfunc = ..., --- @module 'vim.lsp._tagfunc' _watchfiles = ..., --- @module 'vim.lsp._watchfiles' buf = ..., --- @module 'vim.lsp.buf' client = ..., --- @module 'vim.lsp.client' @@ -24,6 +23,9 @@ local lsp = vim._defer_require('vim.lsp', { protocol = ..., --- @module 'vim.lsp.protocol' rpc = ..., --- @module 'vim.lsp.rpc' semantic_tokens = ..., --- @module 'vim.lsp.semantic_tokens' + -- Work around EmmyLuaLs/emmylua-analyzer-rust#1238 for function exports. + --- @type fun(pattern: string, flags: string): table[]|vim.NIL + tagfunc = ..., --- @module 'vim.lsp.tagfunc' util = ..., --- @module 'vim.lsp.util' }) @@ -396,19 +398,17 @@ lsp.config = setmetatable({ _configs = {} }, { local function get_config_names() local config_names = vim .iter(api.nvim_get_runtime_file('lsp/*.lua', true)) - --- @param path string :map(function(path) - local file_name = path:match('[^/]*.lua$') + local file_name = assert(path:match('[^/]*.lua$')) return file_name:sub(0, #file_name - 4) end) - :totable() + :totable() --[[@as string[] ]] vim.list_extend(config_names, vim.tbl_keys(lsp.config._configs)) return vim .iter(config_names) :unique() - --- @param name string :filter(function(name) return name ~= '*' end) @@ -1443,23 +1443,6 @@ function lsp.formatexpr(opts) return 0 end ---- Provides an interface between the built-in client and 'tagfunc'. ---- ---- When used with normal mode commands (e.g. |CTRL-]|) this will invoke ---- the "textDocument/definition" LSP method to find the tag under the cursor. ---- Otherwise, uses "workspace/symbol". If no results are returned from ---- any LSP servers, falls back to using built-in tags. ---- ----@param pattern string Pattern used to find a workspace symbol ----@param flags string See |tag-function| ---- ----@return table[]|vim.NIL tags A list of matching tags, or `vim.NIL` to use the built-in tags. -function lsp.tagfunc(pattern, flags) - -- EmmyLua incorrectly treats function exports referenced by @module as non-callable. - --- @diagnostic disable-next-line:call-non-callable EmmyLuaLs/emmylua-analyzer-rust#1238 - return vim.lsp._tagfunc(pattern, flags) -end - --- Provides an interface between the built-in client and a `foldexpr` function. --- --- To use, set 'foldmethod' to "expr" and set the value of 'foldexpr': diff --git a/runtime/lua/vim/lsp/_capability.lua b/runtime/lua/vim/lsp/_capability.lua index 7fcb59fcf8..0f6a12c77d 100644 --- a/runtime/lua/vim/lsp/_capability.lua +++ b/runtime/lua/vim/lsp/_capability.lua @@ -92,7 +92,7 @@ function M:destroy() api.nvim_del_augroup_by_id(self.augroup) self.active[self.bufnr] = nil - buf_capabilities[self.bufnr] = vim.tbl_filter(function(cap) ---@param cap vim.lsp.Capability + buf_capabilities[self.bufnr] = vim.tbl_filter(function(cap) return cap ~= self end, buf_capabilities[self.bufnr]) diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index 5f06cca62e..ad97332722 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -434,6 +434,7 @@ end --- |lsp-handler| for the method `workspace/codeLens/refresh` --- ---@internal +---@diagnostic disable-next-line: annotation-usage-error ---@type lsp.Handler function M.on_refresh(err, _, ctx) if err then diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/tagfunc.lua similarity index 85% rename from runtime/lua/vim/lsp/_tagfunc.lua rename to runtime/lua/vim/lsp/tagfunc.lua index 18196a70a3..35c1031b41 100644 --- a/runtime/lua/vim/lsp/_tagfunc.lua +++ b/runtime/lua/vim/lsp/tagfunc.lua @@ -86,9 +86,17 @@ local function query_workspace_symbols(pattern) return results end ----@param pattern string ----@param flags string ----@return table[]|vim.NIL +--- Provides an interface between the built-in client and 'tagfunc'. +--- +--- When used with normal mode commands (e.g. |CTRL-]|) this will invoke +--- the "textDocument/definition" LSP method to find the tag under the cursor. +--- Otherwise, uses "workspace/symbol". If no results are returned from +--- any LSP servers, falls back to using built-in tags. +--- +---@param pattern string Pattern used to find a workspace symbol +---@param flags string See |tag-function| +--- +---@return table[]|vim.NIL tags A list of matching tags, or `vim.NIL` to use the built-in tags. local function tagfunc(pattern, flags) -- avoid definition/symbol queries for insert completion if string.match(flags, 'i') then diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index ee0cf11c83..da47043579 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -890,7 +890,7 @@ local function install_list(plug_list, confirm) if not confirm or confirm_install(plug_list) then trigger_events(plug_list, 'PackChangedPre', 'install') run_list(plug_list, do_install, 'Installing plugins') - local installed = vim.tbl_filter(function(p) --- @param p vim.pack.Plug + local installed = vim.tbl_filter(function(p) return p.info.installed == true end, plug_list) trigger_events(installed, 'PackChanged', 'install') @@ -945,7 +945,7 @@ local function infer_update_details(p) local any_version = vim.version.range('*') --[[@as vim.VersionRange]] local last_version = get_last_semver_tag(past_tags, any_version) - local newer_semver_tags = vim.tbl_filter(function(x) --- @param x string + local newer_semver_tags = vim.tbl_filter(function(x) return vim.version.gt(x, last_version) end, all_semver_tags) @@ -1400,7 +1400,7 @@ local function update_list(plug_list) end run_list(plug_list, do_update, 'Applying updates') - local updated = vim.tbl_filter(function(p) --- @param p vim.pack.Plug + local updated = vim.tbl_filter(function(p) return p.info.updated == true end, plug_list) trigger_events(updated, 'PackChanged', 'update') @@ -1503,7 +1503,7 @@ function M.update(names, opts) -- Update and show report if opts.force then - local plugs_to_update = vim.tbl_filter(function(p) --- @param p vim.pack.Plug + local plugs_to_update = vim.tbl_filter(function(p) return p.info.sha_head ~= p.info.sha_target end, plug_list) update_list(plugs_to_update) diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 5589e807f4..ab98067337 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -12,7 +12,6 @@ local M = {} ---@field named vim.treesitter.dev.Node[] local TSTreeView = {} ----@private ---@class (private) vim.treesitter.dev.TSTreeViewOpts ---@field anon boolean If true, display anonymous nodes. ---@field lang boolean If true, display the language alongside each node. diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index 1c7c513ca4..6d98b08dc4 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -347,30 +347,26 @@ do --- Initialize Progress handlers. local function progress_init() progress_group = vim.api.nvim_create_augroup('nvim.ui.progress_status', { clear = true }) - progress_autocmd = require('vim._core.util').nvim_on( - 'Progress', - progress_group, - { - desc = 'Tracks progress messages for vim.ui.progress_status()', - }, ---@param ev {data: vim.event.progress.data} - function(ev) - if not ev.data or not ev.data.id then - return - end - ev.data.percent = ev.data.percent or 0 - progress[ev.data.id] = ev.data - - -- Clear finished items - if - ev.data.status == 'success' - or ev.data.percent == 100 - or ev.data.status == 'failed' - or ev.data.status == 'cancel' - then - progress[ev.data.id] = nil - end + progress_autocmd = require('vim._core.util').nvim_on('Progress', progress_group, { + desc = 'Tracks progress messages for vim.ui.progress_status()', + }, function(ev) + if not ev.data or not ev.data.id then + return end - ) + --- @cast ev.data vim.event.progress.data + ev.data.percent = ev.data.percent or 0 + progress[ev.data.id] = ev.data + + -- Clear finished items + if + ev.data.status == 'success' + or ev.data.percent == 100 + or ev.data.status == 'failed' + or ev.data.status == 'cancel' + then + progress[ev.data.id] = nil + end + end) end --- Gets a status description summarizing currently running progress messages. diff --git a/runtime/lua/vim/version.lua b/runtime/lua/vim/version.lua index c380f5d6e0..0556a79dc6 100644 --- a/runtime/lua/vim/version.lua +++ b/runtime/lua/vim/version.lua @@ -12,7 +12,7 @@ --- end --- ``` --- ---- [vim.version()]() returns the version of the current Nvim process. +--- [vim.version()] returns the version of the current Nvim process. --- --- VERSION RANGE SPEC [version-range]() --- diff --git a/src/gen/gen_vimdoc.lua b/src/gen/gen_vimdoc.lua index 58c3fc687a..b17c70c470 100755 --- a/src/gen/gen_vimdoc.lua +++ b/src/gen/gen_vimdoc.lua @@ -38,7 +38,9 @@ local INDENTATION = 4 --- Generated documentation target, e.g. api.txt --- @field filename string --- ---- @field section_order string[] +--- Ordered filenames or groups of filenames to merge into one section. +--- For a group, the first filename determines the section name and help tag. +--- @field section_order (string|string[])[] --- --- List of files/directories for doxygen to read, relative to `base_dir`. --- @field files string[] @@ -59,11 +61,6 @@ local INDENTATION = 4 --- --- Per-function helptag. --- @field fn_helptag_fmt? fun(fun: nvim.gen_vimdoc.HelptagTarget): string ---- ---- @field append_only? string[] ---- ---- Merge parsed files into the first section instead of rendering one section per file. ---- @field merge_files? boolean ---@alias nvim.gen_vimdoc.HelptagTarget ---| nvim.luacats.parser.fun @@ -179,9 +176,8 @@ local config = { -- Sections at the top, in a specific order: 'builtin.lua', 'options.lua', - 'editor.lua', + { 'editor.lua', 'inspect.lua', 'shared.lua' }, '_inspector.lua', - 'shared.lua', -- Sections in alphanumeric order: 'base64.lua', @@ -233,6 +229,7 @@ local config = { 'runtime/lua/vim/fs.lua', 'runtime/lua/vim/glob.lua', 'runtime/lua/vim/hl.lua', + 'runtime/lua/vim/inspect.lua', 'runtime/lua/vim/iter.lua', 'runtime/lua/vim/keymap.lua', 'runtime/lua/vim/loader.lua', @@ -253,7 +250,7 @@ local config = { fun.module = 'vim' end - if fun.module == 'vim' and contains(fun.name, { 'cmd', 'inspect' }) then + if fun.module == 'vim' and fun.name == 'cmd' then fun.table = nil end @@ -312,15 +309,12 @@ local config = { return fn_helptag_fmt_common(fun) end, - append_only = { - 'shared.lua', - }, }, lsp = { filename = 'lsp.txt', section_order = { -- Sections at the top, in a specific order: - 'lsp.lua', + { 'lsp.lua', 'tagfunc.lua' }, -- Sections in alphanumeric order: 'buf.lua', @@ -338,7 +332,6 @@ local config = { 'on_type_formatting.lua', 'rpc.lua', 'semantic_tokens.lua', - 'tagfunc.lua', -- Sections at the end, in a specific order: 'util.lua', @@ -373,9 +366,8 @@ local config = { async = { filename = 'lua-async.txt', section_order = { - 'async.lua', + { 'async.lua', '_core.lua', '_semaphore.lua' }, }, - merge_files = true, files = { 'runtime/lua/vim/async.lua', 'runtime/lua/vim/async/_core.lua', @@ -388,12 +380,6 @@ local config = { return { 'lua-async', 'vim.async' } end, fn_xform = function(fun) - if fun.module == 'vim.async._core' or fun.module == 'vim.async._semaphore' then - fun.module = 'vim.async' - end - if fun.name == 'new_semaphore' then - fun.name = 'semaphore' - end if fun.classvar == 'M' then fun.classvar = nil end @@ -425,10 +411,8 @@ local config = { 'highlighter.lua', 'language.lua', 'languagetree.lua', - 'query.lua', - 'tsquery.lua', + { 'query.lua', 'tsquery.lua' }, }, - append_only = { 'tsquery.lua' }, files = { 'runtime/lua/vim/treesitter/_meta/', 'runtime/lua/vim/treesitter.lua', @@ -1171,21 +1155,16 @@ local function make_section(filename, cfg, briefs, funs_txt, classes_txt) end --- @param section nvim.gen_vimdoc.Section ---- @param add_header? boolean -local function render_section(section, add_header) +local function render_section(section) local doc = {} --- @type string[] if not section.title then error(('section.title is nil, check section_fmt(). section: %s'):format(vim.inspect(section))) end - if add_header ~= false then - vim.list_extend(doc, { - string.rep('=', TEXT_WIDTH), - '\n', - align_tags(TEXT_WIDTH)(section.title .. ' ' .. section.help_tag), - }) - end + doc[#doc + 1] = string.rep('=', TEXT_WIDTH) + .. '\n' + .. align_tags(TEXT_WIDTH)(section.title .. ' ' .. section.help_tag) if next(section.briefs) then local briefs_txt = {} --- @type string[] @@ -1193,23 +1172,21 @@ local function render_section(section, add_header) briefs_txt[#briefs_txt + 1] = md_to_vimdoc(b, 0, 0, TEXT_WIDTH) end - local sdoc = '\n\n' .. table.concat(briefs_txt, '\n') + local sdoc = table.concat(briefs_txt, '\n') if sdoc:find('[^%s]') then doc[#doc + 1] = sdoc end end if section.classes_txt ~= '' then - table.insert(doc, '\n\n') table.insert(doc, (section.classes_txt:gsub('\n+$', '\n'))) end if section.funs_txt ~= '' then - table.insert(doc, '\n\n') table.insert(doc, section.funs_txt) end - return table.concat(doc) + return table.concat(doc, '\n\n') end local parsers = { @@ -1243,11 +1220,95 @@ local function find_module_class(classes, modvar) end end +--- Add callable exports and inherited module functions under their public names. +--- E.g. `M.semaphore = require('vim.async._semaphore')` in vim.async exposes +--- that module's returned `new_semaphore` function as `vim.async.semaphore()`. +--- A callable table uses its module name: vim.inspect's `__call` => `vim.inspect()`. +--- @param modules table +local function expand_module_docs(modules) + local exports = {} --- @type table + for _, module in pairs(modules) do + local documented = {} --- @type table + for _, fun in ipairs(module.funs) do + if fun.modvar == module.modvar and (not fun.classvar or fun.classvar == module.modvar) then + documented[fun.name] = true + end + end + for name, imported in pairs(module.imports) do + local public_name = module.name .. '.' .. name + if not documented[name] and not ('.' .. public_name):find('%._') then + exports[imported] = exports[imported] or {} + table.insert(exports[imported], public_name) + end + end + end + + for name, module in pairs(modules) do + -- E.g. new_semaphore becomes vim.async.semaphore through a require() binding. + -- Otherwise, use the public module path (vim.inspect for its __call). + if module.callable and (exports[module] or not ('.' .. name):find('%._')) then + for _, public_name in ipairs(exports[module] or { name }) do + local fun = vim.deepcopy(module.callable) + fun.module, fun.name = public_name:match('^(.*)%.([^.]+)$') + fun.name = fun.name or public_name + module.funs[#module.funs + 1] = fun + end + end + end + + -- `@class vim.async: vim.async._core` makes `vim.async._core.run()` => `vim.async.run()`. + -- Copy inherited functions under the public module name, keeping nearer declarations. + local hidden = {} --- @type table + for _, module in pairs(modules) do + if module.class and not ('.' .. module.name):find('%._') then + local names = {} --- @type table + local seen = {} --- @type table + local source = module + while source and not seen[source] do + seen[source] = true + for _, fun in ipairs(source.funs) do + if fun.class == source.class.name and fun.member_sep == '.' then + if + source ~= module + and not names[fun.name] + and not fun.nodoc + and not fun.access + and fun.name:sub(1, 1) ~= '_' + then + local inherited = vim.deepcopy(fun) + inherited.module, inherited.class = module.name, module.class.name + inherited.modvar, inherited.classvar = module.modvar, module.modvar + module.funs[#module.funs + 1] = inherited + end + if source ~= module and ('.' .. source.name):find('%._') then + hidden[fun] = true + end + -- Include functions already inherited by the source module. + names[fun.name] = true + end + end + -- A nearer declaration shadows ancestors, including non-function fields. + for _, field in ipairs(source.class.fields) do + names[field.name] = true + end + for name in pairs(source.assignments) do + names[name] = true + end + source = source.parent + end + end + end + -- Delay hiding until all modules have inherited the original documentation. + for fun in pairs(hidden) do + fun.nodoc = true + end +end + --- @param cfg nvim.gen_vimdoc.Config local function gen_target(cfg) cfg.fn_helptag_fmt = cfg.fn_helptag_fmt or fn_helptag_fmt_common print('Target:', cfg.filename) - local sections = {} --- @type table + local sections = {} --- @type nvim.gen_vimdoc.Section[] expand_files(cfg.files) @@ -1257,20 +1318,28 @@ local function gen_target(cfg) --- @type table local all_classes = {} - --- First pass so we can collect all classes + --- @type table + local modules = {} + + --- First pass so we can collect all classes and module exports. for _, f in vim.spairs(cfg.files) do local ext = f:match('%.([^.]+)$') local parser = parsers[ext] if parser then - local classes, funs, briefs = parser(f) + local classes, funs, briefs, _, module = parser(f) file_results[f] = { classes, funs, briefs } all_classes = vim.tbl_extend('error', all_classes, classes) + if module then + modules[module.name] = module + end end end - local merged_classes = {} --- @type table - local merged_funs = {} --- @type nvim.luacats.parser.fun[] - local merged_briefs = {} --- @type string[] + luacats_parser.resolve_modules(modules) + expand_module_docs(modules) + + --- @type table, nvim.luacats.parser.fun[], string[]]> + local files_by_name = {} for f, r in vim.spairs(file_results) do local classes, funs, briefs = r[1], r[2], r[3] @@ -1291,43 +1360,37 @@ local function gen_target(cfg) print(' Processing file:', f) - if cfg.merge_files then - merged_classes = vim.tbl_extend('error', merged_classes, classes) - vim.list_extend(merged_funs, funs) - vim.list_extend(merged_briefs, briefs) - else - -- FIXME: Using f_base will confuse `_meta/protocol.lua` with `protocol.lua` - local f_base = vim.fs.basename(f) - sections[f_base] = make_section( - f_base, - cfg, - render_briefs(briefs, cfg), - render_funs(funs, all_classes, cfg), - render_classes(classes, funs, cfg) - ) - end + -- FIXME: Using basenames will confuse `_meta/protocol.lua` with `protocol.lua` + files_by_name[vim.fs.basename(f)] = r end - if cfg.merge_files then - local section_file = cfg.section_order[1] - sections[section_file] = make_section( - section_file, + for _, entry in ipairs(cfg.section_order) do + local files = type(entry) == 'table' and entry or { entry } + local classes = {} --- @type table + local funs = {} --- @type nvim.luacats.parser.fun[] + local briefs = {} --- @type string[] + for _, f in ipairs(files) do + local r = files_by_name[f] + if r then + classes = vim.tbl_extend('error', classes, r[1]) + vim.list_extend(funs, r[2]) + vim.list_extend(briefs, r[3]) + end + end + sections[#sections + 1] = make_section( + files[1], cfg, - render_briefs(merged_briefs, cfg), - render_funs(merged_funs, all_classes, cfg), - render_classes(merged_classes, merged_funs, cfg) + render_briefs(briefs, cfg), + render_funs(funs, all_classes, cfg), + render_classes(classes, funs, cfg) ) end - local first_section_tag = sections[cfg.section_order[1]].help_tag + local first_section_tag = sections[1].help_tag local docs = {} --- @type string[] - for _, f in ipairs(cfg.section_order) do - local section = sections[f] - if section then - print(fmt(" Rendering section: '%s'", section.title)) - local add_sep_and_header = not vim.tbl_contains(cfg.append_only or {}, f) - docs[#docs + 1] = render_section(section, add_sep_and_header) - end + for _, section in ipairs(sections) do + print(fmt(" Rendering section: '%s'", section.title)) + docs[#docs + 1] = render_section(section) end table.insert( diff --git a/src/gen/luacats_parser.lua b/src/gen/luacats_parser.lua index 1ec4a512f6..9cee7e6602 100644 --- a/src/gen/luacats_parser.lua +++ b/src/gen/luacats_parser.lua @@ -58,6 +58,17 @@ local luacats_grammar = require('gen.luacats_grammar') * vim.lpeg.Cp() --- @field last_doc_item? nvim.luacats.parser.param|nvim.luacats.parser.return|nvim.luacats.parser.note --- @field last_doc_item_indent? integer +--- @class nvim.luacats.parser.module +--- @field name string Module name inferred from the path, used to resolve require(). +--- @field modvar? string +--- @field class? nvim.luacats.parser.class Class of the returned module table. +--- @field requires table Module fields assigned directly from require(), keyed by field name. +--- @field imports table Bindings linked by resolve_modules(). +--- @field parent? nvim.luacats.parser.module Module whose returned class is this module class's parent. +--- @field callable? nvim.luacats.parser.fun Returned function or the returned table's __call. +--- @field assignments table Field assignments (`M.name = ...`), including unannotated ones. +--- @field funs nvim.luacats.parser.fun[] + --- @alias nvim.luacats.parser.obj --- | nvim.luacats.parser.class --- | nvim.luacats.parser.fun @@ -409,20 +420,9 @@ end local function determine_modvar(str) local modvar --- @type string? for line in vim.gsplit(str, '\n') do - do - --- @type string? - local m = line:match('^return%s+([a-zA-Z_]+)') - if m then - modvar = m - end - end - do - --- @type string? - local m = line:match('^return%s+setmetatable%(([a-zA-Z_]+),') - if m then - modvar = m - end - end + modvar = line:match('^return%s+setmetatable%s*%(%s*([%w_]+)%s*,') + or line:match('^return%s+([%w_]+)') + or modvar end return modvar end @@ -495,6 +495,16 @@ function M.parse_str(str, filename) local module = filename:match('.*/lua/([a-z_][a-z0-9_/]+)%.lua') or filename module = module:gsub('/', '.') + --- @type nvim.luacats.parser.module + local module_info = { + name = module, + modvar = mod_return, + requires = {}, + imports = {}, + assignments = {}, + funs = funs, + } + local classvars = {} --- @type table local state = {} --- @type nvim.luacats.parser.State @@ -503,12 +513,20 @@ function M.parse_str(str, filename) local uncommitted = {} --- @type nvim.luacats.parser.obj[] local comments = {} + local in_module_metatable = false for line in vim.gsplit(str, '\n') do local has_indent = line:match('^%s+') ~= nil line = vim.trim(line) if vim.startswith(line, '---') then comments[#comments + 1] = use_type_alt(line:sub(4):gsub('^%s+@', '@')) else + -- Recognize the returned module's inline metatable at file scope. + if not has_indent and line ~= '' and not vim.startswith(line, '--') then + local target = line:match('^setmetatable%s*%(%s*([%w_]+)%s*,%s*{') + or line:match('^return%s+setmetatable%s*%(%s*([%w_]+)%s*,%s*{') + in_module_metatable = target ~= nil and target == mod_return + end + if #comments > 0 then local comment = table.concat(comments, '\n') .. '\n' local pos = 1 @@ -523,12 +541,36 @@ function M.parse_str(str, filename) end add_doc_lines_to_obj(state) + if not has_indent then + local parent, name, value = line:match('^([%w_]+)%.([%w_]+)%s*=%s*(.*)$') + if parent == mod_return and name then + module_info.assignments[name] = true + local _, required, tail = value:match([[^require%s*%(%s*(['"])([^'"]+)%1%s*%)%s*(.*)$]]) + module_info.requires[name] = required and (tail == '' or tail:match('^%-%-')) and required + or nil + end + end + if state.cur_obj then state.cur_obj.modvar = mod_return state.cur_obj.module = module end - process_lua_line(line, state, classes, classvars, has_indent) + -- Keep the callable declaration separate from the table's member functions. + local returned = line:match('^local%s+function%s+([%w_]+)%s*%(') + local callable_name = returned + if in_module_metatable and line:match('^__call%s*=%s*function%s*%(') then + in_module_metatable = false + returned = mod_return + callable_name = '__call' + end + if returned and returned == mod_return and state.cur_obj then + state.cur_obj.name = callable_name + module_info.callable = state.cur_obj + state.cur_obj = nil + else + process_lua_line(line, state, classes, classvars, has_indent) + end -- Commit the object local cur_obj = state.cur_obj @@ -543,9 +585,51 @@ function M.parse_str(str, filename) end end + if mod_return then + module_info.class = classes[classvars[mod_return]] + end + -- dump_uncommitted(filename, uncommitted) - return classes, funs, briefs, uncommitted + return classes, funs, briefs, uncommitted, module_info +end + +--- Connect separately parsed modules by filling their `imports` and `parent` fields. +--- Call after parsing all input files, passing their module records keyed by module name. +--- +--- For example, the Lua source file pkg/child.lua contains: +--- ```lua +--- --- @class Child: Base +--- local M = {} +--- M.factory = require('pkg.factory') +--- return M +--- ``` +--- The caller stores the parsed record for this file in `modules['pkg.child']`. +--- That record describes M, the table returned by the source file: +--- - `requires.factory` contains the string 'pkg.factory'. This function fills +--- `imports.factory` with `modules['pkg.factory']`, the parsed record for that file. +--- - `class.parent` contains the string 'Base'. This function fills `parent` with +--- the parsed record of the module whose returned table is annotated `@class Base`. +--- +--- Each link points to the original parsed module; a missing target leaves it nil. +--- The string names in `requires` and `class.parent` are retained. No declarations +--- are copied, renamed or hidden. +--- @param modules table Parsed modules keyed by require path. +function M.resolve_modules(modules) + local by_class = {} --- @type table + for _, module in pairs(modules) do + if module.class then + by_class[module.class.name] = module + end + end + + for _, module in pairs(modules) do + module.imports = {} + for name, required in pairs(module.requires) do + module.imports[name] = modules[required] + end + module.parent = module.class and by_class[module.class.parent] or nil + end end --- @param filename string diff --git a/test/functional/script/luacats_parser_spec.lua b/test/functional/script/luacats_parser_spec.lua index dd2addc299..e384414452 100644 --- a/test/functional/script/luacats_parser_spec.lua +++ b/test/functional/script/luacats_parser_spec.lua @@ -143,6 +143,100 @@ describe('luacats parser', function() eq({ { type = 'boolean', desc = 'Whether the value is nil.' } }, funs[1].returns) end) + it('links require bindings without renaming declarations', function() + local _, _, _, _, consumer = parser.parse_str( + dedent([[ + local M = {} + --- A binding with its own documentation. + M.create = require('vim.factory') + M.missing = require('vim.missing') + return M + ]]), + 'runtime/lua/vim/_consumer.lua' + ) + local _, funs, _, _, implementation = parser.parse_str( + dedent([[ + --- Create a value. + --- @param value string + --- @return string + local function new_value(value) end + return new_value + ]]), + 'runtime/lua/vim/factory.lua' + ) + parser.resolve_modules({ ['vim._consumer'] = consumer, ['vim.factory'] = implementation }) + + -- Check table identity: the import must reference the original parsed module. + eq(true, implementation == consumer.imports.create) + eq('vim.missing', consumer.requires.missing) + eq(nil, consumer.imports.missing) + eq({}, funs) + eq('new_value', implementation.callable.name) + eq('vim.factory', implementation.callable.module) + eq('Create a value.', implementation.callable.desc) + eq({ { name = 'value', type = 'string' } }, implementation.callable.params) + eq({ { type = 'string' } }, implementation.callable.returns) + end) + + it('keeps a callable module declaration separate from its members', function() + local _, funs, _, _, module = parser.parse_str( + dedent([[ + local M = {} + setmetatable(M, { + --- Format a value. + --- @param value any + --- @return string + __call = function(_, value) end, + }) + return M + ]]), + 'runtime/lua/vim/example.lua' + ) + eq({}, funs) + eq('__call', module.callable.name) + eq('vim.example', module.callable.module) + eq('Format a value.', module.callable.desc) + eq({ { name = 'value', type = 'any' } }, module.callable.params) + eq({ { type = 'string' } }, module.callable.returns) + end) + + it('links table imports and parent modules without copying or hiding members', function() + local _, core_funs, _, _, core = parser.parse_str( + dedent([[ + --- @class vim._core + local Core = {} + + --- Shared function. + --- @return string + function Core.create() end + return Core + ]]), + 'runtime/lua/vim/_core.lua' + ) + local _, funs, _, _, public = parser.parse_str( + dedent([[ + --- @class vim.public: vim._core + local M = {} + M._core = require('vim._core') + M.create = false + return M + ]]), + 'runtime/lua/vim/public.lua' + ) + parser.resolve_modules({ ['vim._core'] = core, ['vim.public'] = public }) + + -- Check table identity: both links must reference the original parsed module. + eq(true, core == public.imports._core) + eq(true, core == public.parent) + eq(true, public.assignments.create) + eq({}, funs) + eq('create', core_funs[1].name) + eq('vim._core', core_funs[1].module) + eq('Shared function.', core_funs[1].desc) + eq({ { type = 'string' } }, core_funs[1].returns) + eq(nil, core_funs[1].nodoc) + end) + it('parses multiline return annotations', function() local _, funs = parser.parse_str( dedent([[ @@ -168,7 +262,8 @@ describe('luacats parser', function() it('tracks class member declaration style', function() local classes, funs = parser.parse_str( - dedent([[ --- @class vim.MyClass + dedent([[ + --- @class vim.MyClass local MyClass = {} --- Dot member.