aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/_defaults.lua75
-rw-r--r--runtime/lua/vim/_editor.lua28
-rw-r--r--runtime/lua/vim/_inspector.lua27
-rw-r--r--runtime/lua/vim/_meta/api.lua206
-rw-r--r--runtime/lua/vim/_meta/api_keysets.lua102
-rw-r--r--runtime/lua/vim/_meta/api_keysets_extra.lua66
-rw-r--r--runtime/lua/vim/_meta/builtin.lua5
-rw-r--r--runtime/lua/vim/_meta/json.lua15
-rw-r--r--runtime/lua/vim/_meta/options.lua209
-rw-r--r--runtime/lua/vim/_meta/vimfn.lua315
-rw-r--r--runtime/lua/vim/_meta/vvars.lua34
-rw-r--r--runtime/lua/vim/_meta/vvars_extra.lua77
-rw-r--r--runtime/lua/vim/_options.lua13
-rw-r--r--runtime/lua/vim/_system.lua129
-rw-r--r--runtime/lua/vim/diagnostic.lua517
-rw-r--r--runtime/lua/vim/filetype.lua68
-rw-r--r--runtime/lua/vim/filetype/detect.lua61
-rw-r--r--runtime/lua/vim/fs.lua158
-rw-r--r--runtime/lua/vim/func.lua14
-rw-r--r--runtime/lua/vim/func/_memoize.lua56
-rw-r--r--runtime/lua/vim/glob.lua1
-rw-r--r--runtime/lua/vim/health.lua65
-rw-r--r--runtime/lua/vim/health/health.lua13
-rw-r--r--runtime/lua/vim/hl.lua45
-rw-r--r--runtime/lua/vim/inspect.lua1
-rw-r--r--runtime/lua/vim/loader.lua71
-rw-r--r--runtime/lua/vim/lsp.lua692
-rw-r--r--runtime/lua/vim/lsp/_changetracking.lua18
-rw-r--r--runtime/lua/vim/lsp/_folding_range.lua373
-rw-r--r--runtime/lua/vim/lsp/_meta.lua4
-rw-r--r--runtime/lua/vim/lsp/_snippet_grammar.lua1
-rw-r--r--runtime/lua/vim/lsp/_tagfunc.lua18
-rw-r--r--runtime/lua/vim/lsp/_transport.lua182
-rw-r--r--runtime/lua/vim/lsp/_watchfiles.lua3
-rw-r--r--runtime/lua/vim/lsp/buf.lua98
-rw-r--r--runtime/lua/vim/lsp/client.lua411
-rw-r--r--runtime/lua/vim/lsp/codelens.lua14
-rw-r--r--runtime/lua/vim/lsp/completion.lua43
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua29
-rw-r--r--runtime/lua/vim/lsp/handlers.lua20
-rw-r--r--runtime/lua/vim/lsp/health.lua77
-rw-r--r--runtime/lua/vim/lsp/inlay_hint.lua29
-rw-r--r--runtime/lua/vim/lsp/protocol.lua15
-rw-r--r--runtime/lua/vim/lsp/rpc.lua290
-rw-r--r--runtime/lua/vim/lsp/semantic_tokens.lua35
-rw-r--r--runtime/lua/vim/lsp/sync.lua52
-rw-r--r--runtime/lua/vim/lsp/util.lua222
-rw-r--r--runtime/lua/vim/provider/health.lua34
-rw-r--r--runtime/lua/vim/re.lua1
-rw-r--r--runtime/lua/vim/shared.lua74
-rw-r--r--runtime/lua/vim/snippet.lua4
-rw-r--r--runtime/lua/vim/text.lua24
-rw-r--r--runtime/lua/vim/treesitter.lua57
-rw-r--r--runtime/lua/vim/treesitter/_fold.lua325
-rw-r--r--runtime/lua/vim/treesitter/_meta/misc.lua8
-rw-r--r--runtime/lua/vim/treesitter/_meta/tsnode.lua16
-rw-r--r--runtime/lua/vim/treesitter/_query_linter.lua6
-rw-r--r--runtime/lua/vim/treesitter/dev.lua25
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua30
-rw-r--r--runtime/lua/vim/treesitter/language.lua7
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua356
-rw-r--r--runtime/lua/vim/treesitter/query.lua418
-rw-r--r--runtime/lua/vim/uri.lua7
-rw-r--r--runtime/lua/vim/version.lua3
-rw-r--r--runtime/lua/vim/vimhelp.lua2
65 files changed, 4289 insertions, 2105 deletions
diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua
index 06f6ed6829..69204e3fe6 100644
--- a/runtime/lua/vim/_defaults.lua
+++ b/runtime/lua/vim/_defaults.lua
@@ -49,10 +49,10 @@ do
vim.keymap.set('x', '*', function()
return _visual_search('/')
- end, { desc = ':help v_star-default', expr = true, silent = true })
+ end, { desc = ':help v_star-default', expr = true, replace_keycodes = false })
vim.keymap.set('x', '#', function()
return _visual_search('?')
- end, { desc = ':help v_#-default', expr = true, silent = true })
+ end, { desc = ':help v_#-default', expr = true, replace_keycodes = false })
end
--- Map Y to y$. This mimics the behavior of D and C. See |Y-default|
@@ -222,9 +222,9 @@ do
--- Execute a command and print errors without a stacktrace.
--- @param opts table Arguments to |nvim_cmd()|
local function cmd(opts)
- local _, err = pcall(vim.api.nvim_cmd, opts, {})
- if err then
- vim.api.nvim_err_writeln(err:sub(#'Vim:' + 1))
+ local ok, err = pcall(vim.api.nvim_cmd, opts, {})
+ if not ok then
+ vim.api.nvim_echo({ { err:sub(#'Vim:' + 1) } }, true, { err = true })
end
end
@@ -412,7 +412,7 @@ do
end
end
- local nvim_popupmenu_augroup = vim.api.nvim_create_augroup('nvim_popupmenu', {})
+ local nvim_popupmenu_augroup = vim.api.nvim_create_augroup('nvim.popupmenu', {})
vim.api.nvim_create_autocmd('MenuPopup', {
pattern = '*',
group = nvim_popupmenu_augroup,
@@ -429,13 +429,13 @@ end
--- Default autocommands. See |default-autocmds|
do
- local nvim_terminal_augroup = vim.api.nvim_create_augroup('nvim_terminal', {})
+ local nvim_terminal_augroup = vim.api.nvim_create_augroup('nvim.terminal', {})
vim.api.nvim_create_autocmd('BufReadCmd', {
pattern = 'term://*',
group = nvim_terminal_augroup,
desc = 'Treat term:// buffers as terminal buffers',
nested = true,
- command = "if !exists('b:term_title')|call termopen(matchstr(expand(\"<amatch>\"), '\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), {'cwd': expand(get(matchlist(expand(\"<amatch>\"), '\\c\\mterm://\\(.\\{-}\\)//'), 1, ''))})",
+ command = "if !exists('b:term_title')|call jobstart(matchstr(expand(\"<amatch>\"), '\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), {'term': v:true, 'cwd': expand(get(matchlist(expand(\"<amatch>\"), '\\c\\mterm://\\(.\\{-}\\)//'), 1, ''))})",
})
vim.api.nvim_create_autocmd({ 'TermClose' }, {
@@ -492,6 +492,10 @@ do
vim.bo.textwidth = 0
vim.wo[0][0].wrap = false
vim.wo[0][0].list = false
+ vim.wo[0][0].number = false
+ vim.wo[0][0].relativenumber = false
+ vim.wo[0][0].signcolumn = 'no'
+ vim.wo[0][0].foldcolumn = '0'
-- This is gross. Proper list options support when?
local winhl = vim.o.winhighlight
@@ -505,14 +509,14 @@ do
vim.api.nvim_create_autocmd('CmdwinEnter', {
pattern = '[:>]',
desc = 'Limit syntax sync to maxlines=1 in the command window',
- group = vim.api.nvim_create_augroup('nvim_cmdwin', {}),
+ group = vim.api.nvim_create_augroup('nvim.cmdwin', {}),
command = 'syntax sync minlines=1 maxlines=1',
})
vim.api.nvim_create_autocmd('SwapExists', {
pattern = '*',
desc = 'Skip the swapfile prompt when the swapfile is owned by a running Nvim process',
- group = vim.api.nvim_create_augroup('nvim_swapfile', {}),
+ group = vim.api.nvim_create_augroup('nvim.swapfile', {}),
callback = function()
local info = vim.fn.swapinfo(vim.v.swapname)
local user = vim.uv.os_get_passwd().username
@@ -539,15 +543,16 @@ do
end
if tty then
- local group = vim.api.nvim_create_augroup('nvim_tty', {})
+ local group = vim.api.nvim_create_augroup('nvim.tty', {})
--- Set an option after startup (so that OptionSet is fired), but only if not
--- already set by the user.
---
--- @param option string Option name
--- @param value any Option value
- local function setoption(option, value)
- if vim.api.nvim_get_option_info2(option, {}).was_set then
+ --- @param force boolean? Always set the value, even if already set
+ local function setoption(option, value, force)
+ if not force and vim.api.nvim_get_option_info2(option, {}).was_set then
-- Don't do anything if option is already set
return
end
@@ -563,7 +568,7 @@ do
once = true,
nested = true,
callback = function()
- setoption(option, value)
+ setoption(option, value, force)
end,
})
end
@@ -645,11 +650,15 @@ do
return nil, nil, nil
end
- local timer = assert(vim.uv.new_timer())
-
+ -- This autocommand updates the value of 'background' anytime we receive
+ -- an OSC 11 response from the terminal emulator. If the user has set
+ -- 'background' explicitly then we will delete this autocommand,
+ -- effectively disabling automatic background setting.
+ local force = false
local id = vim.api.nvim_create_autocmd('TermResponse', {
group = group,
nested = true,
+ desc = "Update the value of 'background' automatically based on the terminal emulator's background color",
callback = function(args)
local resp = args.data ---@type string
local r, g, b = parseosc11(resp)
@@ -661,27 +670,33 @@ do
if rr and gg and bb then
local luminance = (0.299 * rr) + (0.587 * gg) + (0.114 * bb)
local bg = luminance < 0.5 and 'dark' or 'light'
- setoption('background', bg)
+ setoption('background', bg, force)
+
+ -- On the first query response, don't force setting the option in
+ -- case the user has already set it manually. If they have, then
+ -- this autocommand will be deleted. If they haven't, then we do
+ -- want to force setting the option to override the value set by
+ -- this autocommand.
+ if not force then
+ force = true
+ end
end
+ end
+ end,
+ })
- return true
+ vim.api.nvim_create_autocmd('VimEnter', {
+ group = group,
+ nested = true,
+ once = true,
+ callback = function()
+ if vim.api.nvim_get_option_info2('background', {}).was_set then
+ vim.api.nvim_del_autocmd(id)
end
end,
})
io.stdout:write('\027]11;?\007')
-
- timer:start(1000, 0, function()
- -- Delete the autocommand if no response was received
- vim.schedule(function()
- -- Suppress error if autocommand has already been deleted
- pcall(vim.api.nvim_del_autocmd, id)
- end)
-
- if not timer:is_closing() then
- timer:close()
- end
- end)
end
--- If the TUI (term_has_truecolor) was able to determine that the host
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
index 44f17b3f85..a77ea9bb91 100644
--- a/runtime/lua/vim/_editor.lua
+++ b/runtime/lua/vim/_editor.lua
@@ -58,6 +58,7 @@ vim._extra = {
--- @private
vim.log = {
+ --- @enum vim.log.levels
levels = {
TRACE = 0,
DEBUG = 1,
@@ -92,7 +93,7 @@ local utfs = {
---
--- -- Runs synchronously:
--- local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
---- -- { code = 0, signal = 0, stdout = 'hello', stderr = '' }
+--- -- { code = 0, signal = 0, stdout = 'hello\n', stderr = '' }
---
--- ```
---
@@ -390,7 +391,7 @@ end
local VIM_CMD_ARG_MAX = 20
---- Executes Vim script commands.
+--- Executes Vimscript (|Ex-commands|).
---
--- Note that `vim.cmd` can be indexed with a command name to return a callable function to the
--- command.
@@ -424,8 +425,9 @@ local VIM_CMD_ARG_MAX = 20
--- vim.cmd.colorscheme('blue')
--- ```
---
+---@diagnostic disable-next-line: undefined-doc-param
---@param command string|table Command(s) to execute.
---- If a string, executes multiple lines of Vim script at once. In this
+--- If a string, executes multiple lines of Vimscript at once. In this
--- case, it is an alias to |nvim_exec2()|, where `opts.output` is set
--- to false. Thus it works identical to |:source|.
--- If a table, executes a single command. In this case, it is an alias
@@ -440,10 +442,12 @@ vim.cmd = setmetatable({}, {
return ''
end
end,
+ --- @param t table<string,function>
__index = function(t, command)
t[command] = function(...)
- local opts
+ local opts --- @type vim.api.keyset.cmd
if select('#', ...) == 1 and type(select(1, ...)) == 'table' then
+ --- @type vim.api.keyset.cmd
opts = select(1, ...)
-- Move indexed positions in opts to opt.args
@@ -454,6 +458,7 @@ vim.cmd = setmetatable({}, {
break
end
opts.args[i] = opts[i]
+ --- @diagnostic disable-next-line: no-unknown
opts[i] = nil
end
end
@@ -528,7 +533,7 @@ function vim.region(bufnr, pos1, pos2, regtype, inclusive)
end
if pos1[1] > pos2[1] or (pos1[1] == pos2[1] and pos1[2] > pos2[2]) then
- pos1, pos2 = pos2, pos1
+ pos1, pos2 = pos2, pos1 --- @type [integer, integer], [integer, integer]
end
-- getpos() may return {0,0,0,0}
@@ -620,13 +625,8 @@ end
---@param opts table|nil Optional parameters. Unused by default.
---@diagnostic disable-next-line: unused-local
function vim.notify(msg, level, opts) -- luacheck: no unused args
- if level == vim.log.levels.ERROR then
- vim.api.nvim_err_writeln(msg)
- elseif level == vim.log.levels.WARN then
- vim.api.nvim_echo({ { msg, 'WarningMsg' } }, true, {})
- else
- vim.api.nvim_echo({ { msg } }, true, {})
- end
+ local chunks = { { msg, level == vim.log.levels.WARN and 'WarningMsg' or nil } }
+ vim.api.nvim_echo(chunks, true, { err = level == vim.log.levels.ERROR })
end
do
@@ -705,6 +705,7 @@ function vim._on_key(buf, typed_buf)
local discard = false
for k, v in pairs(on_key_cbs) do
local fn = v[1]
+ --- @type boolean, any
local ok, rv = xpcall(function()
return fn(buf, typed_buf)
end, debug.traceback)
@@ -832,6 +833,7 @@ function vim.str_utfindex(s, encoding, index, strict_indexing)
-- Return (multiple): ~
-- (`integer`) UTF-32 index
-- (`integer`) UTF-16 index
+ --- @diagnostic disable-next-line: redundant-return-value
return col32, col16
end
@@ -1004,7 +1006,7 @@ function vim._expand_pat(pat, env)
or vim.v == final_env and { 'v:', 'var' }
or { nil, nil }
)
- assert(prefix, "Can't resolve final_env")
+ assert(prefix and type, "Can't resolve final_env")
local vars = vim.fn.getcompletion(prefix .. match_part, type) --- @type string[]
insert_keys(vim
.iter(vars)
diff --git a/runtime/lua/vim/_inspector.lua b/runtime/lua/vim/_inspector.lua
index fccf4b8dbe..35063dffca 100644
--- a/runtime/lua/vim/_inspector.lua
+++ b/runtime/lua/vim/_inspector.lua
@@ -1,3 +1,5 @@
+--- @diagnostic disable:no-unknown
+
--- @class vim._inspector.Filter
--- @inlinedoc
---
@@ -53,7 +55,7 @@ function vim.inspect_pos(bufnr, row, col, filter)
local cursor = vim.api.nvim_win_get_cursor(win)
row, col = cursor[1] - 1, cursor[2]
end
- bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr
+ bufnr = vim._resolve_bufnr(bufnr)
local results = {
treesitter = {}, --- @type table[]
@@ -78,6 +80,7 @@ function vim.inspect_pos(bufnr, row, col, filter)
-- treesitter
if filter.treesitter then
for _, capture in pairs(vim.treesitter.get_captures_at_pos(bufnr, row, col)) do
+ --- @diagnostic disable-next-line: inject-field
capture.hl_group = '@' .. capture.capture .. '.' .. capture.lang
results.treesitter[#results.treesitter + 1] = resolve_hl(capture)
end
@@ -128,13 +131,13 @@ function vim.inspect_pos(bufnr, row, col, filter)
if filter.semantic_tokens then
results.semantic_tokens = vim.tbl_filter(function(extmark)
- return extmark.ns:find('vim_lsp_semantic_tokens') == 1
+ return extmark.ns:find('nvim.lsp.semantic_tokens') == 1
end, extmarks)
end
if filter.extmarks then
results.extmarks = vim.tbl_filter(function(extmark)
- return extmark.ns:find('vim_lsp_semantic_tokens') ~= 1
+ return extmark.ns:find('nvim.lsp.semantic_tokens') ~= 1
and (filter.extmarks == 'all' or extmark.opts.hl_group)
end, extmarks)
end
@@ -146,6 +149,13 @@ end
---
---Can also be shown with `:Inspect`. [:Inspect]()
---
+---Example: To bind this function to the vim-scriptease
+---inspired `zS` in Normal mode:
+---
+---```lua
+---vim.keymap.set('n', 'zS', vim.show_pos)
+---```
+---
---@since 11
---@param bufnr? integer defaults to the current buffer
---@param row? integer row to inspect, 0-based. Defaults to the row of the current cursor
@@ -171,7 +181,7 @@ function vim.show_pos(bufnr, row, col, filter)
if data.hl_group ~= data.hl_group_link then
append('links to ', 'MoreMsg')
append(data.hl_group_link, data.hl_group_link)
- append(' ')
+ append(' ')
end
if comment then
append(comment, 'Comment')
@@ -184,7 +194,14 @@ function vim.show_pos(bufnr, row, col, filter)
append('Treesitter', 'Title')
nl()
for _, capture in ipairs(items.treesitter) do
- item(capture, capture.lang)
+ item(
+ capture,
+ string.format(
+ 'priority: %d language: %s',
+ capture.metadata.priority or vim.hl.priorities.treesitter,
+ capture.lang
+ )
+ )
end
nl()
end
diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua
index 3c9b9d4f44..3d10729d23 100644
--- a/runtime/lua/vim/_meta/api.lua
+++ b/runtime/lua/vim/_meta/api.lua
@@ -3,6 +3,10 @@
-- DO NOT EDIT
error('Cannot require a meta file')
+--- This file embeds vimdoc as the function descriptions
+--- so ignore any doc related errors.
+--- @diagnostic disable: undefined-doc-name,luadoc-miss-symbol
+
vim.api = {}
--- @private
@@ -163,35 +167,14 @@ function vim.api.nvim__stats() end
--- @return any
function vim.api.nvim__unpack(str) end
---- Adds a highlight to buffer.
----
---- Useful for plugins that dynamically generate highlights to a buffer
---- (like a semantic highlighter or linter). The function adds a single
---- highlight to a buffer. Unlike `matchaddpos()` highlights follow changes to
---- line numbering (as lines are inserted/removed above the highlighted line),
---- like signs and marks do.
----
---- Namespaces are used for batch deletion/updating of a set of highlights. To
---- create a namespace, use `nvim_create_namespace()` which returns a namespace
---- id. Pass it in to this function as `ns_id` to add highlights to the
---- namespace. All highlights in the same namespace can then be cleared with
---- single call to `nvim_buf_clear_namespace()`. If the highlight never will be
---- deleted by an API call, pass `ns_id = -1`.
----
---- As a shorthand, `ns_id = 0` can be used to create a new namespace for the
---- highlight, the allocated id is then returned. If `hl_group` is the empty
---- string no highlight is added, but a new `ns_id` is still returned. This is
---- supported for backwards compatibility, new code should use
---- `nvim_create_namespace()` to create a new empty namespace.
----
---- @param buffer integer Buffer handle, or 0 for current buffer
---- @param ns_id integer namespace to use or -1 for ungrouped highlight
---- @param hl_group string Name of the highlight group to use
---- @param line integer Line to highlight (zero-indexed)
---- @param col_start integer Start of (byte-indexed) column range to highlight
---- @param col_end integer End of (byte-indexed) column range to highlight,
---- or -1 to highlight to end of line
---- @return integer # The ns_id that was used
+--- @deprecated
+--- @param buffer integer
+--- @param ns_id integer
+--- @param hl_group string
+--- @param line integer
+--- @param col_start integer
+--- @param col_end integer
+--- @return integer
function vim.api.nvim_buf_add_highlight(buffer, ns_id, hl_group, line, col_start, col_end) end
--- Activates buffer-update events on a channel, or as Lua callbacks.
@@ -272,12 +255,12 @@ function vim.api.nvim_buf_attach(buffer, send_buffer, opts) end
--- This temporarily switches current buffer to "buffer".
--- If the current window already shows "buffer", the window is not switched.
--- If a window inside the current tabpage (including a float) already shows the
---- buffer, then one of these windows will be set as current window temporarily.
+--- buffer, then one of those windows will be set as current window temporarily.
--- Otherwise a temporary scratch window (called the "autocmd window" for
--- historical reasons) will be used.
---
--- This is useful e.g. to call Vimscript functions that only work with the
---- current buffer/window currently, like `termopen()`.
+--- current buffer/window currently, like `jobstart(…, {'term': v:true})`.
---
--- @param buffer integer Buffer handle, or 0 for current buffer
--- @param fun function Function to call inside the buffer (currently Lua callable
@@ -452,7 +435,7 @@ function vim.api.nvim_buf_get_extmarks(buffer, ns_id, start, end_, opts) end
---
--- @param buffer integer Buffer handle, or 0 for current buffer
--- @param mode string Mode short-name ("n", "i", "v", ...)
---- @return vim.api.keyset.keymap[] # Array of |maparg()|-like dictionaries describing mappings.
+--- @return vim.api.keyset.get_keymap[] # Array of |maparg()|-like dictionaries describing mappings.
--- The "buffer" key holds the associated buffer handle.
function vim.api.nvim_buf_get_keymap(buffer, mode) end
@@ -595,6 +578,9 @@ function vim.api.nvim_buf_line_count(buffer) end
--- - hl_group : highlight group used for the text range. This and below
--- highlight groups can be supplied either as a string or as an integer,
--- the latter of which can be obtained using `nvim_get_hl_id_by_name()`.
+---
+--- Multiple highlight groups can be stacked by passing an array (highest
+--- priority last).
--- - hl_eol : when true, for a multiline highlight covering the
--- EOL of a line, continue the highlight for the rest
--- of the screen line (just like for diff and
@@ -607,6 +593,15 @@ function vim.api.nvim_buf_line_count(buffer) end
--- (highest priority last).
--- - virt_text_pos : position of virtual text. Possible values:
--- - "eol": right after eol character (default).
+--- - "eol_right_align": display right aligned in the window
+--- unless the virtual text is longer than
+--- the space available. If the virtual
+--- text is too long, it is truncated to
+--- fit in the window after the EOL
+--- character. If the line is wrapped, the
+--- virtual text is shown after the end of
+--- the line rather than the previous
+--- screen line.
--- - "overlay": display over the specified column, without
--- shifting the underlying text.
--- - "right_align": display right aligned in the window.
@@ -885,10 +880,8 @@ function vim.api.nvim_cmd(cmd, opts) end
---
--- On execution error: fails with Vimscript error, updates v:errmsg.
---
---- Prefer using `nvim_cmd()` or `nvim_exec2()` over this. To evaluate multiple lines of Vim script
---- or an Ex command directly, use `nvim_exec2()`. To construct an Ex command using a structured
---- format and then execute it, use `nvim_cmd()`. To modify an Ex command before evaluating it, use
---- `nvim_parse_cmd()` in conjunction with `nvim_cmd()`.
+--- Prefer `nvim_cmd()` or `nvim_exec2()` instead. To modify an Ex command in a structured way
+--- before executing it, modify the result of `nvim_parse_cmd()` then pass it to `nvim_cmd()`.
---
--- @param command string Ex command string
function vim.api.nvim_command(command) end
@@ -963,9 +956,9 @@ function vim.api.nvim_create_augroup(name, opts) end
--- - id: (number) autocommand id
--- - event: (string) name of the triggered event `autocmd-events`
--- - group: (number|nil) autocommand group id, if any
---- - match: (string) expanded value of [<amatch>]
---- - buf: (number) expanded value of [<abuf>]
---- - file: (string) expanded value of [<afile>]
+--- - file: (string) [<afile>] (not expanded to a full path)
+--- - match: (string) [<amatch>] (expanded to a full path)
+--- - buf: (number) [<abuf>]
--- - data: (any) arbitrary data passed from [nvim_exec_autocmds()] [event-data]()
--- - command (string) optional: Vim command to execute on event. Cannot be used with
--- {callback}
@@ -989,7 +982,7 @@ function vim.api.nvim_create_buf(listed, scratch) end
--- Creates a new namespace or gets an existing one. [namespace]()
---
--- Namespaces are used for buffer highlights and virtual text, see
---- `nvim_buf_add_highlight()` and `nvim_buf_set_extmark()`.
+--- `nvim_buf_set_extmark()`.
---
--- Namespaces can be named or anonymous. If `name` matches an existing
--- namespace, the associated id is returned. If `name` is an empty string
@@ -1012,7 +1005,7 @@ function vim.api.nvim_create_namespace(name) end
--- ```
---
--- @param name string Name of the new user command. Must begin with an uppercase letter.
---- @param command any Replacement command to execute when this user command is executed. When called
+--- @param command string|fun(args: vim.api.keyset.create_user_command.command_args) Replacement command to execute when this user command is executed. When called
--- from Lua, the command can also be a Lua function. The function is called with a
--- single table argument that contains the following keys:
--- - name: (string) Command name
@@ -1099,29 +1092,28 @@ function vim.api.nvim_del_user_command(name) end
--- @param name string Variable name
function vim.api.nvim_del_var(name) end
---- Echo a message.
+--- Prints a message given by a list of `[text, hl_group]` "chunks".
+---
+--- Example:
+--- ```lua
+--- vim.api.nvim_echo({ { 'chunk1-line1\nchunk1-line2\n' }, { 'chunk2-line1' } }, true, {})
+--- ```
---
---- @param chunks any[] A list of `[text, hl_group]` arrays, each representing a
---- text chunk with specified highlight group name or ID.
---- `hl_group` element can be omitted for no highlight.
+--- @param chunks any[] List of `[text, hl_group]` pairs, where each is a `text` string highlighted by
+--- the (optional) name or ID `hl_group`.
--- @param history boolean if true, add to `message-history`.
--- @param opts vim.api.keyset.echo_opts Optional parameters.
---- - verbose: Message is printed as a result of 'verbose' option.
---- If Nvim was invoked with -V3log_file, the message will be
---- redirected to the log_file and suppressed from direct output.
+--- - err: Treat the message like `:echoerr`. Sets `hl_group` to `hl-ErrorMsg` by default.
+--- - verbose: Message is controlled by the 'verbose' option. Nvim invoked with `-V3log`
+--- will write the message to the "log" file instead of standard output.
function vim.api.nvim_echo(chunks, history, opts) end
---- Writes a message to the Vim error buffer. Does not append "\n", the
---- message is buffered (won't display) until a linefeed is written.
----
---- @param str string Message
+--- @deprecated
+--- @param str string
function vim.api.nvim_err_write(str) end
---- Writes a message to the Vim error buffer. Appends "\n", so the buffer is
---- flushed (and displayed).
----
---- @see vim.api.nvim_err_write
---- @param str string Message
+--- @deprecated
+--- @param str string
function vim.api.nvim_err_writeln(str) end
--- Evaluates a Vimscript `expression`. Dicts and Lists are recursively expanded.
@@ -1152,7 +1144,9 @@ function vim.api.nvim_eval(expr) end
--- the "highlights" key in {opts} is true. Each element of the array is a
--- |Dict| with these keys:
--- - start: (number) Byte index (0-based) of first character that uses the highlight.
---- - group: (string) Name of highlight group.
+--- - group: (string) Name of highlight group. May be removed in the future, use
+--- `groups` instead.
+--- - groups: (array) Names of stacked highlight groups (highest priority last).
function vim.api.nvim_eval_statusline(str, opts) end
--- @deprecated
@@ -1256,31 +1250,34 @@ function vim.api.nvim_get_all_options_info() end
--- match any combination of them.
---
--- @param opts vim.api.keyset.get_autocmds Dict with at least one of the following:
---- - group (string|integer): the autocommand group name or id to match against.
---- - event (string|array): event or events to match against `autocmd-events`.
---- - pattern (string|array): pattern or patterns to match against `autocmd-pattern`.
---- Cannot be used with {buffer}
---- - buffer: Buffer number or list of buffer numbers for buffer local autocommands
+--- - buffer: (integer) Buffer number or list of buffer numbers for buffer local autocommands
--- `autocmd-buflocal`. Cannot be used with {pattern}
+--- - event: (string|table) event or events to match against `autocmd-events`.
+--- - id: (integer) Autocommand ID to match.
+--- - group: (string|table) the autocommand group name or id to match against.
+--- - pattern: (string|table) pattern or patterns to match against `autocmd-pattern`.
+--- Cannot be used with {buffer}
--- @return vim.api.keyset.get_autocmds.ret[] # Array of autocommands matching the criteria, with each item
--- containing the following fields:
---- - id (number): the autocommand id (only when defined with the API).
---- - group (integer): the autocommand group id.
---- - group_name (string): the autocommand group name.
---- - desc (string): the autocommand description.
---- - event (string): the autocommand event.
---- - command (string): the autocommand command. Note: this will be empty if a callback is set.
---- - callback (function|string|nil): Lua function or name of a Vim script function
+--- - buffer: (integer) the buffer number.
+--- - buflocal: (boolean) true if the autocommand is buffer local.
+--- - command: (string) the autocommand command. Note: this will be empty if a callback is set.
+--- - callback: (function|string|nil): Lua function or name of a Vim script function
--- which is executed when this autocommand is triggered.
---- - once (boolean): whether the autocommand is only run once.
---- - pattern (string): the autocommand pattern.
+--- - desc: (string) the autocommand description.
+--- - event: (string) the autocommand event.
+--- - id: (integer) the autocommand id (only when defined with the API).
+--- - group: (integer) the autocommand group id.
+--- - group_name: (string) the autocommand group name.
+--- - once: (boolean) whether the autocommand is only run once.
+--- - pattern: (string) the autocommand pattern.
--- If the autocommand is buffer local |autocmd-buffer-local|:
---- - buflocal (boolean): true if the autocommand is buffer local.
---- - buffer (number): the buffer number.
function vim.api.nvim_get_autocmds(opts) end
--- Gets information about a channel.
---
+--- See `nvim_list_uis()` for an example of how to get channel info.
+---
--- @param chan integer channel_id, or 0 for current channel
--- @return table<string,any> # Channel info dict with these keys:
--- - "id" Channel id.
@@ -1298,8 +1295,8 @@ function vim.api.nvim_get_autocmds(opts) end
--- "/dev/pts/1". If unknown, the key will still be present if a pty is used (e.g.
--- for conpty on Windows).
--- - "buffer" (optional) Buffer connected to |terminal| instance.
---- - "client" (optional) Info about the peer (client on the other end of the RPC channel),
---- which it provided via |nvim_set_client_info()|.
+--- - "client" (optional) Info about the peer (client on the other end of the channel), as set
+--- by |nvim_set_client_info()|.
---
function vim.api.nvim_get_chan_info(chan) end
@@ -1416,7 +1413,7 @@ function vim.api.nvim_get_hl_ns(opts) end
--- Gets a list of global (non-buffer-local) `mapping` definitions.
---
--- @param mode string Mode short-name ("n", "i", "v", ...)
---- @return vim.api.keyset.keymap[] # Array of |maparg()|-like dictionaries describing mappings.
+--- @return vim.api.keyset.get_keymap[] # Array of |maparg()|-like dictionaries describing mappings.
--- The "buffer" key is always zero.
function vim.api.nvim_get_keymap(mode) end
@@ -1621,6 +1618,14 @@ function vim.api.nvim_list_tabpages() end
--- Gets a list of dictionaries representing attached UIs.
---
+--- Example: The Nvim builtin `TUI` sets its channel info as described in `startup-tui`. In
+--- particular, it sets `client.name` to "nvim-tui". So you can check if the TUI is running by
+--- inspecting the client name of each UI:
+---
+--- ```lua
+--- vim.print(vim.api.nvim_get_chan_info(vim.api.nvim_list_uis()[1].chan).client.name)
+--- ```
+---
--- @return any[] # Array of UI dictionaries, each with these keys:
--- - "height" Requested height of the UI
--- - "width" Requested width of the UI
@@ -1640,21 +1645,17 @@ function vim.api.nvim_list_wins() end
--- @return any
function vim.api.nvim_load_context(dict) end
---- Notify the user with a message
----
---- Relays the call to vim.notify . By default forwards your message in the
---- echo area but can be overridden to trigger desktop notifications.
----
---- @param msg string Message to display to the user
---- @param log_level integer The log level
---- @param opts table<string,any> Reserved for future use.
+--- @deprecated
+--- @param msg string
+--- @param log_level integer
+--- @param opts table<string,any>
--- @return any
function vim.api.nvim_notify(msg, log_level, opts) end
--- Open a terminal instance in a buffer
---
--- By default (and currently the only option) the terminal will not be
---- connected to an external process. Instead, input send on the channel
+--- connected to an external process. Instead, input sent on the channel
--- will be echoed directly by the terminal. This is useful to display
--- ANSI terminal sequences returned as part of a rpc message, or similar.
---
@@ -1665,6 +1666,19 @@ function vim.api.nvim_notify(msg, log_level, opts) end
--- Then `nvim_chan_send()` can be called immediately to process sequences
--- in a virtual terminal having the intended size.
---
+--- Example: this `TermHl` command can be used to display and highlight raw ANSI termcodes, so you
+--- can use Nvim as a "scrollback pager" (for terminals like kitty): [ansi-colorize]()
+--- [terminal-scrollback-pager]()
+---
+--- ```lua
+--- vim.api.nvim_create_user_command('TermHl', function()
+--- local b = vim.api.nvim_create_buf(false, true)
+--- local chan = vim.api.nvim_open_term(b, {})
+--- vim.api.nvim_chan_send(chan, table.concat(vim.api.nvim_buf_get_lines(0, 0, -1, false), '\n'))
+--- vim.api.nvim_win_set_buf(0, b)
+--- end, { desc = 'Highlights ANSI termcodes in curbuf' })
+--- ```
+---
--- @param buffer integer the buffer to use (expected to be empty)
--- @param opts vim.api.keyset.open_term Optional parameters.
--- - on_input: Lua callback for input sent, i e keypresses in terminal
@@ -1738,10 +1752,12 @@ function vim.api.nvim_open_term(buffer, opts) end
--- @param config vim.api.keyset.win_config Map defining the window configuration. Keys:
--- - relative: Sets the window layout to "floating", placed at (row,col)
--- coordinates relative to:
---- - "editor" The global editor grid
---- - "win" Window given by the `win` field, or current window.
---- - "cursor" Cursor position in current window.
---- - "mouse" Mouse position
+--- - "cursor" Cursor position in current window.
+--- - "editor" The global editor grid.
+--- - "laststatus" 'laststatus' if present, or last row.
+--- - "mouse" Mouse position.
+--- - "tabline" Tabline if present, or first row.
+--- - "win" Window given by the `win` field, or current window.
--- - win: `window-ID` window to split, or relative window when creating a
--- float (relative="win").
--- - anchor: Decides which corner of the float to place at (row,col):
@@ -1849,10 +1865,8 @@ function vim.api.nvim_open_term(buffer, opts) end
--- @return integer # Window handle, or 0 on error
function vim.api.nvim_open_win(buffer, enter, config) end
---- Writes a message to the Vim output buffer. Does not append "\n", the
---- message is buffered (won't display) until a linefeed is written.
----
---- @param str string Message
+--- @deprecated
+--- @param str string
function vim.api.nvim_out_write(str) end
--- Parse command line.
@@ -2124,8 +2138,8 @@ function vim.api.nvim_set_current_win(window) end
--- ```
--- ["start", tick]
--- ```
---- - on_buf: called for each buffer being redrawn (before
---- window callbacks)
+--- - on_buf: called for each buffer being redrawn (once per edit,
+--- before window callbacks)
--- ```
--- ["buf", bufnr, tick]
--- ```
diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua
index bf184dee2d..4d0665872b 100644
--- a/runtime/lua/vim/_meta/api_keysets.lua
+++ b/runtime/lua/vim/_meta/api_keysets.lua
@@ -4,11 +4,11 @@
error('Cannot require a meta file')
--- @class vim.api.keyset.buf_attach
---- @field on_lines? function
---- @field on_bytes? function
---- @field on_changedtick? function
---- @field on_detach? function
---- @field on_reload? function
+--- @field on_lines? fun(_: "lines", bufnr: integer, changedtick: integer, first: integer, last_old: integer, last_new: integer, byte_count: integer, deleted_codepoints?: integer, deleted_codeunits?: integer): boolean?
+--- @field on_bytes? fun(_: "bytes", bufnr: integer, changedtick: integer, start_row: integer, start_col: integer, start_byte: integer, old_end_row: integer, old_end_col: integer, old_end_byte: integer, new_end_row: integer, new_end_col: integer, new_end_byte: integer): boolean?
+--- @field on_changedtick? fun(_: "changedtick", bufnr: integer, changedtick: integer)
+--- @field on_detach? fun(_: "detach", bufnr: integer)
+--- @field on_reload? fun(_: "reload", bufnr: integer)
--- @field utf_sizes? boolean
--- @field preview? boolean
@@ -18,9 +18,9 @@ error('Cannot require a meta file')
--- @class vim.api.keyset.clear_autocmds
--- @field buffer? integer
---- @field event? any
---- @field group? any
---- @field pattern? any
+--- @field event? string|string[]
+--- @field group? integer|string
+--- @field pattern? string|string[]
--- @class vim.api.keyset.cmd
--- @field cmd? string
@@ -28,12 +28,12 @@ error('Cannot require a meta file')
--- @field count? integer
--- @field reg? string
--- @field bang? boolean
---- @field args? any[]
+--- @field args? string[]
--- @field magic? table<string,any>
--- @field mods? table<string,any>
---- @field nargs? any
---- @field addr? any
---- @field nextcmd? any
+--- @field nargs? integer|string
+--- @field addr? string
+--- @field nextcmd? string
--- @class vim.api.keyset.cmd_magic
--- @field file? boolean
@@ -72,22 +72,23 @@ error('Cannot require a meta file')
--- @field info? string
--- @class vim.api.keyset.context
---- @field types? any[]
+--- @field types? string[]
--- @class vim.api.keyset.create_augroup
---- @field clear? any
+--- @field clear? boolean
--- @class vim.api.keyset.create_autocmd
--- @field buffer? integer
---- @field callback? any
+--- @field callback? string|(fun(args: vim.api.keyset.create_autocmd.callback_args): boolean?)
--- @field command? string
--- @field desc? string
---- @field group? any
+--- @field group? integer|string
--- @field nested? boolean
--- @field once? boolean
---- @field pattern? any
+--- @field pattern? string|string[]
--- @class vim.api.keyset.echo_opts
+--- @field err? boolean
--- @field verbose? boolean
--- @class vim.api.keyset.empty
@@ -103,19 +104,20 @@ error('Cannot require a meta file')
--- @class vim.api.keyset.exec_autocmds
--- @field buffer? integer
---- @field group? any
+--- @field group? integer|string
--- @field modeline? boolean
---- @field pattern? any
+--- @field pattern? string|string[]
--- @field data? any
--- @class vim.api.keyset.exec_opts
--- @field output? boolean
--- @class vim.api.keyset.get_autocmds
---- @field event? any
---- @field group? any
---- @field pattern? any
---- @field buffer? any
+--- @field event? string|string[]
+--- @field group? integer|string
+--- @field pattern? string|string[]
+--- @field buffer? integer|integer[]
+--- @field id? integer
--- @class vim.api.keyset.get_commands
--- @field builtin? boolean
@@ -154,17 +156,17 @@ error('Cannot require a meta file')
--- @field altfont? boolean
--- @field nocombine? boolean
--- @field default? boolean
---- @field cterm? any
---- @field foreground? any
---- @field fg? any
---- @field background? any
---- @field bg? any
---- @field ctermfg? any
---- @field ctermbg? any
---- @field special? any
---- @field sp? any
---- @field link? any
---- @field global_link? any
+--- @field cterm? integer|string
+--- @field foreground? integer|string
+--- @field fg? integer|string
+--- @field background? integer|string
+--- @field bg? integer|string
+--- @field ctermfg? integer|string
+--- @field ctermbg? integer|string
+--- @field special? integer|string
+--- @field sp? integer|string
+--- @field link? integer|string
+--- @field global_link? integer|string
--- @field fallback? boolean
--- @field blend? integer
--- @field fg_indexed? boolean
@@ -201,7 +203,7 @@ error('Cannot require a meta file')
--- @field wins? any[]
--- @class vim.api.keyset.open_term
---- @field on_input? function
+--- @field on_input? fun(_: "input", term: integer, bufnr: integer, data: any)
--- @field force_crlf? boolean
--- @class vim.api.keyset.option
@@ -227,20 +229,20 @@ error('Cannot require a meta file')
--- @field do_source? boolean
--- @class vim.api.keyset.set_decoration_provider
---- @field on_start? function
---- @field on_buf? function
---- @field on_win? function
---- @field on_line? function
---- @field on_end? function
---- @field _on_hl_def? function
---- @field _on_spell_nav? function
+--- @field on_start? fun(_: "start", tick: integer): boolean?
+--- @field on_buf? fun(_: "buf", bufnr: integer, tick: integer)
+--- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer): boolean?
+--- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer): boolean?
+--- @field on_end? fun(_: "end", tick: integer)
+--- @field _on_hl_def? fun(_: "hl_def")
+--- @field _on_spell_nav? fun(_: "spell_nav")
--- @class vim.api.keyset.set_extmark
--- @field id? integer
--- @field end_line? integer
--- @field end_row? integer
--- @field end_col? integer
---- @field hl_group? number|string
+--- @field hl_group? any
--- @field virt_text? any[]
--- @field virt_text_pos? string
--- @field virt_text_win_col? integer
@@ -258,10 +260,10 @@ error('Cannot require a meta file')
--- @field virt_lines_leftcol? boolean
--- @field strict? boolean
--- @field sign_text? string
---- @field sign_hl_group? number|string
---- @field number_hl_group? number|string
---- @field line_hl_group? number|string
---- @field cursorline_hl_group? number|string
+--- @field sign_hl_group? integer|string
+--- @field number_hl_group? integer|string
+--- @field line_hl_group? integer|string
+--- @field cursorline_hl_group? integer|string
--- @field conceal? string
--- @field spell? boolean
--- @field ui_watched? boolean
@@ -292,7 +294,7 @@ error('Cannot require a meta file')
--- @field relative? string
--- @field split? string
--- @field win? integer
---- @field bufpos? any[]
+--- @field bufpos? integer[]
--- @field external? boolean
--- @field focusable? boolean
--- @field mouse? boolean
@@ -315,12 +317,12 @@ error('Cannot require a meta file')
--- @field end_vcol? integer
--- @class vim.api.keyset.xdl_diff
---- @field on_hunk? function
+--- @field on_hunk? fun(start_a: integer, count_a: integer, start_b: integer, count_b: integer): integer?
--- @field result_type? string
--- @field algorithm? string
--- @field ctxlen? integer
--- @field interhunkctxlen? integer
---- @field linematch? any
+--- @field linematch? boolean|integer
--- @field ignore_whitespace? boolean
--- @field ignore_whitespace_change? boolean
--- @field ignore_whitespace_change_at_eol? boolean
diff --git a/runtime/lua/vim/_meta/api_keysets_extra.lua b/runtime/lua/vim/_meta/api_keysets_extra.lua
index 81bce50746..fbef6fa3bc 100644
--- a/runtime/lua/vim/_meta/api_keysets_extra.lua
+++ b/runtime/lua/vim/_meta/api_keysets_extra.lua
@@ -73,6 +73,51 @@ error('Cannot require a meta file')
--- @field buflocal? boolean
--- @field buffer? integer
+--- @class vim.api.keyset.create_autocmd.callback_args
+--- @field id integer autocommand id
+--- @field event string name of the triggered event |autocmd-events|
+--- @field group? integer autocommand group id, if any
+--- @field match string expanded value of <amatch>
+--- @field buf integer expanded value of <abuf>
+--- @field file string expanded value of <afile>
+--- @field data? any arbitrary data passed from |nvim_exec_autocmds()| *event-data*
+
+--- @class vim.api.keyset.create_user_command.command_args
+--- @field name string Command name
+---
+--- The args passed to the command, if any <args>
+--- @field args string
+---
+--- The args split by unescaped whitespace
+--- (when more than one argument is allowed), if any <f-args>
+--- @field fargs string[]
+---
+--- Number of arguments |:command-nargs|
+--- @field nargs string
+---
+--- "true" if the command was executed with a ! modifier <bang>
+--- @field bang boolean
+---
+--- The starting line of the command range <line1>
+--- @field line1 integer
+---
+--- The final line of the command range <line2>
+--- @field line2 integer
+---
+--- The number of items in the command range: 0, 1, or 2 <range>
+--- @field range integer
+---
+--- Any count supplied <count>
+--- @field count integer
+--- The optional register, if specified <reg>
+--- @field reg string
+--- Command modifiers, if any <mods>
+--- @field mods string
+---
+--- Command modifiers in a structured format. Has the same structure as the
+--- "mods" key of |nvim_parse_cmd()|.
+--- @field smods table
+
--- @class vim.api.keyset.command_info
--- @field name string
--- @field definition string
@@ -114,6 +159,7 @@ error('Cannot require a meta file')
--- @field bg? integer
--- @field sp? integer
--- @field default? true
+--- @field link? string
--- @field blend? integer
--- @field cterm? vim.api.keyset.hl_info.cterm
@@ -127,6 +173,26 @@ error('Cannot require a meta file')
--- @field force? true
--- @field cterm? vim.api.keyset.hl_info.cterm
+--- @class vim.api.keyset.get_keymap
+--- @field abbr? 0|1
+--- @field buffer? 0|1
+--- @field callback? function
+--- @field desc? string
+--- @field expr? 0|1
+--- @field lhs? string
+--- @field lhsraw? string
+--- @field lhsrawalt? string
+--- @field lnum? integer
+--- @field mode? string
+--- @field mode_bits? integer
+--- @field noremap? 0|1
+--- @field nowait? 0|1
+--- @field rhs? string
+--- @field script? 0|1
+--- @field scriptversion? integer
+--- @field sid? integer
+--- @field silent? 0|1
+
--- @class vim.api.keyset.get_mode
--- @field blocking boolean
--- @field mode string
diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua
index b8779b66fe..9fa2e242c4 100644
--- a/runtime/lua/vim/_meta/builtin.lua
+++ b/runtime/lua/vim/_meta/builtin.lua
@@ -233,9 +233,8 @@ function vim.wait(time, callback, interval, fast_only) end
--- {callback} receives event name plus additional parameters. See |ui-popupmenu|
--- and the sections below for event format for respective events.
---
---- Callbacks for `msg_show` events are executed in |api-fast| context unless
---- Nvim will wait for input, in which case messages should be shown
---- immediately.
+--- Callbacks for `msg_show` events are executed in |api-fast| context; showing
+--- the message should be scheduled.
---
--- Excessive errors inside the callback will result in forced detachment.
---
diff --git a/runtime/lua/vim/_meta/json.lua b/runtime/lua/vim/_meta/json.lua
index 07d89aafc8..0d59de5fa6 100644
--- a/runtime/lua/vim/_meta/json.lua
+++ b/runtime/lua/vim/_meta/json.lua
@@ -25,15 +25,18 @@ vim.json = {}
---
---@param str string Stringified JSON data.
---@param opts? table<string,any> Options table with keys:
---- - luanil: (table) Table with keys:
---- * object: (boolean) When true, converts `null` in JSON objects
---- to Lua `nil` instead of |vim.NIL|.
---- * array: (boolean) When true, converts `null` in JSON arrays
---- to Lua `nil` instead of |vim.NIL|.
+--- - luanil: (table) Table with keys:
+--- - object: (boolean) When true, converts `null` in JSON objects
+--- to Lua `nil` instead of |vim.NIL|.
+--- - array: (boolean) When true, converts `null` in JSON arrays
+--- to Lua `nil` instead of |vim.NIL|.
---@return any
function vim.json.decode(str, opts) end
--- Encodes (or "packs") Lua object {obj} as JSON in a Lua string.
---@param obj any
+---@param opts? table<string,any> Options table with keys:
+--- - escape_slash: (boolean) (default false) Escape slash
+--- characters "/" in string values.
---@return string
-function vim.json.encode(obj) end
+function vim.json.encode(obj, opts) end
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
index cb783720ac..52c556867f 100644
--- a/runtime/lua/vim/_meta/options.lua
+++ b/runtime/lua/vim/_meta/options.lua
@@ -52,7 +52,7 @@ vim.go.ari = vim.go.allowrevins
--- set to one of CJK locales. See Unicode Standard Annex #11
--- (https://www.unicode.org/reports/tr11).
---
---- @type string
+--- @type 'single'|'double'
vim.o.ambiwidth = "single"
vim.o.ambw = vim.o.ambiwidth
vim.go.ambiwidth = vim.o.ambiwidth
@@ -208,7 +208,7 @@ vim.go.awa = vim.go.autowriteall
--- will change. To use other settings, place ":highlight" commands AFTER
--- the setting of the 'background' option.
---
---- @type string
+--- @type 'light'|'dark'
vim.o.background = "dark"
vim.o.bg = vim.o.background
vim.go.background = vim.o.background
@@ -595,7 +595,7 @@ vim.wo.briopt = vim.wo.breakindentopt
--- This option is used together with 'buftype' and 'swapfile' to specify
--- special kinds of buffers. See `special-buffers`.
---
---- @type string
+--- @type ''|'hide'|'unload'|'delete'|'wipe'
vim.o.bufhidden = ""
vim.o.bh = vim.o.bufhidden
vim.bo.bufhidden = vim.o.bufhidden
@@ -658,7 +658,7 @@ vim.bo.bl = vim.bo.buflisted
--- without saving. For writing there must be matching `BufWriteCmd|,
--- |FileWriteCmd` or `FileAppendCmd` autocommands.
---
---- @type string
+--- @type ''|'acwrite'|'help'|'nofile'|'nowrite'|'quickfix'|'terminal'|'prompt'
vim.o.buftype = ""
vim.o.bt = vim.o.buftype
vim.bo.buftype = vim.o.buftype
@@ -1086,9 +1086,9 @@ vim.go.cia = vim.go.completeitemalign
--- a match from the menu. Only works in combination with
--- "menu" or "menuone". No effect if "longest" is present.
---
---- noselect Do not select a match in the menu, force the user to
---- select one from the menu. Only works in combination with
---- "menu" or "menuone".
+--- noselect Same as "noinsert", except that no menu item is
+--- pre-selected. If both "noinsert" and "noselect" are
+--- present, "noselect" has precedence.
---
--- fuzzy Enable `fuzzy-matching` for completion candidates. This
--- allows for more flexible and intuitive matching, where
@@ -1098,6 +1098,16 @@ vim.go.cia = vim.go.completeitemalign
--- list of alternatives, but not how the candidates are
--- collected (using different completion types).
---
+--- nosort Disable sorting of completion candidates based on fuzzy
+--- scores when "fuzzy" is enabled. Candidates will appear
+--- in their original order.
+---
+--- preinsert
+--- Preinsert the portion of the first candidate word that is
+--- not part of the current completion leader and using the
+--- `hl-ComplMatchIns` highlight group. Does not work when
+--- "fuzzy" is also included.
+---
--- @type string
vim.o.completeopt = "menu,preview"
vim.o.cot = vim.o.completeopt
@@ -1118,7 +1128,7 @@ vim.go.cot = vim.go.completeopt
--- For Insert mode completion the buffer-local value is used. For
--- command line completion the global value is used.
---
---- @type string
+--- @type ''|'slash'|'backslash'
vim.o.completeslash = ""
vim.o.csl = vim.o.completeslash
vim.bo.completeslash = vim.o.completeslash
@@ -1621,11 +1631,20 @@ vim.go.dex = vim.go.diffexpr
--- Option settings for diff mode. It can consist of the following items.
--- All are optional. Items must be separated by a comma.
---
---- filler Show filler lines, to keep the text
---- synchronized with a window that has inserted
---- lines at the same position. Mostly useful
---- when windows are side-by-side and 'scrollbind'
---- is set.
+--- algorithm:{text} Use the specified diff algorithm with the
+--- internal diff engine. Currently supported
+--- algorithms are:
+--- myers the default algorithm
+--- minimal spend extra time to generate the
+--- smallest possible diff
+--- patience patience diff algorithm
+--- histogram histogram diff algorithm
+---
+--- closeoff When a window is closed where 'diff' is set
+--- and there is only one window remaining in the
+--- same tab page with 'diff' set, execute
+--- `:diffoff` in that window. This undoes a
+--- `:diffsplit` command.
---
--- context:{n} Use a context of {n} lines between a change
--- and a fold that contains unchanged lines.
@@ -1636,6 +1655,23 @@ vim.go.dex = vim.go.diffexpr
--- value (999999) to disable folding completely.
--- See `fold-diff`.
---
+--- filler Show filler lines, to keep the text
+--- synchronized with a window that has inserted
+--- lines at the same position. Mostly useful
+--- when windows are side-by-side and 'scrollbind'
+--- is set.
+---
+--- foldcolumn:{n} Set the 'foldcolumn' option to {n} when
+--- starting diff mode. Without this 2 is used.
+---
+--- followwrap Follow the 'wrap' option and leave as it is.
+---
+--- horizontal Start diff mode with horizontal splits (unless
+--- explicitly specified otherwise).
+---
+--- hiddenoff Do not use diff mode for a buffer when it
+--- becomes hidden.
+---
--- iblank Ignore changes where lines are all blank. Adds
--- the "-B" flag to the "diff" command if
--- 'diffexpr' is empty. Check the documentation
@@ -1649,6 +1685,17 @@ vim.go.dex = vim.go.diffexpr
--- are considered the same. Adds the "-i" flag
--- to the "diff" command if 'diffexpr' is empty.
---
+--- indent-heuristic
+--- Use the indent heuristic for the internal
+--- diff library.
+---
+--- internal Use the internal diff library. This is
+--- ignored when 'diffexpr' is set. *E960*
+--- When running out of memory when writing a
+--- buffer this item will be ignored for diffs
+--- involving that buffer. Set the 'verbose'
+--- option to see when this happens.
+---
--- iwhite Ignore changes in amount of white space. Adds
--- the "-b" flag to the "diff" command if
--- 'diffexpr' is empty. Check the documentation
@@ -1668,56 +1715,19 @@ vim.go.dex = vim.go.diffexpr
--- of the "diff" command for what this does
--- exactly.
---
---- horizontal Start diff mode with horizontal splits (unless
---- explicitly specified otherwise).
+--- linematch:{n} Align and mark changes between the most
+--- similar lines between the buffers. When the
+--- total number of lines in the diff hunk exceeds
+--- {n}, the lines will not be aligned because for
+--- very large diff hunks there will be a
+--- noticeable lag. A reasonable setting is
+--- "linematch:60", as this will enable alignment
+--- for a 2 buffer diff hunk of 30 lines each,
+--- or a 3 buffer diff hunk of 20 lines each.
---
--- vertical Start diff mode with vertical splits (unless
--- explicitly specified otherwise).
---
---- closeoff When a window is closed where 'diff' is set
---- and there is only one window remaining in the
---- same tab page with 'diff' set, execute
---- `:diffoff` in that window. This undoes a
---- `:diffsplit` command.
----
---- hiddenoff Do not use diff mode for a buffer when it
---- becomes hidden.
----
---- foldcolumn:{n} Set the 'foldcolumn' option to {n} when
---- starting diff mode. Without this 2 is used.
----
---- followwrap Follow the 'wrap' option and leave as it is.
----
---- internal Use the internal diff library. This is
---- ignored when 'diffexpr' is set. *E960*
---- When running out of memory when writing a
---- buffer this item will be ignored for diffs
---- involving that buffer. Set the 'verbose'
---- option to see when this happens.
----
---- indent-heuristic
---- Use the indent heuristic for the internal
---- diff library.
----
---- linematch:{n} Enable a second stage diff on each generated
---- hunk in order to align lines. When the total
---- number of lines in a hunk exceeds {n}, the
---- second stage diff will not be performed as
---- very large hunks can cause noticeable lag. A
---- recommended setting is "linematch:60", as this
---- will enable alignment for a 2 buffer diff with
---- hunks of up to 30 lines each, or a 3 buffer
---- diff with hunks of up to 20 lines each.
----
---- algorithm:{text} Use the specified diff algorithm with the
---- internal diff engine. Currently supported
---- algorithms are:
---- myers the default algorithm
---- minimal spend extra time to generate the
---- smallest possible diff
---- patience patience diff algorithm
---- histogram histogram diff algorithm
----
--- Examples:
---
--- ```vim
@@ -1824,7 +1834,7 @@ vim.go.dy = vim.go.display
--- hor horizontally, height of windows is not affected
--- both width and height of windows is affected
---
---- @type string
+--- @type 'both'|'ver'|'hor'
vim.o.eadirection = "both"
vim.o.ead = vim.o.eadirection
vim.go.eadirection = vim.o.eadirection
@@ -2126,7 +2136,7 @@ vim.go.fencs = vim.go.fileencodings
--- option is set, because the file would be different when written.
--- This option cannot be changed when 'modifiable' is off.
---
---- @type string
+--- @type 'unix'|'dos'|'mac'
vim.o.fileformat = "unix"
vim.o.ff = vim.o.fileformat
vim.bo.fileformat = vim.o.fileformat
@@ -2382,7 +2392,7 @@ vim.go.fcl = vim.go.foldclose
--- "[1-9]": to display a fixed number of columns
--- See `folding`.
---
---- @type string
+--- @type 'auto'|'auto:1'|'auto:2'|'auto:3'|'auto:4'|'auto:5'|'auto:6'|'auto:7'|'auto:8'|'auto:9'|'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'
vim.o.foldcolumn = "0"
vim.o.fdc = vim.o.foldcolumn
vim.wo.foldcolumn = vim.o.foldcolumn
@@ -2479,7 +2489,7 @@ vim.wo.fmr = vim.wo.foldmarker
--- `fold-syntax` syntax Syntax highlighting items specify folds.
--- `fold-diff` diff Fold text that is not changed.
---
---- @type string
+--- @type 'manual'|'expr'|'marker'|'indent'|'syntax'|'diff'
vim.o.foldmethod = "manual"
vim.o.fdm = vim.o.foldmethod
vim.wo.foldmethod = vim.o.foldmethod
@@ -2783,6 +2793,7 @@ vim.go.gp = vim.go.grepprg
--- ci Command-line Insert mode
--- cr Command-line Replace mode
--- sm showmatch in Insert mode
+--- t Terminal mode
--- a all modes
--- The argument-list is a dash separated list of these arguments:
--- hor{N} horizontal bar, {N} percent of the character height
@@ -2802,7 +2813,8 @@ vim.go.gp = vim.go.grepprg
--- ```vim
--- set guicursor=n:blinkon0
--- ```
---- - Default is "blinkon0" for each mode.
+---
+--- Default is "blinkon0" for each mode.
--- {group-name}
--- Highlight group that decides the color and font of the
--- cursor.
@@ -2848,7 +2860,7 @@ vim.go.gp = vim.go.grepprg
---
---
--- @type string
-vim.o.guicursor = "n-v-c-sm:block,i-ci-ve:ver25,r-cr-o:hor20"
+vim.o.guicursor = "n-v-c-sm:block,i-ci-ve:ver25,r-cr-o:hor20,t:block-blinkon500-blinkoff500-TermCursor"
vim.o.gcr = vim.o.guicursor
vim.go.guicursor = vim.o.guicursor
vim.go.gcr = vim.go.guicursor
@@ -3016,7 +3028,7 @@ vim.go.hid = vim.go.hidden
--- A history of ":" commands, and a history of previous search patterns
--- is remembered. This option decides how many entries may be stored in
---- each of these histories (see `cmdline-editing` and 'msghistory' for
+--- each of these histories (see `cmdline-editing` and 'messagesopt' for
--- the number of messages to remember).
--- The maximum value is 10000.
---
@@ -3142,7 +3154,7 @@ vim.bo.ims = vim.bo.imsearch
--- 'redrawtime') then 'inccommand' is automatically disabled until
--- `Command-line-mode` is done.
---
---- @type string
+--- @type 'nosplit'|'split'|''
vim.o.inccommand = "nosplit"
vim.o.icm = vim.o.inccommand
vim.go.inccommand = vim.o.inccommand
@@ -4084,6 +4096,31 @@ vim.o.mis = vim.o.menuitems
vim.go.menuitems = vim.o.menuitems
vim.go.mis = vim.go.menuitems
+--- Option settings for outputting messages. It can consist of the
+--- following items. Items must be separated by a comma.
+---
+--- hit-enter Use a `hit-enter` prompt when the message is longer than
+--- 'cmdheight' size.
+---
+--- wait:{n} Instead of using a `hit-enter` prompt, simply wait for
+--- {n} milliseconds so that the user has a chance to read
+--- the message. The maximum value of {n} is 10000. Use
+--- 0 to disable the wait (but then the user may miss an
+--- important message).
+--- This item is ignored when "hit-enter" is present, but
+--- required when "hit-enter" is not present.
+---
+--- history:{n} Determines how many entries are remembered in the
+--- `:messages` history. The maximum value is 10000.
+--- Setting it to zero clears the message history.
+--- This item must always be present.
+---
+--- @type string
+vim.o.messagesopt = "hit-enter,history:500"
+vim.o.mopt = vim.o.messagesopt
+vim.go.messagesopt = vim.o.messagesopt
+vim.go.mopt = vim.go.messagesopt
+
--- Parameters for `:mkspell`. This tunes when to start compressing the
--- word tree. Compression can be slow when there are many words, but
--- it's needed to avoid running out of memory. The amount of memory used
@@ -4327,7 +4364,7 @@ vim.go.mh = vim.go.mousehide
--- "g<LeftMouse>" is "<C-LeftMouse> (jump to tag under mouse click)
--- "g<RightMouse>" is "<C-RightMouse> ("CTRL-T")
---
---- @type string
+--- @type 'extend'|'popup'|'popup_setpos'
vim.o.mousemodel = "popup_setpos"
vim.o.mousem = vim.o.mousemodel
vim.go.mousemodel = vim.o.mousemodel
@@ -4379,15 +4416,6 @@ vim.o.mouset = vim.o.mousetime
vim.go.mousetime = vim.o.mousetime
vim.go.mouset = vim.go.mousetime
---- Determines how many entries are remembered in the `:messages` history.
---- The maximum value is 10000.
----
---- @type integer
-vim.o.msghistory = 500
-vim.o.mhi = vim.o.msghistory
-vim.go.msghistory = vim.o.msghistory
-vim.go.mhi = vim.go.msghistory
-
--- This defines what bases Vim will consider for numbers when using the
--- CTRL-A and CTRL-X commands for adding to and subtracting from a number
--- respectively; see `CTRL-A` for more info on these commands.
@@ -4827,8 +4855,8 @@ vim.go.redrawdebug = vim.o.redrawdebug
vim.go.rdb = vim.go.redrawdebug
--- Time in milliseconds for redrawing the display. Applies to
---- 'hlsearch', 'inccommand', `:match` highlighting and syntax
---- highlighting.
+--- 'hlsearch', 'inccommand', `:match` highlighting, syntax highlighting,
+--- and async `LanguageTree:parse()`.
--- When redrawing takes more than this many milliseconds no further
--- matches will be highlighted.
--- For syntax highlighting the time applies per window. When over the
@@ -4994,6 +5022,7 @@ vim.go.ruf = vim.go.rulerformat
--- indent/ indent scripts `indent-expression`
--- keymap/ key mapping files `mbyte-keymap`
--- lang/ menu translations `:menutrans`
+--- lsp/ LSP client configurations `lsp-config`
--- lua/ `Lua` plugins
--- menu.vim GUI menus `menu.vim`
--- pack/ packages `:packadd`
@@ -5197,11 +5226,13 @@ vim.go.sect = vim.go.sections
--- selection.
--- When "old" is used and 'virtualedit' allows the cursor to move past
--- the end of line the line break still isn't included.
+--- When "exclusive" is used, cursor position in visual mode will be
+--- adjusted for inclusive motions `inclusive-motion-selection-exclusive`.
--- Note that when "exclusive" is used and selecting from the end
--- backwards, you cannot include the last character of a line, when
--- starting in Normal mode and 'virtualedit' empty.
---
---- @type string
+--- @type 'inclusive'|'exclusive'|'old'
vim.o.selection = "inclusive"
vim.o.sel = vim.o.selection
vim.go.selection = vim.o.selection
@@ -5767,7 +5798,7 @@ vim.go.sc = vim.go.showcmd
--- place the text. Without a custom 'statusline' or 'tabline' it will be
--- displayed in a convenient location.
---
---- @type string
+--- @type 'last'|'statusline'|'tabline'
vim.o.showcmdloc = "last"
vim.o.sloc = vim.o.showcmdloc
vim.go.showcmdloc = vim.o.showcmdloc
@@ -5899,7 +5930,7 @@ vim.go.siso = vim.go.sidescrolloff
--- "number" display signs in the 'number' column. If the number
--- column is not present, then behaves like "auto".
---
---- @type string
+--- @type 'yes'|'no'|'auto'|'auto:1'|'auto:2'|'auto:3'|'auto:4'|'auto:5'|'auto:6'|'auto:7'|'auto:8'|'auto:9'|'yes:1'|'yes:2'|'yes:3'|'yes:4'|'yes:5'|'yes:6'|'yes:7'|'yes:8'|'yes:9'|'number'
vim.o.signcolumn = "auto"
vim.o.scl = vim.o.signcolumn
vim.wo.signcolumn = vim.o.signcolumn
@@ -6207,7 +6238,7 @@ vim.go.sb = vim.go.splitbelow
--- with the previous cursor position. For "screen", the text cannot always
--- be kept on the same screen line when 'wrap' is enabled.
---
---- @type string
+--- @type 'cursor'|'screen'|'topline'
vim.o.splitkeep = "cursor"
vim.o.spk = vim.o.splitkeep
vim.go.splitkeep = vim.o.splitkeep
@@ -6310,6 +6341,7 @@ vim.wo.stc = vim.wo.statuscolumn
--- All fields except the {item} are optional. A single percent sign can
--- be given as "%%".
---
+--- *stl-%!*
--- When the option starts with "%!" then it is used as an expression,
--- evaluated and the result is used as the option value. Example:
---
@@ -6854,7 +6886,7 @@ vim.go.tbs = vim.go.tagbsearch
--- match Match case
--- smart Ignore case unless an upper case letter is used
---
---- @type string
+--- @type 'followic'|'ignore'|'match'|'followscs'|'smart'
vim.o.tagcase = "followic"
vim.o.tc = vim.o.tagcase
vim.bo.tagcase = vim.o.tagcase
@@ -7122,6 +7154,13 @@ vim.go.titleold = vim.o.titleold
--- expanded according to the rules used for 'statusline'. If it contains
--- an invalid '%' format, the value is used as-is and no error or warning
--- will be given when the value is set.
+---
+--- The default behaviour is equivalent to:
+---
+--- ```vim
+--- set titlestring=%t%(\ %M%)%(\ \(%{expand(\"%:~:h\")}\)%)%a\ -\ Nvim
+--- ```
+---
--- This option cannot be set in a modeline when 'modelineexpr' is off.
---
--- Example:
@@ -7729,7 +7768,7 @@ vim.go.wop = vim.go.wildoptions
--- key is never used for the menu.
--- This option is not used for <F10>; on Win32.
---
---- @type string
+--- @type 'yes'|'menu'|'no'
vim.o.winaltkeys = "menu"
vim.o.wak = vim.o.winaltkeys
vim.go.winaltkeys = vim.o.winaltkeys
diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua
index 5eb15e1eee..098c0e907a 100644
--- a/runtime/lua/vim/_meta/vimfn.lua
+++ b/runtime/lua/vim/_meta/vimfn.lua
@@ -1023,16 +1023,22 @@ function vim.fn.complete_check() end
--- See |complete_info_mode| for the values.
--- pum_visible |TRUE| if popup menu is visible.
--- See |pumvisible()|.
---- items List of completion matches. Each item is a
---- dictionary containing the entries "word",
+--- items List of all completion candidates. Each item
+--- is a dictionary containing the entries "word",
--- "abbr", "menu", "kind", "info" and "user_data".
--- See |complete-items|.
+--- matches Same as "items", but only returns items that
+--- are matching current query. If both "matches"
+--- and "items" are in "what", the returned list
+--- will still be named "items", but each item
+--- will have an additional "match" field.
--- selected Selected item index. First index is zero.
--- Index is -1 if no item is selected (showing
--- typed text only, or the last completion after
--- no item is selected when using the <Up> or
--- <Down> keys)
---- inserted Inserted string. [NOT IMPLEMENTED YET]
+--- completed Return a dictionary containing the entries of
+--- the currently selected index item.
--- preview_winid Info floating preview window id.
--- preview_bufnr Info floating preview buffer id.
---
@@ -1147,8 +1153,9 @@ function vim.fn.confirm(msg, choices, default, type) end
--- A |Dictionary| is copied in a similar way as a |List|.
--- Also see |deepcopy()|.
---
---- @param expr any
---- @return any
+--- @generic T
+--- @param expr T
+--- @return T
function vim.fn.copy(expr) end
--- Return the cosine of {expr}, measured in radians, as a |Float|.
@@ -1228,7 +1235,7 @@ function vim.fn.ctxpush(types) end
---
--- @param context table
--- @param index? integer
---- @return any
+--- @return integer
function vim.fn.ctxset(context, index) end
--- Returns the size of the |context-stack|.
@@ -1308,9 +1315,10 @@ function vim.fn.debugbreak(pid) end
--- {noref} set to 1 will fail.
--- Also see |copy()|.
---
---- @param expr any
+--- @generic T
+--- @param expr T
--- @param noref? boolean
---- @return any
+--- @return T
function vim.fn.deepcopy(expr, noref) end
--- Without {flags} or with {flags} empty: Deletes the file by the
@@ -1421,7 +1429,7 @@ function vim.fn.dictwatcherdel(dict, pattern, callback) end
--- editing another buffer to set 'filetype' and load a syntax
--- file.
---
---- @return any
+--- @return integer
function vim.fn.did_filetype() end
--- Returns the number of filler lines above line {lnum}.
@@ -1433,7 +1441,7 @@ function vim.fn.did_filetype() end
--- Returns 0 if the current window is not in diff mode.
---
--- @param lnum integer
---- @return any
+--- @return integer
function vim.fn.diff_filler(lnum) end
--- Returns the highlight ID for diff mode at line {lnum} column
@@ -1468,7 +1476,7 @@ function vim.fn.diff_hlID(lnum, col) end
--- <
---
--- @param chars string
---- @return any
+--- @return string
function vim.fn.digraph_get(chars) end
--- Return a list of digraphs. If the {listall} argument is given
@@ -1486,7 +1494,7 @@ function vim.fn.digraph_get(chars) end
--- <
---
--- @param listall? boolean
---- @return any
+--- @return string[][]
function vim.fn.digraph_getlist(listall) end
--- Add digraph {chars} to the list. {chars} must be a string
@@ -1538,7 +1546,7 @@ function vim.fn.digraph_setlist(digraphlist) end
--- - A |Blob| is empty when its length is zero.
---
--- @param expr any
---- @return any
+--- @return integer
function vim.fn.empty(expr) end
--- Return all of environment variables as dictionary. You can
@@ -1561,7 +1569,7 @@ function vim.fn.environ() end
---
--- @param string string
--- @param chars string
---- @return any
+--- @return string
function vim.fn.escape(string, chars) end
--- Evaluate {string} and return the result. Especially useful to
@@ -2368,7 +2376,7 @@ function vim.fn.foldtextresult(lnum) end
---
--- @param expr1 string|table
--- @param expr2 string|function
---- @return any
+--- @return string|table
function vim.fn.foreach(expr1, expr2) end
--- Get the full command name from a short abbreviated command
@@ -2675,7 +2683,7 @@ function vim.fn.getbufinfo(dict) end
--- @param buf integer|string
--- @param lnum integer
--- @param end_? integer
---- @return any
+--- @return string[]
function vim.fn.getbufline(buf, lnum, end_) end
--- Just like `getbufline()` but only get one line and return it
@@ -2740,12 +2748,14 @@ function vim.fn.getcellwidths() end
function vim.fn.getchangelist(buf) end
--- Get a single character from the user or input stream.
---- If {expr} is omitted, wait until a character is available.
+--- If {expr} is omitted or is -1, wait until a character is
+--- available.
--- If {expr} is 0, only get a character when one is available.
--- Return zero otherwise.
--- If {expr} is 1, only check if a character is available, it is
--- not consumed. Return zero if no character available.
---- If you prefer always getting a string use |getcharstr()|.
+--- If you prefer always getting a string use |getcharstr()|, or
+--- specify |FALSE| as "number" in {opts}.
---
--- Without {expr} and when {expr} is 0 a whole character or
--- special key is returned. If it is a single character, the
@@ -2755,7 +2765,8 @@ function vim.fn.getchangelist(buf) end
--- starting with 0x80 (decimal: 128). This is the same value as
--- the String "\<Key>", e.g., "\<Left>". The returned value is
--- also a String when a modifier (shift, control, alt) was used
---- that is not included in the character.
+--- that is not included in the character. |keytrans()| can also
+--- be used to convert a returned String into a readable form.
---
--- When {expr} is 0 and Esc is typed, there will be a short delay
--- while Vim waits to see if this is the start of an escape
@@ -2767,6 +2778,32 @@ function vim.fn.getchangelist(buf) end
---
--- Use getcharmod() to obtain any additional modifiers.
---
+--- The optional argument {opts} is a Dict and supports the
+--- following items:
+---
+--- cursor A String specifying cursor behavior
+--- when waiting for a character.
+--- "hide": hide the cursor.
+--- "keep": keep current cursor unchanged.
+--- "msg": move cursor to message area.
+--- (default: automagically decide
+--- between "keep" and "msg")
+---
+--- number If |TRUE|, return a Number when getting
+--- a single character.
+--- If |FALSE|, the return value is always
+--- converted to a String, and an empty
+--- String (instead of 0) is returned when
+--- no character is available.
+--- (default: |TRUE|)
+---
+--- simplify If |TRUE|, include modifiers in the
+--- character if possible. E.g., return
+--- the same value for CTRL-I and <Tab>.
+--- If |FALSE|, don't include modifiers in
+--- the character.
+--- (default: |TRUE|)
+---
--- When the user clicks a mouse button, the mouse event will be
--- returned. The position can then be found in |v:mouse_col|,
--- |v:mouse_lnum|, |v:mouse_winid| and |v:mouse_win|.
@@ -2803,9 +2840,10 @@ function vim.fn.getchangelist(buf) end
--- endfunction
--- <
---
---- @param expr? 0|1
---- @return integer
-function vim.fn.getchar(expr) end
+--- @param expr? -1|0|1
+--- @param opts? table
+--- @return integer|string
+function vim.fn.getchar(expr, opts) end
--- The result is a Number which is the state of the modifiers for
--- the last obtained character with getchar() or in another way.
@@ -2864,20 +2902,13 @@ function vim.fn.getcharpos(expr) end
--- @return table
function vim.fn.getcharsearch() end
---- Get a single character from the user or input stream as a
---- string.
---- If {expr} is omitted, wait until a character is available.
---- If {expr} is 0 or false, only get a character when one is
---- available. Return an empty string otherwise.
---- If {expr} is 1 or true, only check if a character is
---- available, it is not consumed. Return an empty string
---- if no character is available.
---- Otherwise this works like |getchar()|, except that a number
---- result is converted to a string.
----
---- @param expr? 0|1
+--- The same as |getchar()|, except that this always returns a
+--- String, and "number" isn't allowed in {opts}.
+---
+--- @param expr? -1|0|1
+--- @param opts? table
--- @return string
-function vim.fn.getcharstr(expr) end
+function vim.fn.getcharstr(expr, opts) end
--- Return completion pattern of the current command-line.
--- Only works when the command line is being edited, thus
@@ -2943,7 +2974,7 @@ function vim.fn.getcmdprompt() end
--- Also see |getcmdpos()|, |setcmdpos()|, |getcmdline()| and
--- |setcmdline()|.
---
---- @return any
+--- @return integer
function vim.fn.getcmdscreenpos() end
--- Return the current command-line type. Possible return values
@@ -3763,6 +3794,20 @@ function vim.fn.getregtype(regname) end
--- @return vim.fn.getscriptinfo.ret[]
function vim.fn.getscriptinfo(opts) end
+--- Returns the current stack trace of Vim scripts.
+--- Stack trace is a |List|, of which each item is a |Dictionary|
+--- with the following items:
+--- funcref The funcref if the stack is at a function,
+--- otherwise this item is omitted.
+--- event The string of the event description if the
+--- stack is at an autocmd event, otherwise this
+--- item is omitted.
+--- lnum The line number in the script on the stack.
+--- filepath The file path of the script on the stack.
+---
+--- @return table[]
+function vim.fn.getstacktrace() end
+
--- If {tabnr} is not specified, then information about all the
--- tab pages is returned as a |List|. Each List item is a
--- |Dictionary|. Otherwise, {tabnr} specifies the tab page
@@ -3869,7 +3914,7 @@ function vim.fn.gettagstack(winnr) end
--- strings.
---
--- @param text string
---- @return any
+--- @return string
function vim.fn.gettext(text) end
--- Returns information about windows as a |List| with Dictionaries.
@@ -3885,6 +3930,8 @@ function vim.fn.gettext(text) end
--- botline last complete displayed buffer line
--- bufnr number of buffer in the window
--- height window height (excluding winbar)
+--- leftcol first column displayed; only used when
+--- 'wrap' is off
--- loclist 1 if showing a location list
--- quickfix 1 if quickfix or location list window
--- terminal 1 if a terminal window
@@ -4018,7 +4065,7 @@ function vim.fn.glob(expr, nosuf, list, alllinks) end
--- a backslash usually means a path separator.
---
--- @param string string
---- @return any
+--- @return string
function vim.fn.glob2regpat(string) end
--- Perform glob() for String {expr} on all directories in {path}
@@ -4352,7 +4399,7 @@ function vim.fn.hostname() end
--- @param string string
--- @param from string
--- @param to string
---- @return any
+--- @return string
function vim.fn.iconv(string, from, to) end
--- Returns a |String| which is a unique identifier of the
@@ -4372,7 +4419,7 @@ function vim.fn.iconv(string, from, to) end
--- reuse identifiers of the garbage-collected ones.
---
--- @param expr any
---- @return any
+--- @return string
function vim.fn.id(expr) end
--- The result is a Number, which is indent of line {lnum} in the
@@ -4416,7 +4463,7 @@ function vim.fn.indent(lnum) end
--- @param expr any
--- @param start? integer
--- @param ic? boolean
---- @return any
+--- @return integer
function vim.fn.index(object, expr, start, ic) end
--- Returns the index of an item in {object} where {expr} is
@@ -4460,14 +4507,14 @@ function vim.fn.index(object, expr, start, ic) end
--- @param object any
--- @param expr any
--- @param opts? table
---- @return any
+--- @return integer
function vim.fn.indexof(object, expr, opts) end
---
--- @param prompt string
--- @param text? string
--- @param completion? string
---- @return any
+--- @return string
function vim.fn.input(prompt, text, completion) end
--- The result is a String, which is whatever the user typed on
@@ -4581,7 +4628,7 @@ function vim.fn.input(prompt, text, completion) end
--- <
---
--- @param opts table
---- @return any
+--- @return string
function vim.fn.input(opts) end
--- @deprecated
@@ -4616,7 +4663,7 @@ function vim.fn.inputlist(textlist) end
--- called. Calling it more often is harmless though.
--- Returns TRUE when there is nothing to restore, FALSE otherwise.
---
---- @return any
+--- @return integer
function vim.fn.inputrestore() end
--- Preserve typeahead (also from mappings) and clear it, so that
@@ -4626,7 +4673,7 @@ function vim.fn.inputrestore() end
--- many inputrestore() calls.
--- Returns TRUE when out of memory, FALSE otherwise.
---
---- @return any
+--- @return integer
function vim.fn.inputsave() end
--- This function acts much like the |input()| function with but
@@ -4641,7 +4688,7 @@ function vim.fn.inputsave() end
---
--- @param prompt string
--- @param text? string
---- @return any
+--- @return string
function vim.fn.inputsecret(prompt, text) end
--- When {object} is a |List| or a |Blob| insert {item} at the start
@@ -4687,8 +4734,8 @@ function vim.fn.interrupt() end
--- let bits = invert(bits)
--- <
---
---- @param expr number
---- @return any
+--- @param expr integer
+--- @return integer
function vim.fn.invert(expr) end
--- The result is a Number, which is |TRUE| when {path} is an
@@ -4767,7 +4814,7 @@ function vim.fn.isnan(expr) end
--- cases, items() returns a List with the index and the value at
--- the index.
---
---- @param dict any
+--- @param dict table
--- @return any
function vim.fn.items(dict) end
@@ -4801,7 +4848,7 @@ function vim.fn.jobresize(job, width, height) end
--- @return any
function vim.fn.jobsend(...) end
---- Note: Prefer |vim.system()| in Lua (unless using the `pty` option).
+--- Note: Prefer |vim.system()| in Lua (unless using `rpc`, `pty`, or `term`).
---
--- Spawns {cmd} as a job.
--- If {cmd} is a List it runs directly (no 'shell').
@@ -4809,8 +4856,11 @@ function vim.fn.jobsend(...) end
--- call jobstart(split(&shell) + split(&shellcmdflag) + ['{cmd}'])
--- <(See |shell-unquoting| for details.)
---
---- Example: >vim
---- call jobstart('nvim -h', {'on_stdout':{j,d,e->append(line('.'),d)}})
+--- Example: start a job and handle its output: >vim
+--- call jobstart(['nvim', '-h'], {'on_stdout':{j,d,e->append(line('.'),d)}})
+--- <
+--- Example: start a job in a |terminal| connected to the current buffer: >vim
+--- call jobstart(['nvim', '-h'], {'term':v:true})
--- <
--- Returns |job-id| on success, 0 on invalid arguments (or job
--- table is full), -1 if {cmd}[0] or 'shell' is not executable.
@@ -4875,6 +4925,10 @@ function vim.fn.jobsend(...) end
--- stdin: (string) Either "pipe" (default) to connect the
--- job's stdin to a channel or "null" to disconnect
--- stdin.
+--- term: (boolean) Spawns {cmd} in a new pseudo-terminal session
+--- connected to the current (unmodified) buffer. Implies "pty".
+--- Default "height" and "width" are set to the current window
+--- dimensions. |jobstart()|. Defaults $TERM to "xterm-256color".
--- width: (number) Width of the `pty` terminal.
---
--- {opts} is passed as |self| dictionary to the callback; the
@@ -4888,7 +4942,7 @@ function vim.fn.jobsend(...) end
---
--- @param cmd string|string[]
--- @param opts? table
---- @return any
+--- @return integer
function vim.fn.jobstart(cmd, opts) end
--- Stop |job-id| {id} by sending SIGTERM to the job process. If
@@ -4901,7 +4955,7 @@ function vim.fn.jobstart(cmd, opts) end
--- exited or stopped.
---
--- @param id integer
---- @return any
+--- @return integer
function vim.fn.jobstop(id) end
--- Waits for jobs and their |on_exit| handlers to complete.
@@ -4926,7 +4980,7 @@ function vim.fn.jobstop(id) end
---
--- @param jobs integer[]
--- @param timeout? integer
---- @return any
+--- @return integer[]
function vim.fn.jobwait(jobs, timeout) end
--- Join the items in {list} together into one String.
@@ -4941,7 +4995,7 @@ function vim.fn.jobwait(jobs, timeout) end
---
--- @param list any[]
--- @param sep? string
---- @return any
+--- @return string
function vim.fn.join(list, sep) end
--- Convert {expr} from JSON object. Accepts |readfile()|-style
@@ -4974,14 +5028,14 @@ function vim.fn.json_decode(expr) end
--- |Blob|s are converted to arrays of the individual bytes.
---
--- @param expr any
---- @return any
+--- @return string
function vim.fn.json_encode(expr) end
--- Return a |List| with all the keys of {dict}. The |List| is in
--- arbitrary order. Also see |items()| and |values()|.
---
--- @param dict table
---- @return any
+--- @return string[]
function vim.fn.keys(dict) end
--- Turn the internal byte representation of keys into a form that
@@ -4991,7 +5045,7 @@ function vim.fn.keys(dict) end
--- < <C-Home>
---
--- @param string string
---- @return any
+--- @return string
function vim.fn.keytrans(string) end
--- @deprecated
@@ -5010,8 +5064,8 @@ function vim.fn.last_buffer_nr() end
--- |Dictionary| is returned.
--- Otherwise an error is given and returns zero.
---
---- @param expr any
---- @return any
+--- @param expr any[]
+--- @return integer
function vim.fn.len(expr) end
--- Call function {funcname} in the run-time library {libname}
@@ -5122,7 +5176,7 @@ function vim.fn.line2byte(lnum) end
--- When {lnum} is invalid, -1 is returned.
---
--- @param lnum integer
---- @return any
+--- @return integer
function vim.fn.lispindent(lnum) end
--- Return a Blob concatenating all the number values in {list}.
@@ -5135,7 +5189,7 @@ function vim.fn.lispindent(lnum) end
--- |blob2list()| does the opposite.
---
--- @param list any[]
---- @return any
+--- @return string
function vim.fn.list2blob(list) end
--- Convert each number in {list} to a character string can
@@ -5155,13 +5209,13 @@ function vim.fn.list2blob(list) end
---
--- @param list any[]
--- @param utf8? boolean
---- @return any
+--- @return string
function vim.fn.list2str(list, utf8) end
--- Return the current time, measured as seconds since 1st Jan
--- 1970. See also |strftime()|, |strptime()| and |getftime()|.
---
---- @return any
+--- @return integer
function vim.fn.localtime() end
--- Return the natural logarithm (base e) of {expr} as a |Float|.
@@ -5175,7 +5229,7 @@ function vim.fn.localtime() end
--- < 5.0
---
--- @param expr number
---- @return any
+--- @return number
function vim.fn.log(expr) end
--- Return the logarithm of Float {expr} to base 10 as a |Float|.
@@ -5188,7 +5242,7 @@ function vim.fn.log(expr) end
--- < -2.0
---
--- @param expr number
---- @return any
+--- @return number
function vim.fn.log10(expr) end
--- {expr1} must be a |List|, |String|, |Blob| or |Dictionary|.
@@ -5950,7 +6004,7 @@ function vim.fn.matchstrpos(expr, pat, start, count) end
--- an error. An empty |List| or |Dictionary| results in zero.
---
--- @param expr any
---- @return any
+--- @return number
function vim.fn.max(expr) end
--- Returns a |List| of |Dictionaries| describing |menus| (defined
@@ -6088,7 +6142,7 @@ function vim.fn.menu_info(name, mode) end
--- an error. An empty |List| or |Dictionary| results in zero.
---
--- @param expr any
---- @return any
+--- @return number
function vim.fn.min(expr) end
--- Create directory {name}.
@@ -6133,7 +6187,7 @@ function vim.fn.min(expr) end
--- @param name string
--- @param flags? string
--- @param prot? string
---- @return any
+--- @return integer
function vim.fn.mkdir(name, flags, prot) end
--- Return a string that indicates the current mode.
@@ -6296,7 +6350,7 @@ function vim.fn.msgpackparse(data) end
--- See also |prevnonblank()|.
---
--- @param lnum integer
---- @return any
+--- @return integer
function vim.fn.nextnonblank(lnum) end
--- Return a string with a single character, which has the number
@@ -6315,7 +6369,7 @@ function vim.fn.nextnonblank(lnum) end
---
--- @param expr integer
--- @param utf8? boolean
---- @return any
+--- @return string
function vim.fn.nr2char(expr, utf8) end
--- Bitwise OR on the two arguments. The arguments are converted
@@ -6349,7 +6403,7 @@ vim.fn['or'] = function(expr, expr1) end
---
--- @param path string
--- @param len? integer
---- @return any
+--- @return string
function vim.fn.pathshorten(path, len) end
--- Evaluate |perl| expression {expr} and return its result
@@ -6383,7 +6437,7 @@ function vim.fn.perleval(expr) end
---
--- @param x number
--- @param y number
---- @return any
+--- @return number
function vim.fn.pow(x, y) end
--- Return the line number of the first line at or above {lnum}
@@ -6395,7 +6449,7 @@ function vim.fn.pow(x, y) end
--- Also see |nextnonblank()|.
---
--- @param lnum integer
---- @return any
+--- @return integer
function vim.fn.prevnonblank(lnum) end
--- Return a String with {fmt}, where "%" items are replaced by
@@ -7014,10 +7068,11 @@ function vim.fn.readfile(fname, type, max) end
--- echo reduce('xyz', { acc, val -> acc .. ',' .. val })
--- <
---
+--- @generic T
--- @param object any
---- @param func function
+--- @param func fun(accumulator: T, current: any): any
--- @param initial? any
---- @return any
+--- @return T
function vim.fn.reduce(object, func, initial) end
--- Returns the single letter name of the register being executed.
@@ -7170,7 +7225,7 @@ function vim.fn.remove(dict, key) end
---
--- @param from string
--- @param to string
---- @return any
+--- @return integer
function vim.fn.rename(from, to) end
--- Repeat {expr} {count} times and return the concatenated
@@ -7200,7 +7255,7 @@ vim.fn['repeat'] = function(expr, count) end
--- path name) and also keeps a trailing path separator.
---
--- @param filename string
---- @return any
+--- @return string
function vim.fn.resolve(filename) end
--- Reverse the order of items in {object}. {object} can be a
@@ -7213,8 +7268,9 @@ function vim.fn.resolve(filename) end
--- let revlist = reverse(copy(mylist))
--- <
---
---- @param object any
---- @return any
+--- @generic T
+--- @param object T[]
+--- @return T[]
function vim.fn.reverse(object) end
--- Round off {expr} to the nearest integral value and return it
@@ -7231,7 +7287,7 @@ function vim.fn.reverse(object) end
--- < -5.0
---
--- @param expr number
---- @return any
+--- @return number
function vim.fn.round(expr) end
--- Sends {event} to {channel} via |RPC| and returns immediately.
@@ -7242,9 +7298,9 @@ function vim.fn.round(expr) end
---
--- @param channel integer
--- @param event string
---- @param args? any
---- @return any
-function vim.fn.rpcnotify(channel, event, args) end
+--- @param ... any
+--- @return integer
+function vim.fn.rpcnotify(channel, event, ...) end
--- Sends a request to {channel} to invoke {method} via
--- |RPC| and blocks until a response is received.
@@ -7254,9 +7310,9 @@ function vim.fn.rpcnotify(channel, event, args) end
---
--- @param channel integer
--- @param method string
---- @param args? any
+--- @param ... any
--- @return any
-function vim.fn.rpcrequest(channel, method, args) end
+function vim.fn.rpcrequest(channel, method, ...) end
--- @deprecated
--- Deprecated. Replace >vim
@@ -7300,7 +7356,7 @@ function vim.fn.rubyeval(expr) end
---
--- @param row integer
--- @param col integer
---- @return any
+--- @return integer
function vim.fn.screenattr(row, col) end
--- The result is a Number, which is the character at position
@@ -7314,7 +7370,7 @@ function vim.fn.screenattr(row, col) end
---
--- @param row integer
--- @param col integer
---- @return any
+--- @return integer
function vim.fn.screenchar(row, col) end
--- The result is a |List| of Numbers. The first number is the same
@@ -7325,7 +7381,7 @@ function vim.fn.screenchar(row, col) end
---
--- @param row integer
--- @param col integer
---- @return any
+--- @return integer[]
function vim.fn.screenchars(row, col) end
--- The result is a Number, which is the current screen column of
@@ -7342,7 +7398,7 @@ function vim.fn.screenchars(row, col) end
--- noremap GG <Cmd>echom screencol()<CR>
--- <
---
---- @return any
+--- @return integer[]
function vim.fn.screencol() end
--- The result is a Dict with the screen position of the text
@@ -7381,7 +7437,7 @@ function vim.fn.screenpos(winid, lnum, col) end
---
--- Note: Same restrictions as with |screencol()|.
---
---- @return any
+--- @return integer
function vim.fn.screenrow() end
--- The result is a String that contains the base character and
@@ -7393,7 +7449,7 @@ function vim.fn.screenrow() end
---
--- @param row integer
--- @param col integer
---- @return any
+--- @return string
function vim.fn.screenstring(row, col) end
--- Search for regexp pattern {pattern}. The search starts at the
@@ -7505,7 +7561,7 @@ function vim.fn.screenstring(row, col) end
--- @param stopline? integer
--- @param timeout? integer
--- @param skip? string|function
---- @return any
+--- @return integer
function vim.fn.search(pattern, flags, stopline, timeout, skip) end
--- Get or update the last search count, like what is displayed
@@ -7798,7 +7854,7 @@ function vim.fn.searchpos(pattern, flags, stopline, timeout, skip) end
--- echo serverlist()
--- <
---
---- @return any
+--- @return string[]
function vim.fn.serverlist() end
--- Opens a socket or named pipe at {address} and listens for
@@ -7835,7 +7891,7 @@ function vim.fn.serverlist() end
--- <
---
--- @param address? string
---- @return any
+--- @return string
function vim.fn.serverstart(address) end
--- Closes the pipe or socket at {address}.
@@ -7844,7 +7900,7 @@ function vim.fn.serverstart(address) end
--- address in |serverlist()|.
---
--- @param address string
---- @return any
+--- @return integer
function vim.fn.serverstop(address) end
--- Set line {lnum} to {text} in buffer {buf}. This works like
@@ -7874,7 +7930,7 @@ function vim.fn.serverstop(address) end
--- @param buf integer|string
--- @param lnum integer
--- @param text string|string[]
---- @return any
+--- @return integer
function vim.fn.setbufline(buf, lnum, text) end
--- Set option or local variable {varname} in buffer {buf} to
@@ -7979,7 +8035,7 @@ function vim.fn.setcharsearch(dict) end
---
--- @param str string
--- @param pos? integer
---- @return any
+--- @return integer
function vim.fn.setcmdline(str, pos) end
--- Set the cursor position in the command line to byte position
@@ -8289,7 +8345,7 @@ function vim.fn.setpos(expr, list) end
--- @param list vim.quickfix.entry[]
--- @param action? string
--- @param what? vim.fn.setqflist.what
---- @return any
+--- @return integer
function vim.fn.setqflist(list, action, what) end
--- Set the register {regname} to {value}.
@@ -8442,7 +8498,7 @@ function vim.fn.setwinvar(nr, varname, val) end
--- checksum of {string}.
---
--- @param string string
---- @return any
+--- @return string
function vim.fn.sha256(string) end
--- Escape {string} for use as a shell command argument.
@@ -8478,7 +8534,7 @@ function vim.fn.sha256(string) end
---
--- @param string string
--- @param special? boolean
---- @return any
+--- @return string
function vim.fn.shellescape(string, special) end
--- Returns the effective value of 'shiftwidth'. This is the
@@ -8930,7 +8986,7 @@ function vim.fn.sign_unplacelist(list) end
--- links before simplifying the path name, use |resolve()|.
---
--- @param filename string
---- @return any
+--- @return string
function vim.fn.simplify(filename) end
--- Return the sine of {expr}, measured in radians, as a |Float|.
@@ -8943,7 +8999,7 @@ function vim.fn.simplify(filename) end
--- < 0.763301
---
--- @param expr number
---- @return any
+--- @return number
function vim.fn.sin(expr) end
--- Return the hyperbolic sine of {expr} as a |Float| in the range
@@ -9077,10 +9133,11 @@ function vim.fn.sockconnect(mode, address, opts) end
--- eval mylist->sort({i1, i2 -> i1 - i2})
--- <
---
---- @param list any
+--- @generic T
+--- @param list T[]
--- @param how? string|function
--- @param dict? any
---- @return any
+--- @return T[]
function vim.fn.sort(list, how, dict) end
--- Return the sound-folded equivalent of {word}. Uses the first
@@ -9091,7 +9148,7 @@ function vim.fn.sort(list, how, dict) end
--- the method can be quite slow.
---
--- @param word string
---- @return any
+--- @return string
function vim.fn.soundfold(word) end
--- Without argument: The result is the badly spelled word under
@@ -9144,7 +9201,7 @@ function vim.fn.spellbadword(sentence) end
--- @param word string
--- @param max? integer
--- @param capital? boolean
---- @return any
+--- @return string[]
function vim.fn.spellsuggest(word, max, capital) end
--- Make a |List| out of {string}. When {pattern} is omitted or
@@ -9174,7 +9231,7 @@ function vim.fn.spellsuggest(word, max, capital) end
--- @param string string
--- @param pattern? string
--- @param keepempty? boolean
---- @return any
+--- @return string[]
function vim.fn.split(string, pattern, keepempty) end
--- Return the non-negative square root of Float {expr} as a
@@ -9326,6 +9383,7 @@ function vim.fn.str2float(string, quoted) end
--- and exists only for backwards-compatibility.
--- With UTF-8 composing characters are handled properly: >vim
--- echo str2list("á") " returns [97, 769]
+--- <
---
--- @param string string
--- @param utf8? boolean
@@ -10160,23 +10218,12 @@ function vim.fn.tanh(expr) end
--- @return string
function vim.fn.tempname() end
---- Spawns {cmd} in a new pseudo-terminal session connected
---- to the current (unmodified) buffer. Parameters and behavior
---- are the same as |jobstart()| except "pty", "width", "height",
---- and "TERM" are ignored: "height" and "width" are taken from
---- the current window. Note that termopen() implies a "pty" arg
---- to jobstart(), and thus has the implications documented at
---- |jobstart()|.
----
---- Returns the same values as jobstart().
----
---- Terminal environment is initialized as in |jobstart-env|,
---- except $TERM is set to "xterm-256color". Full behavior is
---- described in |terminal|.
+--- @deprecated
+--- Use |jobstart()| with `{term: v:true}` instead.
---
--- @param cmd string|string[]
--- @param opts? table
---- @return any
+--- @return integer
function vim.fn.termopen(cmd, opts) end
--- Return a list with information about timers.
@@ -10576,7 +10623,7 @@ function vim.fn.virtcol(expr, list, winid) end
--- @param winid integer
--- @param lnum integer
--- @param col integer
---- @return any
+--- @return integer
function vim.fn.virtcol2col(winid, lnum, col) end
--- The result is a String, which describes the last Visual mode
@@ -10597,7 +10644,7 @@ function vim.fn.virtcol2col(winid, lnum, col) end
--- the old value is returned. See |non-zero-arg|.
---
--- @param expr? boolean
---- @return any
+--- @return string
function vim.fn.visualmode(expr) end
--- Waits until {condition} evaluates to |TRUE|, where {condition}
@@ -10714,7 +10761,7 @@ function vim.fn.win_id2tabwin(expr) end
--- Return 0 if the window cannot be found in the current tabpage.
---
--- @param expr integer
---- @return any
+--- @return integer
function vim.fn.win_id2win(expr) end
--- Move window {nr}'s vertical separator (i.e., the right border)
@@ -10868,7 +10915,7 @@ function vim.fn.winheight(nr) end
--- <
---
--- @param tabnr? integer
---- @return any
+--- @return any[]
function vim.fn.winlayout(tabnr) end
--- The result is a Number, which is the screen line of the cursor
@@ -10912,7 +10959,7 @@ function vim.fn.winline() end
--- <
---
--- @param arg? string|integer
---- @return any
+--- @return integer
function vim.fn.winnr(arg) end
--- Returns a sequence of |:resize| commands that should restore
@@ -10925,7 +10972,7 @@ function vim.fn.winnr(arg) end
--- exe cmd
--- <
---
---- @return any
+--- @return string
function vim.fn.winrestcmd() end
--- Uses the |Dictionary| returned by |winsaveview()| to restore
@@ -10990,7 +11037,7 @@ function vim.fn.winsaveview() end
--- option.
---
--- @param nr integer
---- @return any
+--- @return integer
function vim.fn.winwidth(nr) end
--- The result is a dictionary of byte/chars/word statistics for
@@ -11075,7 +11122,7 @@ function vim.fn.writefile(object, fname, flags) end
--- let bits = xor(bits, 0x80)
--- <
---
---- @param expr number
---- @param expr1 number
---- @return any
+--- @param expr integer
+--- @param expr1 integer
+--- @return integer
function vim.fn.xor(expr, expr1) end
diff --git a/runtime/lua/vim/_meta/vvars.lua b/runtime/lua/vim/_meta/vvars.lua
index 8784fdbac9..c1b8695bbf 100644
--- a/runtime/lua/vim/_meta/vvars.lua
+++ b/runtime/lua/vim/_meta/vvars.lua
@@ -15,7 +15,7 @@ vim.v.argv = ...
--- Argument for evaluating 'formatexpr' and used for the typed
--- character when using <expr> in an abbreviation `:map-<expr>`.
--- It is also used by the `InsertCharPre` and `InsertEnter` events.
---- @type any
+--- @type string
vim.v.char = ...
--- The name of the character encoding of a file to be converted.
@@ -60,7 +60,7 @@ vim.v.collate = ...
--- mode.
--- Note: Plugins can modify the value to emulate the builtin
--- `CompleteDone` event behavior.
---- @type any
+--- @type vim.v.completed_item
vim.v.completed_item = ...
--- The count given for the last Normal mode command. Can be used
@@ -90,7 +90,7 @@ vim.v.count1 = ...
--- This variable can not be set directly, use the `:language`
--- command.
--- See `multi-lang`.
---- @type any
+--- @type string
vim.v.ctype = ...
--- Normally zero. When a deadly signal is caught it's set to
@@ -197,11 +197,14 @@ vim.v.errors = ...
--- changing window (or tab) on `DirChanged`.
--- status Job status or exit code, -1 means "unknown". `TermClose`
--- reason Reason for completion being done. `CompleteDone`
---- @type any
+--- complete_word The word that was selected, empty if abandoned complete.
+--- complete_type See `complete_info_mode`
+--- @type vim.v.event
vim.v.event = ...
--- The value of the exception most recently caught and not
---- finished. See also `v:throwpoint` and `throw-variables`.
+--- finished. See also `v:stacktrace`, `v:throwpoint`, and
+--- `throw-variables`.
--- Example:
---
--- ```vim
@@ -223,7 +226,7 @@ vim.v.exception = ...
--- ```vim
--- :au VimLeave * echo "Exit value is " .. v:exiting
--- ```
---- @type any
+--- @type integer?
vim.v.exiting = ...
--- Special value used to put "false" in JSON and msgpack. See
@@ -419,7 +422,7 @@ vim.v.mouse_winid = ...
--- and `msgpackdump()`. All types inside dictionary are fixed
--- (not editable) empty lists. To check whether some list is one
--- of msgpack types, use `is` operator.
---- @type any
+--- @type table
vim.v.msgpack_types = ...
--- Special value used to put "null" in JSON and NIL in msgpack.
@@ -563,7 +566,7 @@ vim.v.relnum = ...
--- typed command.
--- This can be used to find out why your script causes the
--- hit-enter prompt.
---- @type any
+--- @type string
vim.v.scrollstart = ...
--- Search direction: 1 after a forward search, 0 after a
@@ -614,6 +617,13 @@ vim.v.servername = ...
--- @type integer
vim.v.shell_error = ...
+--- The stack trace of the exception most recently caught and
+--- not finished. Refer to `getstacktrace()` for the structure of
+--- stack trace. See also `v:exception`, `v:throwpoint`, and
+--- `throw-variables`.
+--- @type table[]
+vim.v.stacktrace = ...
+
--- Last given status message.
--- Modifiable (can be set).
--- @type string
@@ -705,18 +715,18 @@ vim.v.termrequest = ...
vim.v.termresponse = ...
--- Must be set before using `test_garbagecollect_now()`.
---- @type any
+--- @type integer
vim.v.testing = ...
--- Full filename of the last loaded or saved session file.
--- Empty when no session file has been saved. See `:mksession`.
--- Modifiable (can be set).
---- @type any
+--- @type string
vim.v.this_session = ...
--- The point where the exception most recently caught and not
--- finished was thrown. Not set when commands are typed. See
---- also `v:exception` and `throw-variables`.
+--- also `v:exception`, `v:stacktrace`, and `throw-variables`.
--- Example:
---
--- ```vim
@@ -728,7 +738,7 @@ vim.v.this_session = ...
--- ```
---
--- Output: "Exception from test.vim, line 2"
---- @type any
+--- @type string
vim.v.throwpoint = ...
--- Special value used to put "true" in JSON and msgpack. See
diff --git a/runtime/lua/vim/_meta/vvars_extra.lua b/runtime/lua/vim/_meta/vvars_extra.lua
new file mode 100644
index 0000000000..7ef3021e89
--- /dev/null
+++ b/runtime/lua/vim/_meta/vvars_extra.lua
@@ -0,0 +1,77 @@
+--- @meta _
+error('Cannot require a meta file')
+
+--- Extra types for vim.v dictionary fields
+
+--- @class vim.v.completed_item
+--- @field word? string the text that will be inserted, mandatory
+--- abbreviation of "word"; when not empty it is used in the menu instead of "word"
+--- @field abbr? string
+--- extra text for the popup menu, displayed after "word" or "abbr"
+--- @field menu? string
+--- more information about the item, can be displayed in a preview window
+--- @field info? string
+--- @field kind? string single letter indicating the type of completion
+--- when non-zero case is to be ignored when comparing items to be equal; when
+--- omitted zero is used, thus items that only differ in case are added
+--- @field icase? integer
+--- when non-zero, always treat this item to be equal when comparing. Which
+--- means, "equal=1" disables filtering of this item.
+--- @field equal? integer
+--- when non-zero this match will be added even when an item with the same word
+--- is already present.
+--- @field dup? integer
+--- when non-zero this match will be added even when it is an empty string
+--- @field empty? integer
+--- custom data which is associated with the item and available
+--- in |v:completed_item|; it can be any type; defaults to an empty string
+--- @field user_data? any
+--- an additional highlight group whose attributes are combined
+--- with |hl-PmenuSel| and |hl-Pmenu| or |hl-PmenuMatchSel| and |hl-PmenuMatch|
+--- highlight attributes in the popup menu to apply cterm and gui properties
+--- (with higher priority) like strikethrough to the completion items abbreviation
+--- @field abbr_hlgroup? string
+--- an additional highlight group specifically for setting the highlight
+--- attributes of the completion kind. When this field is present, it will
+--- override the |hl-PmenuKind| highlight group, allowing for the customization
+--- of ctermfg and guifg properties for the completion kind
+--- @field kind_hlgroup? string
+
+--- @class vim.v.event
+--- Whether the event triggered during an aborting condition (e.g. |c_Esc| or
+--- |c_CTRL-C| for |CmdlineLeave|).
+--- @field abort? boolean
+--- @field chan? integer See |channel-id|
+--- @field info? table Dict of arbitrary event data.
+--- @field cmdlevel? integer Level of cmdline.
+--- @field cmdtype? string Type of cmdline, |cmdline-char|.
+--- @field cwd? string Current working directory.
+--- @field inclusive? boolean Motion is |inclusive|, else exclusive.
+--- @field scope? string Event-specific scope name.
+--- Current |operator|. Also set for Ex commands (unlike |v:operator|). For
+--- example if |TextYankPost| is triggered by the |:yank| Ex command then
+--- `v:event.operator` is "y".
+--- @field operator? string
+--- Text stored in the register as a |readfile()|-style list of lines.
+--- @field regcontents? string
+--- Requested register (e.g "x" for "xyy) or the empty string for an unnamed operation.
+--- @field regname? string
+--- @field regtype? string Type of register as returned by |getregtype()|.
+--- @field visual? boolean Selection is visual (as opposed to, e.g., via motion).
+--- @field completed_item? vim.v.completed_item
+--- Current selected complete item on |CompleteChanged|, Is `{}` when no
+--- complete item selected.
+--- @field height? integer
+--- @field width? integer Height of popup menu on |CompleteChanged|
+--- @field row? integer Width of popup menu on |CompleteChanged|
+--- Col count of popup menu on |CompleteChanged|, relative to screen.
+--- @field col? integer
+--- @field size? integer Total number of completion items on |CompleteChanged|.
+--- Is |v:true| if popup menu have scrollbar, or |v:false| if not.
+--- @field scrollbar? boolean
+--- Is |v:true| if the event fired while changing window (or tab) on |DirChanged|.
+--- @field changed_window? boolean
+--- @field status? boolean Job status or exit code, -1 means "unknown". |TermClose|
+--- @field reason? string Reason for completion being done. |CompleteDone|
+--- The word that was selected, empty if abandoned complete. @field complete_word? string
+--- @field complete_type? string See |complete_info_mode|
diff --git a/runtime/lua/vim/_options.lua b/runtime/lua/vim/_options.lua
index 77d7054626..973ad87ee8 100644
--- a/runtime/lua/vim/_options.lua
+++ b/runtime/lua/vim/_options.lua
@@ -229,10 +229,8 @@ end
--- global value of a |global-local| option, see |:setglobal|.
--- </pre>
---- Get or set |options|. Like `:set`. Invalid key is an error.
----
---- Note: this works on both buffer-scoped and window-scoped options using the
---- current buffer and window.
+--- Get or set |options|. Works like `:set`, so buffer/window-scoped options target the current
+--- buffer/window. Invalid key is an error.
---
--- Example:
---
@@ -690,6 +688,7 @@ local function remove_value(info, current, new)
end
local function create_option_accessor(scope)
+ --- @diagnostic disable-next-line: no-unknown
local option_mt
local function make_option(name, value)
@@ -698,6 +697,7 @@ local function create_option_accessor(scope)
if type(value) == 'table' and getmetatable(value) == option_mt then
assert(name == value._name, "must be the same value, otherwise that's weird.")
+ --- @diagnostic disable-next-line: no-unknown
value = value._value
end
@@ -721,6 +721,7 @@ local function create_option_accessor(scope)
end,
append = function(self, right)
+ --- @diagnostic disable-next-line: no-unknown
self._value = add_value(self._info, self._value, right)
self:_set()
end,
@@ -730,6 +731,7 @@ local function create_option_accessor(scope)
end,
prepend = function(self, right)
+ --- @diagnostic disable-next-line: no-unknown
self._value = prepend_value(self._info, self._value, right)
self:_set()
end,
@@ -739,6 +741,7 @@ local function create_option_accessor(scope)
end,
remove = function(self, right)
+ --- @diagnostic disable-next-line: no-unknown
self._value = remove_value(self._info, self._value, right)
self:_set()
end,
@@ -770,7 +773,7 @@ end
---
---
--- A special interface |vim.opt| exists for conveniently interacting with list-
---- and map-style option from Lua: It allows accessing them as Lua tables and
+--- and map-style options from Lua: It allows accessing them as Lua tables and
--- offers object-oriented method for adding and removing entries.
---
--- Examples: ~
diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua
index ce5dbffeaa..157172447a 100644
--- a/runtime/lua/vim/_system.lua
+++ b/runtime/lua/vim/_system.lua
@@ -47,15 +47,6 @@ local function close_handle(handle)
end
end
----@param state vim.SystemState
-local function close_handles(state)
- close_handle(state.handle)
- close_handle(state.stdin)
- close_handle(state.stdout)
- close_handle(state.stderr)
- close_handle(state.timer)
-end
-
--- @class vim.SystemObj
--- @field cmd string[]
--- @field pid integer
@@ -88,7 +79,8 @@ function SystemObj:_timeout(signal)
self:kill(signal or SIG.TERM)
end
-local MAX_TIMEOUT = 2 ^ 31
+-- Use max 32-bit signed int value to avoid overflow on 32-bit systems. #31633
+local MAX_TIMEOUT = 2 ^ 31 - 1
--- @param timeout? integer
--- @return vim.SystemCompleted
@@ -132,9 +124,7 @@ function SystemObj:write(data)
-- (https://github.com/neovim/neovim/pull/17620#discussion_r820775616)
stdin:write('', function()
stdin:shutdown(function()
- if stdin then
- stdin:close()
- end
+ close_handle(stdin)
end)
end)
end
@@ -146,25 +136,52 @@ function SystemObj:is_closing()
return handle == nil or handle:is_closing() or false
end
----@param output fun(err:string?, data: string?)|false
----@return uv.uv_stream_t?
----@return fun(err:string?, data: string?)? Handler
-local function setup_output(output)
- if output == nil then
- return assert(uv.new_pipe(false)), nil
+--- @param output? uv.read_start.callback|false
+--- @param text? boolean
+--- @return uv.uv_stream_t? pipe
+--- @return uv.read_start.callback? handler
+--- @return string[]? data
+local function setup_output(output, text)
+ if output == false then
+ return
end
+ local bucket --- @type string[]?
+ local handler --- @type uv.read_start.callback
+
if type(output) == 'function' then
- return assert(uv.new_pipe(false)), output
+ handler = output
+ else
+ bucket = {}
+ handler = function(err, data)
+ if err then
+ error(err)
+ end
+ if text and data then
+ bucket[#bucket + 1] = data:gsub('\r\n', '\n')
+ else
+ bucket[#bucket + 1] = data
+ end
+ end
end
- assert(output == false)
- return nil, nil
+ local pipe = assert(uv.new_pipe(false))
+
+ --- @type uv.read_start.callback
+ local function handler_with_close(err, data)
+ handler(err, data)
+ if data == nil then
+ pipe:read_stop()
+ pipe:close()
+ end
+ end
+
+ return pipe, handler_with_close, bucket
end
----@param input string|string[]|true|nil
----@return uv.uv_stream_t?
----@return string|string[]?
+--- @param input? string|string[]|boolean
+--- @return uv.uv_stream_t?
+--- @return string|string[]?
local function setup_input(input)
if not input then
return
@@ -208,28 +225,6 @@ local function setup_env(env, clear_env)
return renv
end
---- @param stream uv.uv_stream_t
---- @param text? boolean
---- @param bucket string[]
---- @return fun(err: string?, data: string?)
-local function default_handler(stream, text, bucket)
- return function(err, data)
- if err then
- error(err)
- end
- if data ~= nil then
- if text then
- bucket[#bucket + 1] = data:gsub('\r\n', '\n')
- else
- bucket[#bucket + 1] = data
- end
- else
- stream:read_stop()
- stream:close()
- end
- end
-end
-
local is_win = vim.fn.has('win32') == 1
local M = {}
@@ -255,9 +250,9 @@ local function spawn(cmd, opts, on_exit, on_error)
return handle, pid_or_err --[[@as integer]]
end
----@param timeout integer
----@param cb fun()
----@return uv.uv_timer_t
+--- @param timeout integer
+--- @param cb fun()
+--- @return uv.uv_timer_t
local function timer_oneshot(timeout, cb)
local timer = assert(uv.new_timer())
timer:start(timeout, 0, function()
@@ -273,7 +268,12 @@ end
--- @param signal integer
--- @param on_exit fun(result: vim.SystemCompleted)?
local function _on_exit(state, code, signal, on_exit)
- close_handles(state)
+ close_handle(state.handle)
+ close_handle(state.stdin)
+ close_handle(state.timer)
+
+ -- #30846: Do not close stdout/stderr here, as they may still have data to
+ -- read. They will be closed in uv.read_start on EOF.
local check = assert(uv.new_check())
check:start(function()
@@ -311,6 +311,15 @@ local function _on_exit(state, code, signal, on_exit)
end)
end
+--- @param state vim.SystemState
+local function _on_error(state)
+ close_handle(state.handle)
+ close_handle(state.stdin)
+ close_handle(state.stdout)
+ close_handle(state.stderr)
+ close_handle(state.timer)
+end
+
--- Run a system command
---
--- @param cmd string[]
@@ -324,8 +333,8 @@ function M.run(cmd, opts, on_exit)
opts = opts or {}
- local stdout, stdout_handler = setup_output(opts.stdout)
- local stderr, stderr_handler = setup_output(opts.stderr)
+ local stdout, stdout_handler, stdout_data = setup_output(opts.stdout, opts.text)
+ local stderr, stderr_handler, stderr_data = setup_output(opts.stderr, opts.text)
local stdin, towrite = setup_input(opts.stdin)
--- @type vim.SystemState
@@ -335,7 +344,9 @@ function M.run(cmd, opts, on_exit)
timeout = opts.timeout,
stdin = stdin,
stdout = stdout,
+ stdout_data = stdout_data,
stderr = stderr,
+ stderr_data = stderr_data,
}
--- @diagnostic disable-next-line:missing-fields
@@ -350,17 +361,15 @@ function M.run(cmd, opts, on_exit)
}, function(code, signal)
_on_exit(state, code, signal, on_exit)
end, function()
- close_handles(state)
+ _on_error(state)
end)
- if stdout then
- state.stdout_data = {}
- stdout:read_start(stdout_handler or default_handler(stdout, opts.text, state.stdout_data))
+ if stdout and stdout_handler then
+ stdout:read_start(stdout_handler)
end
- if stderr then
- state.stderr_data = {}
- stderr:read_start(stderr_handler or default_handler(stderr, opts.text, state.stderr_data))
+ if stderr and stderr_handler then
+ stderr:read_start(stderr_handler)
end
local obj = new_systemobj(state)
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
index 4fb8c6a686..621945aedd 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -2,6 +2,20 @@ local api, if_nil = vim.api, vim.F.if_nil
local M = {}
+--- @param title string
+--- @return integer?
+local function get_qf_id_for_title(title)
+ local lastqflist = vim.fn.getqflist({ nr = '$' })
+ for i = 1, lastqflist.nr do
+ local qflist = vim.fn.getqflist({ nr = i, id = 0, title = 0 })
+ if qflist.title == title then
+ return qflist.id
+ end
+ end
+
+ return nil
+end
+
--- [diagnostic-structure]()
---
--- Diagnostics use the same indexing as the rest of the Nvim API (i.e. 0-based
@@ -56,9 +70,13 @@ local M = {}
--- Use virtual text for diagnostics. If multiple diagnostics are set for a
--- namespace, one prefix per diagnostic + the last diagnostic message are
--- shown.
---- (default: `true`)
+--- (default: `false`)
--- @field virtual_text? boolean|vim.diagnostic.Opts.VirtualText|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.VirtualText
---
+--- Use virtual lines for diagnostics.
+--- (default: `false`)
+--- @field virtual_lines? boolean|vim.diagnostic.Opts.VirtualLines|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.VirtualLines
+---
--- Use signs for diagnostics |diagnostic-signs|.
--- (default: `true`)
--- @field signs? boolean|vim.diagnostic.Opts.Signs|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.Signs
@@ -87,6 +105,7 @@ local M = {}
--- @field update_in_insert boolean
--- @field underline vim.diagnostic.Opts.Underline
--- @field virtual_text vim.diagnostic.Opts.VirtualText
+--- @field virtual_lines vim.diagnostic.Opts.VirtualLines
--- @field signs vim.diagnostic.Opts.Signs
--- @field severity_sort {reverse?:boolean}
@@ -131,10 +150,11 @@ local M = {}
--- Overrides the setting from |vim.diagnostic.config()|.
--- @field source? boolean|'if_many'
---
---- A function that takes a diagnostic as input and returns a string.
---- The return value is the text used to display the diagnostic.
+--- A function that takes a diagnostic as input and returns a string or nil.
+--- If the return value is nil, the diagnostic is not displayed by the handler.
+--- Else the output text is used to display the diagnostic.
--- Overrides the setting from |vim.diagnostic.config()|.
---- @field format? fun(diagnostic:vim.Diagnostic): string
+--- @field format? fun(diagnostic:vim.Diagnostic): string?
---
--- Prefix each diagnostic in the floating window:
--- - If a `function`, {i} is the index of the diagnostic being evaluated and
@@ -170,6 +190,10 @@ local M = {}
--- severity |diagnostic-severity|
--- @field severity? vim.diagnostic.SeverityFilter
---
+--- Only show diagnostics for the current line.
+--- (default `false`)
+--- @field current_line? boolean
+---
--- Include the diagnostic source in virtual text. Use `'if_many'` to only
--- show sources if there is more than one diagnostic source in the buffer.
--- Otherwise, any truthy value means to always show the diagnostic source.
@@ -188,7 +212,7 @@ local M = {}
--- This can be used to render an LSP diagnostic error code.
--- @field suffix? string|(fun(diagnostic:vim.Diagnostic): string)
---
---- The return value is the text used to display the diagnostic. Example:
+--- If not nil, the return value is the text used to display the diagnostic. Example:
--- ```lua
--- function(diagnostic)
--- if diagnostic.severity == vim.diagnostic.severity.ERROR then
@@ -197,7 +221,8 @@ local M = {}
--- return diagnostic.message
--- end
--- ```
---- @field format? fun(diagnostic:vim.Diagnostic): string
+--- If the return value is nil, the diagnostic is not displayed by the handler.
+--- @field format? fun(diagnostic:vim.Diagnostic): string?
---
--- See |nvim_buf_set_extmark()|.
--- @field hl_mode? 'replace'|'combine'|'blend'
@@ -206,7 +231,7 @@ local M = {}
--- @field virt_text? [string,any][]
---
--- See |nvim_buf_set_extmark()|.
---- @field virt_text_pos? 'eol'|'overlay'|'right_align'|'inline'
+--- @field virt_text_pos? 'eol'|'eol_right_align'|'inline'|'overlay'|'right_align'
---
--- See |nvim_buf_set_extmark()|.
--- @field virt_text_win_col? integer
@@ -214,6 +239,17 @@ local M = {}
--- See |nvim_buf_set_extmark()|.
--- @field virt_text_hide? boolean
+--- @class vim.diagnostic.Opts.VirtualLines
+---
+--- Only show diagnostics for the current line.
+--- (default: `false`)
+--- @field current_line? boolean
+---
+--- A function that takes a diagnostic as input and returns a string or nil.
+--- If the return value is nil, the diagnostic is not displayed by the handler.
+--- Else the output text is used to display the diagnostic.
+--- @field format? fun(diagnostic:vim.Diagnostic): string?
+
--- @class vim.diagnostic.Opts.Signs
---
--- Only show virtual text for diagnostics matching the given
@@ -298,7 +334,8 @@ M.severity = {
local global_diagnostic_options = {
signs = true,
underline = true,
- virtual_text = true,
+ virtual_text = false,
+ virtual_lines = false,
float = true,
update_in_insert = false,
severity_sort = false,
@@ -342,7 +379,7 @@ local bufnr_and_namespace_cacher_mt = {
-- bufnr -> ns -> Diagnostic[]
local diagnostic_cache = {} --- @type table<integer,table<integer,vim.Diagnostic[]>>
do
- local group = api.nvim_create_augroup('DiagnosticBufWipeout', {})
+ local group = api.nvim_create_augroup('nvim.diagnostic.buf_wipeout', {})
setmetatable(diagnostic_cache, {
--- @param t table<integer,vim.Diagnostic[]>
--- @param bufnr integer
@@ -473,15 +510,21 @@ local function prefix_source(diagnostics)
end, diagnostics)
end
+--- @param format fun(vim.Diagnostic): string?
--- @param diagnostics vim.Diagnostic[]
--- @return vim.Diagnostic[]
local function reformat_diagnostics(format, diagnostics)
vim.validate('format', format, 'function')
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
- local formatted = vim.deepcopy(diagnostics, true)
- for _, diagnostic in ipairs(formatted) do
- diagnostic.message = format(diagnostic)
+ local formatted = {}
+ for _, diagnostic in ipairs(diagnostics) do
+ local message = format(diagnostic)
+ if message ~= nil then
+ local formatted_diagnostic = vim.deepcopy(diagnostic, true)
+ formatted_diagnostic.message = message
+ table.insert(formatted, formatted_diagnostic)
+ end
end
return formatted
end
@@ -567,17 +610,11 @@ end
-- TODO(lewis6991): these highlight maps can only be indexed with an integer, however there usage
-- implies they can be indexed with any vim.diagnostic.Severity
local virtual_text_highlight_map = make_highlight_map('VirtualText')
+local virtual_lines_highlight_map = make_highlight_map('VirtualLines')
local underline_highlight_map = make_highlight_map('Underline')
local floating_highlight_map = make_highlight_map('Floating')
local sign_highlight_map = make_highlight_map('Sign')
-local function get_bufnr(bufnr)
- if not bufnr or bufnr == 0 then
- return api.nvim_get_current_buf()
- end
- return bufnr
-end
-
--- @param diagnostics vim.Diagnostic[]
--- @return table<integer,vim.Diagnostic[]>
local function diagnostic_lines(diagnostics)
@@ -597,6 +634,26 @@ local function diagnostic_lines(diagnostics)
return diagnostics_by_line
end
+--- @param diagnostics table<integer, vim.Diagnostic[]>
+--- @return vim.Diagnostic[]
+local function diagnostics_at_cursor(diagnostics)
+ local lnum = api.nvim_win_get_cursor(0)[1] - 1
+
+ if diagnostics[lnum] ~= nil then
+ return diagnostics[lnum]
+ end
+
+ local cursor_diagnostics = {}
+ for _, line_diags in pairs(diagnostics) do
+ for _, diag in ipairs(line_diags) do
+ if diag.end_lnum and lnum >= diag.lnum and lnum <= diag.end_lnum then
+ table.insert(cursor_diagnostics, diag)
+ end
+ end
+ end
+ return cursor_diagnostics
+end
+
--- @param namespace integer
--- @param bufnr integer
--- @param diagnostics vim.Diagnostic[]
@@ -640,7 +697,7 @@ end
--- @param namespace integer
--- @param bufnr? integer
local function save_extmarks(namespace, bufnr)
- bufnr = get_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
if not diagnostic_attached_buffers[bufnr] then
api.nvim_buf_attach(bufnr, false, {
on_lines = function(_, _, _, _, _, last)
@@ -812,7 +869,7 @@ local function get_diagnostics(bufnr, opts, clamp)
end
end
elseif namespace == nil then
- bufnr = get_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
for iter_namespace in pairs(diagnostic_cache[bufnr]) do
add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace])
end
@@ -823,7 +880,7 @@ local function get_diagnostics(bufnr, opts, clamp)
end
end
else
- bufnr = get_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
for _, iter_namespace in ipairs(namespace) do
add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace] or {})
end
@@ -847,13 +904,34 @@ local function set_list(loclist, opts)
-- numbers beyond the end of the buffer
local diagnostics = get_diagnostics(bufnr, opts --[[@as vim.diagnostic.GetOpts]], false)
local items = M.toqflist(diagnostics)
+ local qf_id = nil
if loclist then
- vim.fn.setloclist(winnr, {}, ' ', { title = title, items = items })
+ vim.fn.setloclist(winnr, {}, 'u', { title = title, items = items })
else
- vim.fn.setqflist({}, ' ', { title = title, items = items })
+ qf_id = get_qf_id_for_title(title)
+
+ -- If we already have a diagnostics quickfix, update it rather than creating a new one.
+ -- This avoids polluting the finite set of quickfix lists, and preserves the currently selected
+ -- entry.
+ vim.fn.setqflist({}, qf_id and 'u' or ' ', {
+ title = title,
+ items = items,
+ id = qf_id,
+ })
end
+
if open then
- api.nvim_command(loclist and 'lwindow' or 'botright cwindow')
+ if not loclist then
+ -- First navigate to the diagnostics quickfix list.
+ --- @type integer
+ local nr = vim.fn.getqflist({ id = qf_id, nr = 0 }).nr
+ api.nvim_command(('silent %dchistory'):format(nr))
+
+ -- Now open the quickfix list.
+ api.nvim_command('botright cwindow')
+ else
+ api.nvim_command('lwindow')
+ end
end
end
@@ -1081,7 +1159,7 @@ function M.set(namespace, bufnr, diagnostics, opts)
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
vim.validate('opts', opts, 'table', true)
- bufnr = get_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
if vim.tbl_isempty(diagnostics) then
diagnostic_cache[bufnr][namespace] = nil
@@ -1361,17 +1439,13 @@ M.handlers.signs = {
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
vim.validate('opts', opts, 'table', true)
- bufnr = get_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
opts = opts or {}
if not api.nvim_buf_is_loaded(bufnr) then
return
end
- if opts.signs and opts.signs.severity then
- diagnostics = filter_by_severity(opts.signs.severity, diagnostics)
- end
-
-- 10 is the default sign priority when none is explicitly specified
local priority = opts.signs and opts.signs.priority or 10
local get_priority = severity_to_extmark_priority(priority, opts)
@@ -1379,7 +1453,7 @@ M.handlers.signs = {
local ns = M.get_namespace(namespace)
if not ns.user_data.sign_ns then
ns.user_data.sign_ns =
- api.nvim_create_namespace(string.format('%s/diagnostic/signs', ns.name))
+ api.nvim_create_namespace(string.format('nvim.%s.diagnostic.signs', ns.name))
end
-- Handle legacy diagnostic sign definitions
@@ -1467,21 +1541,17 @@ M.handlers.underline = {
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
vim.validate('opts', opts, 'table', true)
- bufnr = get_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
opts = opts or {}
if not vim.api.nvim_buf_is_loaded(bufnr) then
return
end
- if opts.underline and opts.underline.severity then
- diagnostics = filter_by_severity(opts.underline.severity, diagnostics)
- end
-
local ns = M.get_namespace(namespace)
if not ns.user_data.underline_ns then
ns.user_data.underline_ns =
- api.nvim_create_namespace(string.format('%s/diagnostic/underline', ns.name))
+ api.nvim_create_namespace(string.format('nvim.%s.diagnostic.underline', ns.name))
end
local underline_ns = ns.user_data.underline_ns
@@ -1524,6 +1594,28 @@ M.handlers.underline = {
end,
}
+--- @param namespace integer
+--- @param bufnr integer
+--- @param diagnostics table<integer, vim.Diagnostic[]>
+--- @param opts vim.diagnostic.Opts.VirtualText
+local function render_virtual_text(namespace, bufnr, diagnostics, opts)
+ api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
+
+ for line, line_diagnostics in pairs(diagnostics) do
+ local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts)
+
+ if virt_texts then
+ api.nvim_buf_set_extmark(bufnr, namespace, line, 0, {
+ hl_mode = opts.hl_mode or 'combine',
+ virt_text = virt_texts,
+ virt_text_pos = opts.virt_text_pos,
+ virt_text_hide = opts.virt_text_hide,
+ virt_text_win_col = opts.virt_text_win_col,
+ })
+ end
+ end
+end
+
M.handlers.virtual_text = {
show = function(namespace, bufnr, diagnostics, opts)
vim.validate('namespace', namespace, 'number')
@@ -1531,14 +1623,13 @@ M.handlers.virtual_text = {
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
vim.validate('opts', opts, 'table', true)
- bufnr = get_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
opts = opts or {}
if not vim.api.nvim_buf_is_loaded(bufnr) then
return
end
- local severity --- @type vim.diagnostic.SeverityFilter?
if opts.virtual_text then
if opts.virtual_text.format then
diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics)
@@ -1549,36 +1640,51 @@ M.handlers.virtual_text = {
then
diagnostics = prefix_source(diagnostics)
end
- if opts.virtual_text.severity then
- severity = opts.virtual_text.severity
- end
end
local ns = M.get_namespace(namespace)
if not ns.user_data.virt_text_ns then
ns.user_data.virt_text_ns =
- api.nvim_create_namespace(string.format('%s/diagnostic/virtual_text', ns.name))
+ api.nvim_create_namespace(string.format('nvim.%s.diagnostic.virtual_text', ns.name))
+ end
+ if not ns.user_data.virt_text_augroup then
+ ns.user_data.virt_text_augroup = api.nvim_create_augroup(
+ string.format('nvim.%s.diagnostic.virt_text', ns.name),
+ { clear = true }
+ )
end
- local virt_text_ns = ns.user_data.virt_text_ns
- local buffer_line_diagnostics = diagnostic_lines(diagnostics)
- for line, line_diagnostics in pairs(buffer_line_diagnostics) do
- if severity then
- line_diagnostics = filter_by_severity(severity, line_diagnostics)
- end
- local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts.virtual_text)
-
- if virt_texts then
- api.nvim_buf_set_extmark(bufnr, virt_text_ns, line, 0, {
- hl_mode = opts.virtual_text.hl_mode or 'combine',
- virt_text = virt_texts,
- virt_text_pos = opts.virtual_text.virt_text_pos,
- virt_text_hide = opts.virtual_text.virt_text_hide,
- virt_text_win_col = opts.virtual_text.virt_text_win_col,
- })
- end
+ api.nvim_clear_autocmds({ group = ns.user_data.virt_text_augroup, buffer = bufnr })
+
+ local line_diagnostics = diagnostic_lines(diagnostics)
+
+ if opts.virtual_text.current_line == true then
+ api.nvim_create_autocmd('CursorMoved', {
+ buffer = bufnr,
+ group = ns.user_data.virt_text_augroup,
+ callback = function()
+ local lnum = api.nvim_win_get_cursor(0)[1] - 1
+ render_virtual_text(
+ ns.user_data.virt_text_ns,
+ bufnr,
+ { [lnum] = diagnostics_at_cursor(line_diagnostics) },
+ opts.virtual_text
+ )
+ end,
+ })
+ -- Also show diagnostics for the current line before the first CursorMoved event.
+ local lnum = api.nvim_win_get_cursor(0)[1] - 1
+ render_virtual_text(
+ ns.user_data.virt_text_ns,
+ bufnr,
+ { [lnum] = diagnostics_at_cursor(line_diagnostics) },
+ opts.virtual_text
+ )
+ else
+ render_virtual_text(ns.user_data.virt_text_ns, bufnr, line_diagnostics, opts.virtual_text)
end
- save_extmarks(virt_text_ns, bufnr)
+
+ save_extmarks(ns.user_data.virt_text_ns, bufnr)
end,
hide = function(namespace, bufnr)
local ns = M.get_namespace(namespace)
@@ -1587,6 +1693,262 @@ M.handlers.virtual_text = {
if api.nvim_buf_is_valid(bufnr) then
api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_text_ns, 0, -1)
end
+ api.nvim_clear_autocmds({ group = ns.user_data.virt_text_augroup, buffer = bufnr })
+ end
+ end,
+}
+
+--- Some characters (like tabs) take up more than one cell. Additionally, inline
+--- virtual text can make the distance between 2 columns larger.
+--- A diagnostic aligned under such characters needs to account for that and that
+--- many spaces to its left.
+--- @param bufnr integer
+--- @param lnum integer
+--- @param start_col integer
+--- @param end_col integer
+--- @return integer
+local function distance_between_cols(bufnr, lnum, start_col, end_col)
+ return api.nvim_buf_call(bufnr, function()
+ local s = vim.fn.virtcol({ lnum + 1, start_col })
+ local e = vim.fn.virtcol({ lnum + 1, end_col + 1 })
+ return e - 1 - s
+ end)
+end
+
+--- @param namespace integer
+--- @param bufnr integer
+--- @param diagnostics vim.Diagnostic[]
+local function render_virtual_lines(namespace, bufnr, diagnostics)
+ table.sort(diagnostics, function(d1, d2)
+ if d1.lnum == d2.lnum then
+ return d1.col < d2.col
+ else
+ return d1.lnum < d2.lnum
+ end
+ end)
+
+ api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
+
+ if not next(diagnostics) then
+ return
+ end
+
+ -- This loop reads each line, putting them into stacks with some extra data since
+ -- rendering each line requires understanding what is beneath it.
+ local ElementType = { Space = 1, Diagnostic = 2, Overlap = 3, Blank = 4 } ---@enum ElementType
+ local line_stacks = {} ---@type table<integer, {[1]:ElementType, [2]:string|vim.diagnostic.Severity|vim.Diagnostic}[]>
+ local prev_lnum = -1
+ local prev_col = 0
+ for _, diag in ipairs(diagnostics) do
+ if not line_stacks[diag.lnum] then
+ line_stacks[diag.lnum] = {}
+ end
+
+ local stack = line_stacks[diag.lnum]
+
+ if diag.lnum ~= prev_lnum then
+ table.insert(stack, {
+ ElementType.Space,
+ string.rep(' ', distance_between_cols(bufnr, diag.lnum, 0, diag.col)),
+ })
+ elseif diag.col ~= prev_col then
+ table.insert(stack, {
+ ElementType.Space,
+ string.rep(
+ ' ',
+ -- +1 because indexing starts at 0 in one API but at 1 in the other.
+ -- -1 for non-first lines, since the previous column was already drawn.
+ distance_between_cols(bufnr, diag.lnum, prev_col + 1, diag.col) - 1
+ ),
+ })
+ else
+ table.insert(stack, { ElementType.Overlap, diag.severity })
+ end
+
+ if diag.message:find('^%s*$') then
+ table.insert(stack, { ElementType.Blank, diag })
+ else
+ table.insert(stack, { ElementType.Diagnostic, diag })
+ end
+
+ prev_lnum, prev_col = diag.lnum, diag.col
+ end
+
+ local chars = {
+ cross = '┼',
+ horizontal = '─',
+ horizontal_up = '┴',
+ up_right = '└',
+ vertical = '│',
+ vertical_right = '├',
+ }
+
+ for lnum, stack in pairs(line_stacks) do
+ local virt_lines = {}
+
+ -- Note that we read in the order opposite to insertion.
+ for i = #stack, 1, -1 do
+ if stack[i][1] == ElementType.Diagnostic then
+ local diagnostic = stack[i][2]
+ local left = {} ---@type {[1]:string, [2]:string}
+ local overlap = false
+ local multi = false
+
+ -- Iterate the stack for this line to find elements on the left.
+ for j = 1, i - 1 do
+ local type = stack[j][1]
+ local data = stack[j][2]
+ if type == ElementType.Space then
+ if multi then
+ ---@cast data string
+ table.insert(left, {
+ string.rep(chars.horizontal, data:len()),
+ virtual_lines_highlight_map[diagnostic.severity],
+ })
+ else
+ table.insert(left, { data, '' })
+ end
+ elseif type == ElementType.Diagnostic then
+ -- If an overlap follows this line, don't add an extra column.
+ if stack[j + 1][1] ~= ElementType.Overlap then
+ table.insert(left, { chars.vertical, virtual_lines_highlight_map[data.severity] })
+ end
+ overlap = false
+ elseif type == ElementType.Blank then
+ if multi then
+ table.insert(
+ left,
+ { chars.horizontal_up, virtual_lines_highlight_map[data.severity] }
+ )
+ else
+ table.insert(left, { chars.up_right, virtual_lines_highlight_map[data.severity] })
+ end
+ multi = true
+ elseif type == ElementType.Overlap then
+ overlap = true
+ end
+ end
+
+ local center_char ---@type string
+ if overlap and multi then
+ center_char = chars.cross
+ elseif overlap then
+ center_char = chars.vertical_right
+ elseif multi then
+ center_char = chars.horizontal_up
+ else
+ center_char = chars.up_right
+ end
+ local center = {
+ {
+ string.format('%s%s', center_char, string.rep(chars.horizontal, 4) .. ' '),
+ virtual_lines_highlight_map[diagnostic.severity],
+ },
+ }
+
+ -- We can draw on the left side if and only if:
+ -- a. Is the last one stacked this line.
+ -- b. Has enough space on the left.
+ -- c. Is just one line.
+ -- d. Is not an overlap.
+ local msg ---@type string
+ if diagnostic.code then
+ msg = string.format('%s: %s', diagnostic.code, diagnostic.message)
+ else
+ msg = diagnostic.message
+ end
+ for msg_line in msg:gmatch('([^\n]+)') do
+ local vline = {}
+ vim.list_extend(vline, left)
+ vim.list_extend(vline, center)
+ vim.list_extend(vline, { { msg_line, virtual_lines_highlight_map[diagnostic.severity] } })
+
+ table.insert(virt_lines, vline)
+
+ -- Special-case for continuation lines:
+ if overlap then
+ center = {
+ { chars.vertical, virtual_lines_highlight_map[diagnostic.severity] },
+ { ' ', '' },
+ }
+ else
+ center = { { ' ', '' } }
+ end
+ end
+ end
+ end
+
+ api.nvim_buf_set_extmark(bufnr, namespace, lnum, 0, { virt_lines = virt_lines })
+ end
+end
+
+M.handlers.virtual_lines = {
+ show = function(namespace, bufnr, diagnostics, opts)
+ vim.validate('namespace', namespace, 'number')
+ vim.validate('bufnr', bufnr, 'number')
+ vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
+ vim.validate('opts', opts, 'table', true)
+
+ bufnr = vim._resolve_bufnr(bufnr)
+ opts = opts or {}
+
+ if not api.nvim_buf_is_loaded(bufnr) then
+ return
+ end
+
+ local ns = M.get_namespace(namespace)
+ if not ns.user_data.virt_lines_ns then
+ ns.user_data.virt_lines_ns =
+ api.nvim_create_namespace(string.format('nvim.%s.diagnostic.virtual_lines', ns.name))
+ end
+ if not ns.user_data.virt_lines_augroup then
+ ns.user_data.virt_lines_augroup = api.nvim_create_augroup(
+ string.format('nvim.%s.diagnostic.virt_lines', ns.name),
+ { clear = true }
+ )
+ end
+
+ api.nvim_clear_autocmds({ group = ns.user_data.virt_lines_augroup, buffer = bufnr })
+
+ if opts.virtual_lines.format then
+ diagnostics = reformat_diagnostics(opts.virtual_lines.format, diagnostics)
+ end
+
+ if opts.virtual_lines.current_line == true then
+ -- Create a mapping from line -> diagnostics so that we can quickly get the
+ -- diagnostics we need when the cursor line doesn't change.
+ local line_diagnostics = diagnostic_lines(diagnostics)
+ api.nvim_create_autocmd('CursorMoved', {
+ buffer = bufnr,
+ group = ns.user_data.virt_lines_augroup,
+ callback = function()
+ render_virtual_lines(
+ ns.user_data.virt_lines_ns,
+ bufnr,
+ diagnostics_at_cursor(line_diagnostics)
+ )
+ end,
+ })
+ -- Also show diagnostics for the current line before the first CursorMoved event.
+ render_virtual_lines(
+ ns.user_data.virt_lines_ns,
+ bufnr,
+ diagnostics_at_cursor(line_diagnostics)
+ )
+ else
+ render_virtual_lines(ns.user_data.virt_lines_ns, bufnr, diagnostics)
+ end
+
+ save_extmarks(ns.user_data.virt_lines_ns, bufnr)
+ end,
+ hide = function(namespace, bufnr)
+ local ns = M.get_namespace(namespace)
+ if ns.user_data.virt_lines_ns then
+ diagnostic_cache_extmarks[bufnr][ns.user_data.virt_lines_ns] = {}
+ if api.nvim_buf_is_valid(bufnr) then
+ api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_lines_ns, 0, -1)
+ end
+ api.nvim_clear_autocmds({ group = ns.user_data.virt_lines_augroup, buffer = bufnr })
end
end,
}
@@ -1656,7 +2018,7 @@ function M.hide(namespace, bufnr)
vim.validate('namespace', namespace, 'number', true)
vim.validate('bufnr', bufnr, 'number', true)
- local buffers = bufnr and { get_bufnr(bufnr) } or vim.tbl_keys(diagnostic_cache)
+ local buffers = bufnr and { vim._resolve_bufnr(bufnr) } or vim.tbl_keys(diagnostic_cache)
for _, iter_bufnr in ipairs(buffers) do
local namespaces = namespace and { namespace } or vim.tbl_keys(diagnostic_cache[iter_bufnr])
for _, iter_namespace in ipairs(namespaces) do
@@ -1683,7 +2045,7 @@ function M.is_enabled(filter)
return vim.tbl_isempty(diagnostic_disabled) and not diagnostic_disabled[1]
end
- local bufnr = get_bufnr(filter.bufnr)
+ local bufnr = vim._resolve_bufnr(filter.bufnr)
if type(diagnostic_disabled[bufnr]) == 'table' then
return not diagnostic_disabled[bufnr][filter.ns_id]
end
@@ -1724,7 +2086,7 @@ function M.show(namespace, bufnr, diagnostics, opts)
end
else
-- namespace is nil
- bufnr = get_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
for iter_namespace in pairs(diagnostic_cache[bufnr]) do
M.show(iter_namespace, bufnr, nil, opts)
end
@@ -1770,7 +2132,8 @@ function M.show(namespace, bufnr, diagnostics, opts)
for handler_name, handler in pairs(M.handlers) do
if handler.show and opts_res[handler_name] then
- handler.show(namespace, bufnr, diagnostics, opts_res)
+ local filtered = filter_by_severity(opts_res[handler_name].severity, diagnostics)
+ handler.show(namespace, bufnr, filtered, opts_res)
end
end
end
@@ -1791,7 +2154,7 @@ function M.open_float(opts, ...)
end
opts = opts or {}
- bufnr = get_bufnr(bufnr or opts.bufnr)
+ bufnr = vim._resolve_bufnr(bufnr or opts.bufnr)
do
-- Resolve options with user settings from vim.diagnostic.config
@@ -1961,17 +2324,24 @@ function M.open_float(opts, ...)
if not opts.focus_id then
opts.focus_id = scope
end
+
+ --- @diagnostic disable-next-line: param-type-mismatch
local float_bufnr, winnr = vim.lsp.util.open_floating_preview(lines, 'plaintext', opts)
+ vim.bo[float_bufnr].path = vim.bo[bufnr].path
+
+ --- @diagnostic disable-next-line: deprecated
+ local add_highlight = api.nvim_buf_add_highlight
+
for i, hl in ipairs(highlights) do
local line = lines[i]
local prefix_len = hl.prefix and hl.prefix.length or 0
local suffix_len = hl.suffix and hl.suffix.length or 0
if prefix_len > 0 then
- api.nvim_buf_add_highlight(float_bufnr, -1, hl.prefix.hlname, i - 1, 0, prefix_len)
+ add_highlight(float_bufnr, -1, hl.prefix.hlname, i - 1, 0, prefix_len)
end
- api.nvim_buf_add_highlight(float_bufnr, -1, hl.hlname, i - 1, prefix_len, #line - suffix_len)
+ add_highlight(float_bufnr, -1, hl.hlname, i - 1, prefix_len, #line - suffix_len)
if suffix_len > 0 then
- api.nvim_buf_add_highlight(float_bufnr, -1, hl.suffix.hlname, i - 1, #line - suffix_len, -1)
+ add_highlight(float_bufnr, -1, hl.suffix.hlname, i - 1, #line - suffix_len, -1)
end
end
@@ -1993,7 +2363,7 @@ function M.reset(namespace, bufnr)
vim.validate('namespace', namespace, 'number', true)
vim.validate('bufnr', bufnr, 'number', true)
- local buffers = bufnr and { get_bufnr(bufnr) } or vim.tbl_keys(diagnostic_cache)
+ local buffers = bufnr and { vim._resolve_bufnr(bufnr) } or vim.tbl_keys(diagnostic_cache)
for _, iter_bufnr in ipairs(buffers) do
local namespaces = namespace and { namespace } or vim.tbl_keys(diagnostic_cache[iter_bufnr])
for _, iter_namespace in ipairs(namespaces) do
@@ -2024,7 +2394,8 @@ end
--- (default: `true`)
--- @field open? boolean
---
---- Title of quickfix list. Defaults to "Diagnostics".
+--- Title of quickfix list. Defaults to "Diagnostics". If there's already a quickfix list with this
+--- title, it's updated. If not, a new quickfix list is created.
--- @field title? string
---
--- See |diagnostic-severity|.
@@ -2131,7 +2502,7 @@ function M.enable(enable, filter)
ns.disabled = not enable
end
else
- bufnr = get_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
if not ns_id then
diagnostic_disabled[bufnr] = (not enable) and true or nil
else
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
index e1e73d63fe..cc7358ee49 100644
--- a/runtime/lua/vim/filetype.lua
+++ b/runtime/lua/vim/filetype.lua
@@ -275,6 +275,9 @@ local extension = {
mdh = 'c',
epro = 'c',
qc = 'c',
+ c3 = 'c3',
+ c3i = 'c3',
+ c3t = 'c3',
cabal = 'cabal',
cairo = 'cairo',
capnp = 'capnp',
@@ -300,6 +303,7 @@ local extension = {
cho = 'chordpro',
chordpro = 'chordpro',
ck = 'chuck',
+ cl = detect.cl,
eni = 'cl',
icl = 'clean',
cljx = 'clojure',
@@ -349,6 +353,7 @@ local extension = {
cql = 'cqlang',
crm = 'crm',
cr = 'crystal',
+ cake = 'cs',
csx = 'cs',
cs = 'cs',
csc = 'csc',
@@ -500,6 +505,7 @@ local extension = {
gdshader = 'gdshader',
shader = 'gdshader',
ged = 'gedcom',
+ gel = 'gel',
gmi = 'gemtext',
gemini = 'gemtext',
gift = 'gift',
@@ -588,6 +594,7 @@ local extension = {
hw = detect.hw,
module = detect.hw,
pkg = detect.hw,
+ hy = 'hy',
iba = 'ibasic',
ibi = 'ibasic',
icn = 'icon',
@@ -612,6 +619,7 @@ local extension = {
janet = 'janet',
jav = 'java',
java = 'java',
+ jsh = 'java',
jj = 'javacc',
jjt = 'javacc',
es = 'javascript',
@@ -624,7 +632,7 @@ local extension = {
clp = 'jess',
jgr = 'jgraph',
jinja = 'jinja',
- jjdescription = 'jj',
+ jjdescription = 'jjdescription',
j73 = 'jovial',
jov = 'jovial',
jovial = 'jovial',
@@ -653,6 +661,10 @@ local extension = {
jsp = 'jsp',
jl = 'julia',
just = 'just',
+ Just = 'just',
+ JUST = 'just',
+ kl = 'karel',
+ KL = 'karel',
kdl = 'kdl',
kv = 'kivy',
kix = 'kix',
@@ -666,6 +678,7 @@ local extension = {
k = 'kwt',
ACE = 'lace',
ace = 'lace',
+ lalrpop = 'lalrpop',
latte = 'latte',
lte = 'latte',
ld = 'ld',
@@ -686,13 +699,11 @@ local extension = {
ily = 'lilypond',
liquid = 'liquid',
liq = 'liquidsoap',
- cl = 'lisp',
L = 'lisp',
lisp = 'lisp',
el = 'lisp',
lsp = 'lisp',
asd = 'lisp',
- stsg = 'lisp',
lt = 'lite',
lite = 'lite',
livemd = 'livebook',
@@ -721,14 +732,14 @@ local extension = {
mc = detect.mc,
quake = 'm3quake',
m4 = function(path, bufnr)
- path = path:lower()
- return not (path:find('html%.m4$') or path:find('fvwm2rc')) and 'm4' or nil
+ local pathl = path:lower()
+ return not (pathl:find('html%.m4$') or pathl:find('fvwm2rc')) and 'm4' or nil
end,
eml = 'mail',
mk = detect.make,
mak = detect.make,
page = 'mallard',
- map = 'map',
+ map = detect_line1('^%*+$', 'lnkmap', 'map'),
mws = 'maple',
mpl = 'maple',
mv = 'maple',
@@ -738,6 +749,7 @@ local extension = {
mkd = detect.markdown,
markdown = detect.markdown,
mdown = detect.markdown,
+ masm = 'masm',
mhtml = 'mason',
mason = 'mason',
master = 'master',
@@ -800,6 +812,7 @@ local extension = {
n1ql = 'n1ql',
nql = 'n1ql',
nanorc = 'nanorc',
+ nasm = 'nasm',
NSA = 'natural',
NSC = 'natural',
NSG = 'natural',
@@ -834,6 +847,7 @@ local extension = {
tr = 'nroff',
nsi = 'nsis',
nsh = 'nsis',
+ nt = 'ntriples',
nu = 'nu',
obj = 'obj',
objdump = 'objdump',
@@ -953,11 +967,14 @@ local extension = {
ps1xml = 'ps1xml',
psf = 'psf',
psl = 'psl',
+ ptx = 'ptx',
pug = 'pug',
purs = 'purescript',
arr = 'pyret',
pxd = 'pyrex',
+ pxi = 'pyrex',
pyx = 'pyrex',
+ ['pyx+'] = 'pyrex',
pyw = 'python',
py = 'python',
pyi = 'python',
@@ -1050,16 +1067,17 @@ local extension = {
builder = 'ruby',
rake = 'ruby',
rs = 'rust',
+ sa = detect.sa,
sage = 'sage',
sls = 'salt',
sas = 'sas',
sass = 'sass',
- sa = 'sather',
sbt = 'sbt',
scala = 'scala',
ss = 'scheme',
scm = 'scheme',
sld = 'scheme',
+ stsg = 'scheme',
sce = 'scilab',
sci = 'scilab',
scss = 'scss',
@@ -1082,6 +1100,7 @@ local extension = {
la = 'sh',
lai = 'sh',
mdd = 'sh',
+ slang = 'shaderslang',
sieve = 'sieve',
siv = 'sieve',
sig = detect.sig,
@@ -1224,6 +1243,7 @@ local extension = {
toml = 'toml',
tpp = 'tpp',
treetop = 'treetop',
+ trig = 'trig',
slt = 'tsalt',
tsscl = 'tsscl',
tssgm = 'tssgm',
@@ -1323,6 +1343,7 @@ local extension = {
xlb = 'xml',
xlc = 'xml',
xba = 'xml',
+ slnx = 'xml',
xpm = detect_line1('XPM2', 'xpm2', 'xpm'),
xpm2 = 'xpm2',
xqy = 'xquery',
@@ -1378,7 +1399,7 @@ local extension = {
txt = detect.txt,
xml = detect.xml,
y = detect.y,
- cmd = detect_line1('^/%*', 'rexx', 'dosbatch'),
+ cmd = detect.cmd,
rul = detect.rul,
cpy = detect_line1('^##', 'python', 'cobol'),
dsl = detect_line1('^%s*<!', 'dsl', 'structurizr'),
@@ -1427,6 +1448,7 @@ local filename = {
['/etc/asound.conf'] = 'alsaconf',
['build.xml'] = 'ant',
['.htaccess'] = 'apache',
+ APKBUILD = 'apkbuild',
['apt.conf'] = 'aptconf',
['/.aptitude/config'] = 'aptconf',
['=tagging-method'] = 'arch',
@@ -1485,6 +1507,7 @@ local filename = {
['NEWS.dch'] = 'debchangelog',
['NEWS.Debian'] = 'debchangelog',
['/debian/control'] = 'debcontrol',
+ ['/DEBIAN/control'] = 'debcontrol',
['/debian/copyright'] = 'debcopyright',
['/etc/apt/sources.list'] = 'debsources',
['denyhosts.conf'] = 'denyhosts',
@@ -1532,6 +1555,8 @@ local filename = {
['filter-rules'] = 'elmfilt',
['exim.conf'] = 'exim',
exports = 'exports',
+ fennelrc = 'fennel',
+ ['.fennelrc'] = 'fennel',
['.fetchmailrc'] = 'fetchmail',
fvSchemes = detect.foam,
fvSolution = detect.foam,
@@ -1553,6 +1578,12 @@ local filename = {
['.gitmodules'] = 'gitconfig',
['.gitattributes'] = 'gitattributes',
['.gitignore'] = 'gitignore',
+ ['.ignore'] = 'gitignore',
+ ['.dockerignore'] = 'gitignore',
+ ['.fdignore'] = 'gitignore',
+ ['.npmignore'] = 'gitignore',
+ ['.rgignore'] = 'gitignore',
+ ['.vscodeignore'] = 'gitignore',
['gitolite.conf'] = 'gitolite',
['git-rebase-todo'] = 'gitrebase',
gkrellmrc = 'gkrellmrc',
@@ -1587,6 +1618,7 @@ local filename = {
['/etc/host.conf'] = 'hostconf',
['/etc/hosts.allow'] = 'hostsaccess',
['/etc/hosts.deny'] = 'hostsaccess',
+ ['.hy-history'] = 'hy',
['hyprland.conf'] = 'hyprlang',
['hyprpaper.conf'] = 'hyprlang',
['hypridle.conf'] = 'hyprlang',
@@ -1608,6 +1640,7 @@ local filename = {
['.lintstagedrc'] = 'json',
['deno.lock'] = 'json',
['flake.lock'] = 'json',
+ ['.swcrc'] = 'json',
['.babelrc'] = 'jsonc',
['.eslintrc'] = 'jsonc',
['.hintrc'] = 'jsonc',
@@ -1617,9 +1650,13 @@ local filename = {
['.luaurc'] = 'jsonc',
['.swrc'] = 'jsonc',
['.vsconfig'] = 'jsonc',
+ ['bun.lock'] = 'jsonc',
['.justfile'] = 'just',
+ ['.Justfile'] = 'just',
+ ['.JUSTFILE'] = 'just',
['justfile'] = 'just',
['Justfile'] = 'just',
+ ['JUSTFILE'] = 'just',
Kconfig = 'kconfig',
['Kconfig.debug'] = 'kconfig',
['Config.in'] = 'kconfig',
@@ -1762,6 +1799,7 @@ local filename = {
['Rantfile'] = 'ruby',
Vagrantfile = 'ruby',
['smb.conf'] = 'samba',
+ ['.lips_repl_history'] = 'scheme',
screenrc = 'screen',
['.screenrc'] = 'screen',
['/etc/sensors3.conf'] = 'sensors',
@@ -1783,7 +1821,6 @@ local filename = {
['.kshrc'] = detect.ksh,
['.profile'] = detect.sh,
['/etc/profile'] = detect.sh,
- APKBUILD = detect.bash,
PKGBUILD = detect.bash,
['.tcshrc'] = detect.tcsh,
['tcsh.login'] = detect.tcsh,
@@ -1860,11 +1897,17 @@ local filename = {
['/etc/blkid.tab'] = 'xml',
['/etc/blkid.tab.old'] = 'xml',
['fonts.conf'] = 'xml',
+ ['Directory.Packages.props'] = 'xml',
+ ['Directory.Build.props'] = 'xml',
+ ['Directory.Build.targets'] = 'xml',
['.clangd'] = 'yaml',
['.clang-format'] = 'yaml',
['.clang-tidy'] = 'yaml',
+ ['pixi.lock'] = 'yaml',
['yarn.lock'] = 'yaml',
matplotlibrc = 'yaml',
+ ['.condarc'] = 'yaml',
+ condarc = 'yaml',
zathurarc = 'zathurarc',
['/etc/zprofile'] = 'zsh',
['.zlogin'] = 'zsh',
@@ -2141,8 +2184,8 @@ local pattern = {
['/gitolite%-admin/conf/'] = starsetf('gitolite'),
['/%.i3/config$'] = 'i3config',
['/i3/config$'] = 'i3config',
- ['/supertux2/config$'] = 'lisp',
['/%.mplayer/config$'] = 'mplayerconf',
+ ['/supertux2/config$'] = 'scheme',
['/neofetch/config%.conf$'] = 'sh',
['/%.ssh/config$'] = 'sshconfig',
['/%.sway/config$'] = 'swayconfig',
@@ -2210,8 +2253,10 @@ local pattern = {
['^dictd.*%.conf$'] = 'dictdconf',
['/lxqt/.*%.conf$'] = 'dosini',
['/screengrab/.*%.conf$'] = 'dosini',
+ ['/%.config/fd/ignore$'] = 'gitignore',
['^${GNUPGHOME}/gpg%.conf$'] = 'gpg',
['/boot/grub/grub%.conf$'] = 'grub',
+ ['/hypr/.*%.conf$'] = 'hyprlang',
['^lilo%.conf'] = starsetf('lilo'),
['^named.*%.conf$'] = 'named',
['^rndc.*%.conf$'] = 'named',
@@ -2313,6 +2358,7 @@ local pattern = {
['%.cmake%.in$'] = 'cmake',
['^crontab%.'] = starsetf('crontab'),
['^cvs%d+$'] = 'cvs',
+ ['/DEBIAN/control$'] = 'debcontrol',
['^php%.ini%-'] = 'dosini',
['^php%-fpm%.conf'] = 'dosini',
['^www%.conf'] = 'dosini',
@@ -2338,6 +2384,8 @@ local pattern = {
['%.html%.m4$'] = 'htmlm4',
['^JAM.*%.'] = starsetf('jam'),
['^Prl.*%.'] = starsetf('jam'),
+ ['^${HOME}/.*/Code/User/.*%.json$'] = 'jsonc',
+ ['^${HOME}/.*/VSCodium/User/.*%.json$'] = 'jsonc',
['%.properties_..$'] = 'jproperties',
['%.properties_.._..$'] = 'jproperties',
['%.properties_.._.._'] = starsetf('jproperties'),
diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua
index 98b001bd51..fc0b45ecd8 100644
--- a/runtime/lua/vim/filetype/detect.lua
+++ b/runtime/lua/vim/filetype/detect.lua
@@ -34,6 +34,12 @@ local matchregex = vim.filetype._matchregex
-- can be detected from the first five lines of the file.
--- @type vim.filetype.mapfn
function M.asm(path, bufnr)
+ -- tiasm uses `* comment`
+ local lines = table.concat(getlines(bufnr, 1, 10), '\n')
+ if findany(lines, { '^%*', '\n%*', 'Texas Instruments Incorporated' }) then
+ return 'tiasm'
+ end
+
local syntax = vim.b[bufnr].asmsyntax
if not syntax or syntax == '' then
syntax = M.asm_syntax(path, bufnr)
@@ -181,6 +187,16 @@ function M.changelog(_, bufnr)
end
--- @type vim.filetype.mapfn
+function M.cl(_, bufnr)
+ local lines = table.concat(getlines(bufnr, 1, 4))
+ if lines:match('/%*') then
+ return 'opencl'
+ else
+ return 'lisp'
+ end
+end
+
+--- @type vim.filetype.mapfn
function M.class(_, bufnr)
-- Check if not a Java class (starts with '\xca\xfe\xba\xbe')
if not getline(bufnr, 1):find('^\202\254\186\190') then
@@ -209,6 +225,24 @@ function M.cls(_, bufnr)
return 'st'
end
+--- *.cmd is close to a Batch file, but on OS/2 Rexx files and TI linker command files also use *.cmd.
+--- lnk: `/* comment */`, `// comment`, and `--linker-option=value`
+--- rexx: `/* comment */`, `-- comment`
+--- @type vim.filetype.mapfn
+function M.cmd(_, bufnr)
+ local lines = table.concat(getlines(bufnr, 1, 20))
+ if matchregex(lines, [[MEMORY\|SECTIONS\|\%(^\|\n\)--\S\|\%(^\|\n\)//]]) then
+ return 'lnk'
+ else
+ local line1 = getline(bufnr, 1)
+ if line1:find('^/%*') then
+ return 'rexx'
+ else
+ return 'dosbatch'
+ end
+ end
+end
+
--- @type vim.filetype.mapfn
function M.conf(path, bufnr)
if fn.did_filetype() ~= 0 or path:find(vim.g.ft_ignore_pat) then
@@ -227,7 +261,8 @@ end
--- Debian Control
--- @type vim.filetype.mapfn
function M.control(_, bufnr)
- if getline(bufnr, 1):find('^Source:') then
+ local line1 = getline(bufnr, 1)
+ if line1 and findany(line1, { '^Source:', '^Package:' }) then
return 'debcontrol'
end
end
@@ -722,7 +757,7 @@ function M.html(_, bufnr)
if
matchregex(
line,
- [[@\(if\|for\|defer\|switch\)\|\*\(ngIf\|ngFor\|ngSwitch\|ngTemplateOutlet\)\|ng-template\|ng-content\|{{.*}}]]
+ [[@\(if\|for\|defer\|switch\)\|\*\(ngIf\|ngFor\|ngSwitch\|ngTemplateOutlet\)\|ng-template\|ng-content]]
)
then
return 'htmlangular'
@@ -846,7 +881,7 @@ end
--- (refactor of filetype.vim since the patterns are case-insensitive)
--- @type vim.filetype.mapfn
function M.log(path, _)
- path = path:lower()
+ path = path:lower() --- @type string LuaLS bug
if
findany(
path,
@@ -1132,7 +1167,7 @@ end
--- @type vim.filetype.mapfn
function M.perl(path, bufnr)
local dir_name = vim.fs.dirname(path)
- if fn.expand(path, '%:e') == 't' and (dir_name == 't' or dir_name == 'xt') then
+ if fn.fnamemodify(path, '%:e') == 't' and (dir_name == 't' or dir_name == 'xt') then
return 'perl'
end
local first_line = getline(bufnr, 1)
@@ -1340,7 +1375,7 @@ end
local udev_rules_pattern = '^%s*udev_rules%s*=%s*"([%^"]+)/*".*'
--- @type vim.filetype.mapfn
function M.rules(path)
- path = path:lower()
+ path = path:lower() --- @type string LuaLS bug
if
findany(path, {
'/etc/udev/.*%.rules$',
@@ -1363,7 +1398,7 @@ function M.rules(path)
if not ok then
return 'hog'
end
- local dir = fn.expand(path, ':h')
+ local dir = fn.fnamemodify(path, ':h')
for _, line in ipairs(config_lines) do
local match = line:match(udev_rules_pattern)
if match then
@@ -1395,6 +1430,15 @@ function M.sig(_, bufnr)
end
end
+--- @type vim.filetype.mapfn
+function M.sa(_, bufnr)
+ local lines = table.concat(getlines(bufnr, 1, 4), '\n')
+ if findany(lines, { '^;', '\n;' }) then
+ return 'tiasm'
+ end
+ return 'sather'
+end
+
-- This function checks the first 25 lines of file extension "sc" to resolve
-- detection between scala and SuperCollider
--- @type vim.filetype.mapfn
@@ -1719,7 +1763,7 @@ function M.v(_, bufnr)
return vim.g.filetype_v
end
local in_comment = 0
- for _, line in ipairs(getlines(bufnr, 1, 200)) do
+ for _, line in ipairs(getlines(bufnr, 1, 500)) do
if line:find('^%s*/%*') then
in_comment = 1
end
@@ -1733,7 +1777,7 @@ function M.v(_, bufnr)
or line:find('%(%*') and not line:find('/[/*].*%(%*')
then
return 'coq'
- elseif findany(line, { ';%s*$', ';%s*/[/*]' }) then
+ elseif findany(line, { ';%s*$', ';%s*/[/*]', '^%s*module%s+%w+%s*%(' }) then
return 'verilog'
end
end
@@ -1833,6 +1877,7 @@ local patterns_hashbang = {
ruby = 'ruby',
['node\\(js\\)\\=\\>\\|js\\>'] = { 'javascript', { vim_regex = true } },
['rhino\\>'] = { 'javascript', { vim_regex = true } },
+ just = 'just',
-- BC calculator
['^bc\\>'] = { 'bc', { vim_regex = true } },
['sed\\>'] = { 'sed', { vim_regex = true } },
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index d91eeaf02f..8b4242223a 100644
--- a/runtime/lua/vim/fs.lua
+++ b/runtime/lua/vim/fs.lua
@@ -1,3 +1,15 @@
+--- @brief <pre>help
+--- *vim.fs.exists()*
+--- Use |uv.fs_stat()| to check a file's type, and whether it exists.
+---
+--- Example:
+---
+--- >lua
+--- if vim.uv.fs_stat(file) then
+--- vim.print("file exists")
+--- end
+--- <
+
local uv = vim.uv
local M = {}
@@ -93,14 +105,23 @@ function M.basename(file)
return file:match('/$') and '' or (file:match('[^/]*$'))
end
---- Concatenate directories and/or file paths into a single path with normalization
---- (e.g., `"foo/"` and `"bar"` get joined to `"foo/bar"`)
+--- Concatenates partial paths (one absolute or relative path followed by zero or more relative
+--- paths). Slashes are normalized: redundant slashes are removed, and (on Windows) backslashes are
+--- replaced with forward-slashes.
+---
+--- Examples:
+--- - "foo/", "/bar" => "foo/bar"
+--- - Windows: "a\foo\", "\bar" => "a/foo/bar"
---
---@since 12
---@param ... string
---@return string
function M.joinpath(...)
- return (table.concat({ ... }, '/'):gsub('//+', '/'))
+ local path = table.concat({ ... }, '/')
+ if iswin then
+ path = path:gsub('\\', '/')
+ end
+ return (path:gsub('//+', '/'))
end
---@alias Iterator fun(): string?, string?
@@ -115,6 +136,7 @@ end
--- - skip: (fun(dir_name: string): boolean)|nil Predicate
--- to control traversal. Return false to stop searching the current directory.
--- Only useful when depth > 1
+--- - follow: boolean|nil Follow symbolic links. (default: true)
---
---@return Iterator over items in {path}. Each iteration yields two values: "name" and "type".
--- "name" is the basename of the item relative to {path}.
@@ -126,6 +148,7 @@ function M.dir(path, opts)
vim.validate('path', path, 'string')
vim.validate('depth', opts.depth, 'number', true)
vim.validate('skip', opts.skip, 'function', true)
+ vim.validate('follow', opts.follow, 'boolean', true)
path = M.normalize(path)
if not opts.depth or opts.depth == 1 then
@@ -156,7 +179,9 @@ function M.dir(path, opts)
if
opts.depth
and level < opts.depth
- and t == 'directory'
+ and (t == 'directory' or (t == 'link' and opts.follow ~= false and (vim.uv.fs_stat(
+ M.joinpath(path, f)
+ ) or {}).type == 'directory'))
and (not opts.skip or opts.skip(f) ~= false)
then
dirs[#dirs + 1] = { f, level + 1 }
@@ -190,6 +215,10 @@ end
--- Use `math.huge` to place no limit on the number of matches.
--- (default: `1`)
--- @field limit? number
+---
+--- Follow symbolic links.
+--- (default: `true`)
+--- @field follow? boolean
--- Find files or directories (or other items as specified by `opts.type`) in the given path.
---
@@ -213,7 +242,7 @@ end
---
--- -- get all files ending with .cpp or .hpp inside lib/
--- local cpp_hpp = vim.fs.find(function(name, path)
---- return name:match('.*%.[ch]pp$') and path:match('[/\\\\]lib$')
+--- return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$')
--- end, {limit = math.huge, type = 'file'})
--- ```
---
@@ -223,6 +252,7 @@ end
--- If {names} is a function, it is called for each traversed item with args:
--- - name: base name of the current item
--- - path: full path of the current item
+---
--- The function should return `true` if the given item is considered a match.
---
---@param opts vim.fs.find.Opts Optional keyword arguments:
@@ -235,6 +265,7 @@ function M.find(names, opts)
vim.validate('stop', opts.stop, 'string', true)
vim.validate('type', opts.type, 'string', true)
vim.validate('limit', opts.limit, 'number', true)
+ vim.validate('follow', opts.follow, 'boolean', true)
if type(names) == 'string' then
names = { names }
@@ -324,7 +355,14 @@ function M.find(names, opts)
end
end
- if type_ == 'directory' then
+ if
+ type_ == 'directory'
+ or (
+ type_ == 'link'
+ and opts.follow ~= false
+ and (vim.uv.fs_stat(f) or {}).type == 'directory'
+ )
+ then
dirs[#dirs + 1] = f
end
end
@@ -493,6 +531,27 @@ local function path_resolve_dot(path)
return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/')
end
+--- Expand tilde (~) character at the beginning of the path to the user's home directory.
+---
+--- @param path string Path to expand.
+--- @param sep string|nil Path separator to use. Uses os_sep by default.
+--- @return string Expanded path.
+local function expand_home(path, sep)
+ sep = sep or os_sep
+
+ if vim.startswith(path, '~') then
+ local home = uv.os_homedir() or '~' --- @type string
+
+ if home:sub(-1) == sep then
+ home = home:sub(1, -2)
+ end
+
+ path = home .. path:sub(2) --- @type string
+ end
+
+ return path
+end
+
--- @class vim.fs.normalize.Opts
--- @inlinedoc
---
@@ -556,18 +615,12 @@ function M.normalize(path, opts)
return ''
end
- -- Expand ~ to users home directory
- if vim.startswith(path, '~') then
- local home = uv.os_homedir() or '~'
- if home:sub(-1) == os_sep_local then
- home = home:sub(1, -2)
- end
- path = home .. path:sub(2)
- end
+ -- Expand ~ to user's home directory
+ path = expand_home(path, os_sep_local)
-- Expand environment variables if `opts.expand_env` isn't `false`
if opts.expand_env == nil or opts.expand_env then
- path = path:gsub('%$([%w_]+)', uv.os_getenv)
+ path = path:gsub('%$([%w_]+)', uv.os_getenv) --- @type string
end
if win then
@@ -593,8 +646,8 @@ function M.normalize(path, opts)
return prefix .. path
end
- -- Remove extraneous slashes from the prefix
- prefix = prefix:gsub('/+', '/')
+ -- Ensure capital drive and remove extraneous slashes from the prefix
+ prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/')
end
if not opts._fast then
@@ -667,4 +720,75 @@ function M.rm(path, opts)
end
end
+--- Convert path to an absolute path. A tilde (~) character at the beginning of the path is expanded
+--- to the user's home directory. Does not check if the path exists, normalize the path, resolve
+--- symlinks or hardlinks (including `.` and `..`), or expand environment variables. If the path is
+--- already absolute, it is returned unchanged. Also converts `\` path separators to `/`.
+---
+--- @param path string Path
+--- @return string Absolute path
+function M.abspath(path)
+ vim.validate('path', path, 'string')
+
+ -- Expand ~ to user's home directory
+ path = expand_home(path)
+
+ -- Convert path separator to `/`
+ path = path:gsub(os_sep, '/')
+
+ local prefix = ''
+
+ if iswin then
+ prefix, path = split_windows_path(path)
+ end
+
+ if prefix == '//' or vim.startswith(path, '/') then
+ -- Path is already absolute, do nothing
+ return prefix .. path
+ end
+
+ -- Windows allows paths like C:foo/bar, these paths are relative to the current working directory
+ -- of the drive specified in the path
+ local cwd = (iswin and prefix:match('^%w:$')) and uv.fs_realpath(prefix) or uv.cwd()
+ assert(cwd ~= nil)
+ -- Convert cwd path separator to `/`
+ cwd = cwd:gsub(os_sep, '/')
+
+ -- Prefix is not needed for expanding relative paths, as `cwd` already contains it.
+ return M.joinpath(cwd, path)
+end
+
+--- Gets `target` path relative to `base`, or `nil` if `base` is not an ancestor.
+---
+--- Example:
+---
+--- ```lua
+--- vim.fs.relpath('/var', '/var/lib') -- 'lib'
+--- vim.fs.relpath('/var', '/usr/bin') -- nil
+--- ```
+---
+--- @param base string
+--- @param target string
+--- @param opts table? Reserved for future use
+--- @return string|nil
+function M.relpath(base, target, opts)
+ vim.validate('base', base, 'string')
+ vim.validate('target', target, 'string')
+ vim.validate('opts', opts, 'table', true)
+
+ base = vim.fs.normalize(vim.fs.abspath(base))
+ target = vim.fs.normalize(vim.fs.abspath(target))
+ if base == target then
+ return '.'
+ end
+
+ local prefix = ''
+ if iswin then
+ prefix, base = split_windows_path(base)
+ end
+ base = prefix .. base .. (base ~= '/' and '/' or '')
+
+ return vim.startswith(target, base) and target:sub(#base + 1) or nil
+end
+
return M
diff --git a/runtime/lua/vim/func.lua b/runtime/lua/vim/func.lua
index f71659ffb4..fc8fa62c71 100644
--- a/runtime/lua/vim/func.lua
+++ b/runtime/lua/vim/func.lua
@@ -3,9 +3,6 @@ local M = {}
-- TODO(lewis6991): Private for now until:
-- - There are other places in the codebase that could benefit from this
-- (e.g. LSP), but might require other changes to accommodate.
--- - Invalidation of the cache needs to be controllable. Using weak tables
--- is an acceptable invalidation policy, but it shouldn't be the only
--- one.
-- - I don't think the story around `hash` is completely thought out. We
-- may be able to have a good default hash by hashing each argument,
-- so basically a better 'concat'.
@@ -17,6 +14,10 @@ local M = {}
--- Internally uses a |lua-weaktable| to cache the results of {fn} meaning the
--- cache will be invalidated whenever Lua does garbage collection.
---
+--- The cache can also be manually invalidated by calling `:clear()` on the returned object.
+--- Calling this function with no arguments clears the entire cache; otherwise, the arguments will
+--- be interpreted as function inputs, and only the cache entry at their hash will be cleared.
+---
--- The memoized function returns shared references so be wary about
--- mutating return values.
---
@@ -32,11 +33,12 @@ local M = {}
--- first n arguments passed to {fn}.
---
--- @param fn F Function to memoize.
---- @param strong? boolean Do not use a weak table
+--- @param weak? boolean Use a weak table (default `true`)
--- @return F # Memoized version of {fn}
--- @nodoc
-function M._memoize(hash, fn, strong)
- return require('vim.func._memoize')(hash, fn, strong)
+function M._memoize(hash, fn, weak)
+ -- this is wrapped in a function to lazily require the module
+ return require('vim.func._memoize')(hash, fn, weak)
end
return M
diff --git a/runtime/lua/vim/func/_memoize.lua b/runtime/lua/vim/func/_memoize.lua
index 6e557905a7..c46f878067 100644
--- a/runtime/lua/vim/func/_memoize.lua
+++ b/runtime/lua/vim/func/_memoize.lua
@@ -1,5 +1,7 @@
--- Module for private utility functions
+--- @alias vim.func.MemoObj { _hash: (fun(...): any), _weak: boolean?, _cache: table<any> }
+
--- @param argc integer?
--- @return fun(...): any
local function concat_hash(argc)
@@ -33,29 +35,49 @@ local function resolve_hash(hash)
return hash
end
+--- @param weak boolean?
+--- @return table
+local create_cache = function(weak)
+ return setmetatable({}, {
+ __mode = weak ~= false and 'kv',
+ })
+end
+
--- @generic F: function
--- @param hash integer|string|fun(...): any
--- @param fn F
---- @param strong? boolean
+--- @param weak? boolean
--- @return F
-return function(hash, fn, strong)
+return function(hash, fn, weak)
vim.validate('hash', hash, { 'number', 'string', 'function' })
vim.validate('fn', fn, 'function')
+ vim.validate('weak', weak, 'boolean', true)
- ---@type table<any,table<any,any>>
- local cache = {}
- if not strong then
- setmetatable(cache, { __mode = 'kv' })
- end
-
- hash = resolve_hash(hash)
+ --- @type vim.func.MemoObj
+ local obj = {
+ _cache = create_cache(weak),
+ _hash = resolve_hash(hash),
+ _weak = weak,
+ --- @param self vim.func.MemoObj
+ clear = function(self, ...)
+ if select('#', ...) == 0 then
+ self._cache = create_cache(self._weak)
+ return
+ end
+ local key = self._hash(...)
+ self._cache[key] = nil
+ end,
+ }
- return function(...)
- local key = hash(...)
- if cache[key] == nil then
- cache[key] = vim.F.pack_len(fn(...))
- end
-
- return vim.F.unpack_len(cache[key])
- end
+ return setmetatable(obj, {
+ --- @param self vim.func.MemoObj
+ __call = function(self, ...)
+ local key = self._hash(...)
+ local cache = self._cache
+ if cache[key] == nil then
+ cache[key] = vim.F.pack_len(fn(...))
+ end
+ return vim.F.unpack_len(cache[key])
+ end,
+ })
end
diff --git a/runtime/lua/vim/glob.lua b/runtime/lua/vim/glob.lua
index 4f86d5e1ca..242c70d4b2 100644
--- a/runtime/lua/vim/glob.lua
+++ b/runtime/lua/vim/glob.lua
@@ -53,6 +53,7 @@ function M.to_lpeg(pattern)
end
-- luacheck: pop
+ --- @diagnostic disable-next-line: missing-fields
local p = P({
'Pattern',
Pattern = V('Elem') ^ -1 * V('End'),
diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua
index 52a7a13966..a265e2b901 100644
--- a/runtime/lua/vim/health.lua
+++ b/runtime/lua/vim/health.lua
@@ -11,7 +11,7 @@
--- <
--- Plugin authors are encouraged to write new healthchecks. |health-dev|
---
---- Commands *health-commands*
+--- COMMANDS *health-commands*
---
--- *:che* *:checkhealth*
--- :che[ckhealth] Run all healthchecks.
@@ -39,6 +39,23 @@
--- :checkhealth vim*
--- <
---
+--- USAGE *health-usage*
+---
+--- Local mappings in the healthcheck buffer:
+---
+--- q Closes the window.
+---
+--- Global configuration:
+---
+--- *g:health*
+--- g:health Dictionary with the following optional keys:
+--- - `style` (`'float'|nil`) Set to "float" to display :checkhealth in
+--- a floating window instead of the default behavior.
+---
+--- Example: >lua
+--- vim.g.health = { style = 'float' }
+---
+--- --------------------------------------------------------------------------------
--- Create a healthcheck *health-dev*
---
--- Healthchecks are functions that check the user environment, configuration, or
@@ -101,7 +118,7 @@ local function filepath_to_healthcheck(path)
func = 'health#' .. name .. '#check'
filetype = 'v'
else
- local subpath = path:gsub('.*lua/', '')
+ local subpath = path:gsub('.*/lua/', '')
if vim.fs.basename(subpath) == 'health.lua' then
-- */health.lua
name = vim.fs.dirname(subpath)
@@ -109,7 +126,7 @@ local function filepath_to_healthcheck(path)
-- */health/init.lua
name = vim.fs.dirname(vim.fs.dirname(subpath))
end
- name = name:gsub('/', '.')
+ name = assert(name:gsub('/', '.')) --- @type string
func = 'require("' .. name .. '.health").check()'
filetype = 'l'
@@ -218,7 +235,7 @@ local function format_report_message(status, msg, ...)
-- Report each suggestion
for _, v in ipairs(varargs) do
if v then
- output = output .. '\n - ' .. indent_after_line1(v, 6)
+ output = output .. '\n - ' .. indent_after_line1(v, 6) --- @type string
end
end
end
@@ -331,13 +348,31 @@ function M._check(mods, plugin_names)
local emptybuf = vim.fn.bufnr('$') == 1 and vim.fn.getline(1) == '' and 1 == vim.fn.line('$')
- -- When no command modifiers are used:
- -- - If the current buffer is empty, open healthcheck directly.
- -- - If not specified otherwise open healthcheck in a tab.
- local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer'
-
local bufnr = vim.api.nvim_create_buf(true, true)
- vim.cmd(buf_cmd .. ' ' .. bufnr)
+ if
+ vim.g.health
+ and type(vim.g.health) == 'table'
+ and vim.tbl_get(vim.g.health, 'style') == 'float'
+ then
+ local max_height = math.floor(vim.o.lines * 0.8)
+ local max_width = 80
+ local float_bufnr, float_winid = vim.lsp.util.open_floating_preview({}, '', {
+ height = max_height,
+ width = max_width,
+ offset_x = math.floor((vim.o.columns - max_width) / 2),
+ offset_y = math.floor((vim.o.lines - max_height) / 2) - 1,
+ relative = 'editor',
+ })
+ vim.api.nvim_set_current_win(float_winid)
+ vim.bo[float_bufnr].modifiable = true
+ vim.wo[float_winid].list = false
+ else
+ -- When no command modifiers are used:
+ -- - If the current buffer is empty, open healthcheck directly.
+ -- - If not specified otherwise open healthcheck in a tab.
+ local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer'
+ vim.cmd(buf_cmd .. ' ' .. bufnr)
+ end
if vim.fn.bufexists('health://') == 1 then
vim.cmd.bwipe('health://')
@@ -407,6 +442,16 @@ function M._check(mods, plugin_names)
-- Clear the 'Running healthchecks...' message.
vim.cmd.redraw()
vim.print('')
+
+ -- Quit with 'q' inside healthcheck buffers.
+ vim.keymap.set('n', 'q', function()
+ if not pcall(vim.cmd.close) then
+ vim.cmd.bdelete()
+ end
+ end, { buffer = bufnr, silent = true, noremap = true, nowait = true })
+
+ -- Once we're done writing checks, set nomodifiable.
+ vim.bo[bufnr].modifiable = false
end
return M
diff --git a/runtime/lua/vim/health/health.lua b/runtime/lua/vim/health/health.lua
index d226f35f9a..dd6fe7f608 100644
--- a/runtime/lua/vim/health/health.lua
+++ b/runtime/lua/vim/health/health.lua
@@ -183,13 +183,16 @@ end
local function check_rplugin_manifest()
health.start('Remote Plugins')
- local existing_rplugins = {}
- for _, item in ipairs(vim.fn['remote#host#PluginsForHost']('python3')) do
+ local existing_rplugins = {} --- @type table<string,string>
+ --- @type {path:string}[]
+ local items = vim.fn['remote#host#PluginsForHost']('python3')
+ for _, item in ipairs(items) do
existing_rplugins[item.path] = 'python3'
end
local require_update = false
local handle_path = function(path)
+ --- @type string[]
local python_glob = vim.fn.glob(path .. '/rplugin/python*', true, true)
if vim.tbl_isempty(python_glob) then
return
@@ -198,6 +201,7 @@ local function check_rplugin_manifest()
local python_dir = python_glob[1]
local python_version = vim.fs.basename(python_dir)
+ --- @type string[]
local scripts = vim.fn.glob(python_dir .. '/*.py', true, true)
vim.list_extend(scripts, vim.fn.glob(python_dir .. '/*/__init__.py', true, true))
@@ -227,7 +231,10 @@ local function check_rplugin_manifest()
end
end
- for _, path in ipairs(vim.fn.map(vim.split(vim.o.runtimepath, ','), 'resolve(v:val)')) do
+ --- @type string[]
+ local paths = vim.fn.map(vim.split(vim.o.runtimepath, ','), 'resolve(v:val)')
+
+ for _, path in ipairs(paths) do
handle_path(path)
end
diff --git a/runtime/lua/vim/hl.lua b/runtime/lua/vim/hl.lua
index 099efa3c61..070748d31e 100644
--- a/runtime/lua/vim/hl.lua
+++ b/runtime/lua/vim/hl.lua
@@ -17,6 +17,9 @@ M.priorities = {
user = 200,
}
+local range_timer --- @type uv.uv_timer_t?
+local range_hl_clear --- @type fun()?
+
--- @class vim.hl.range.Opts
--- @inlinedoc
---
@@ -31,6 +34,10 @@ M.priorities = {
--- Highlight priority
--- (default: `vim.hl.priorities.user`)
--- @field priority? integer
+---
+--- Time in ms before highlight is cleared
+--- (default: -1 no timeout)
+--- @field timeout? integer
--- Apply highlight group to range of text.
---
@@ -45,6 +52,7 @@ function M.range(bufnr, ns, higroup, start, finish, opts)
local regtype = opts.regtype or 'v'
local inclusive = opts.inclusive or false
local priority = opts.priority or M.priorities.user
+ local timeout = opts.timeout or -1
local v_maxcol = vim.v.maxcol
@@ -100,6 +108,19 @@ function M.range(bufnr, ns, higroup, start, finish, opts)
end
end
+ if range_timer and not range_timer:is_closing() then
+ range_timer:close()
+ assert(range_hl_clear)
+ range_hl_clear()
+ end
+
+ range_hl_clear = function()
+ range_timer = nil
+ range_hl_clear = nil
+ pcall(vim.api.nvim_buf_clear_namespace, bufnr, ns, 0, -1)
+ pcall(vim.api.nvim__ns_set, { wins = {} })
+ end
+
for _, res in ipairs(region) do
local start_row = res[1][2] - 1
local start_col = res[1][3] - 1
@@ -113,11 +134,13 @@ function M.range(bufnr, ns, higroup, start, finish, opts)
strict = false,
})
end
+
+ if timeout ~= -1 then
+ range_timer = vim.defer_fn(range_hl_clear, timeout)
+ end
end
-local yank_ns = api.nvim_create_namespace('hlyank')
-local yank_timer --- @type uv.uv_timer_t?
-local yank_cancel --- @type fun()?
+local yank_ns = api.nvim_create_namespace('nvim.hlyank')
--- Highlight the yanked text during a |TextYankPost| event.
---
@@ -152,31 +175,17 @@ function M.on_yank(opts)
end
local higroup = opts.higroup or 'IncSearch'
- local timeout = opts.timeout or 150
local bufnr = vim.api.nvim_get_current_buf()
local winid = vim.api.nvim_get_current_win()
- if yank_timer then
- yank_timer:close()
- assert(yank_cancel)
- yank_cancel()
- end
vim.api.nvim__ns_set(yank_ns, { wins = { winid } })
M.range(bufnr, yank_ns, higroup, "'[", "']", {
regtype = event.regtype,
inclusive = event.inclusive,
priority = opts.priority or M.priorities.user,
+ timeout = opts.timeout or 150,
})
-
- yank_cancel = function()
- yank_timer = nil
- yank_cancel = nil
- pcall(vim.api.nvim_buf_clear_namespace, bufnr, yank_ns, 0, -1)
- pcall(vim.api.nvim__ns_set, { wins = {} })
- end
-
- yank_timer = vim.defer_fn(yank_cancel, timeout)
end
return M
diff --git a/runtime/lua/vim/inspect.lua b/runtime/lua/vim/inspect.lua
index c232f69590..cdf34897d4 100644
--- a/runtime/lua/vim/inspect.lua
+++ b/runtime/lua/vim/inspect.lua
@@ -1,3 +1,4 @@
+--- @diagnostic disable: no-unknown
local inspect = {
_VERSION = 'inspect.lua 3.1.0',
_URL = 'http://github.com/kikito/inspect.lua',
diff --git a/runtime/lua/vim/loader.lua b/runtime/lua/vim/loader.lua
index 0cce0ab21d..c7158673fe 100644
--- a/runtime/lua/vim/loader.lua
+++ b/runtime/lua/vim/loader.lua
@@ -399,50 +399,57 @@ function M.reset(path)
end
end
---- Enables the experimental Lua module loader:
---- * overrides loadfile
+--- Enables or disables the experimental Lua module loader:
+---
+--- Enable (`enable=true`):
+--- * overrides |loadfile()|
--- * adds the Lua loader using the byte-compilation cache
--- * adds the libs loader
--- * removes the default Nvim loader
---
---- @since 0
-function M.enable()
- if M.enabled then
- return
- end
- M.enabled = true
- vim.fn.mkdir(vim.fn.fnamemodify(M.path, ':p'), 'p')
- _G.loadfile = loadfile_cached
- -- add Lua loader
- table.insert(loaders, 2, loader_cached)
- -- add libs loader
- table.insert(loaders, 3, loader_lib_cached)
- -- remove Nvim loader
- for l, loader in ipairs(loaders) do
- if loader == vim._load_package then
- table.remove(loaders, l)
- break
- end
- end
-end
-
---- Disables the experimental Lua module loader:
+--- Disable (`enable=false`):
--- * removes the loaders
--- * adds the default Nvim loader
---
--- @since 0
-function M.disable()
- if not M.enabled then
+---
+--- @param enable? (boolean) true/nil to enable, false to disable
+function M.enable(enable)
+ enable = enable == nil and true or enable
+ if enable == M.enabled then
return
end
- M.enabled = false
- _G.loadfile = _loadfile
- for l, loader in ipairs(loaders) do
- if loader == loader_cached or loader == loader_lib_cached then
- table.remove(loaders, l)
+ M.enabled = enable
+
+ if enable then
+ vim.fn.mkdir(vim.fn.fnamemodify(M.path, ':p'), 'p')
+ _G.loadfile = loadfile_cached
+ -- add Lua loader
+ table.insert(loaders, 2, loader_cached)
+ -- add libs loader
+ table.insert(loaders, 3, loader_lib_cached)
+ -- remove Nvim loader
+ for l, loader in ipairs(loaders) do
+ if loader == vim._load_package then
+ table.remove(loaders, l)
+ break
+ end
+ end
+ else
+ _G.loadfile = _loadfile
+ for l, loader in ipairs(loaders) do
+ if loader == loader_cached or loader == loader_lib_cached then
+ table.remove(loaders, l)
+ end
end
+ table.insert(loaders, 2, vim._load_package)
end
- table.insert(loaders, 2, vim._load_package)
+end
+
+--- @deprecated
+function M.disable()
+ vim.deprecate('vim.loader.disable', 'vim.loader.enable(false)', '0.12')
+ vim.loader.enable(false)
end
--- Tracks the time spent in a function
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 0de3b4ee4d..a45f9adeb6 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -3,6 +3,7 @@ local validate = vim.validate
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'
@@ -57,6 +58,7 @@ lsp._request_name_to_capability = {
[ms.textDocument_documentHighlight] = { 'documentHighlightProvider' },
[ms.textDocument_documentLink] = { 'documentLinkProvider' },
[ms.textDocument_documentSymbol] = { 'documentSymbolProvider' },
+ [ms.textDocument_foldingRange] = { 'foldingRangeProvider' },
[ms.textDocument_formatting] = { 'documentFormattingProvider' },
[ms.textDocument_hover] = { 'hoverProvider' },
[ms.textDocument_implementation] = { 'implementationProvider' },
@@ -87,18 +89,6 @@ lsp._request_name_to_capability = {
-- TODO improve handling of scratch buffers with LSP attached.
---- Returns the buffer number for the given {bufnr}.
----
----@param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer
----@return integer bufnr
-local function resolve_bufnr(bufnr)
- validate('bufnr', bufnr, 'number', true)
- if bufnr == nil or bufnr == 0 then
- return api.nvim_get_current_buf()
- end
- return bufnr
-end
-
---@private
--- Called by the client when trying to call a method that's not
--- supported in any of the servers registered for the current buffer.
@@ -112,6 +102,22 @@ function lsp._unsupported_method(method)
return msg
end
+---@private
+---@param workspace_folders string|lsp.WorkspaceFolder[]?
+---@return lsp.WorkspaceFolder[]?
+function lsp._get_workspace_folders(workspace_folders)
+ if type(workspace_folders) == 'table' then
+ return workspace_folders
+ elseif type(workspace_folders) == 'string' then
+ return {
+ {
+ uri = vim.uri_from_fname(workspace_folders),
+ name = workspace_folders,
+ },
+ }
+ end
+end
+
local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' }
local format_line_ending = {
@@ -194,34 +200,393 @@ local function reuse_client_default(client, config)
return false
end
- if config.root_dir then
- local root = vim.uri_from_fname(config.root_dir)
- for _, dir in ipairs(client.workspace_folders or {}) do
- -- note: do not need to check client.root_dir since that should be client.workspace_folders[1]
- if root == dir.uri then
- return true
+ local config_folders = lsp._get_workspace_folders(config.workspace_folders or config.root_dir)
+
+ if not config_folders or not next(config_folders) then
+ -- Reuse if the client was configured with no workspace folders
+ local client_config_folders =
+ lsp._get_workspace_folders(client.config.workspace_folders or client.config.root_dir)
+ return not client_config_folders or not next(client_config_folders)
+ end
+
+ for _, config_folder in ipairs(config_folders) do
+ local found = false
+ for _, client_folder in ipairs(client.workspace_folders or {}) do
+ if config_folder.uri == client_folder.uri then
+ found = true
+ break
end
end
+ if not found then
+ return false
+ end
end
- -- TODO(lewis6991): also check config.workspace_folders
+ return true
+end
- return false
+--- Reset defaults set by `set_defaults`.
+--- Must only be called if the last client attached to a buffer exits.
+local function reset_defaults(bufnr)
+ if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then
+ vim.bo[bufnr].tagfunc = nil
+ end
+ if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then
+ vim.bo[bufnr].omnifunc = nil
+ end
+ if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then
+ vim.bo[bufnr].formatexpr = nil
+ end
+ vim._with({ buf = bufnr }, function()
+ local keymap = vim.fn.maparg('K', 'n', false, true)
+ if keymap and keymap.callback == vim.lsp.buf.hover and keymap.buffer == 1 then
+ vim.keymap.del('n', 'K', { buffer = bufnr })
+ end
+ end)
end
---- @class vim.lsp.start.Opts
---- @inlinedoc
+--- @param code integer
+--- @param signal integer
+--- @param client_id integer
+local function on_client_exit(code, signal, client_id)
+ local client = all_clients[client_id]
+
+ vim.schedule(function()
+ for bufnr in pairs(client.attached_buffers) do
+ if client and client.attached_buffers[bufnr] and api.nvim_buf_is_valid(bufnr) then
+ api.nvim_exec_autocmds('LspDetach', {
+ buffer = bufnr,
+ modeline = false,
+ data = { client_id = client_id },
+ })
+ end
+
+ client.attached_buffers[bufnr] = nil
+
+ if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then
+ reset_defaults(bufnr)
+ end
+ end
+
+ local namespace = vim.lsp.diagnostic.get_namespace(client_id)
+ vim.diagnostic.reset(namespace)
+ end)
+
+ local name = client.name or 'unknown'
+
+ -- Schedule the deletion of the client object so that it exists in the execution of LspDetach
+ -- autocommands
+ vim.schedule(function()
+ all_clients[client_id] = nil
+
+ -- Client can be absent if executable starts, but initialize fails
+ -- init/attach won't have happened
+ if client then
+ changetracking.reset(client)
+ end
+ if code ~= 0 or (signal ~= 0 and signal ~= 15) then
+ local msg = string.format(
+ 'Client %s quit with exit code %s and signal %s. Check log for errors: %s',
+ name,
+ code,
+ signal,
+ lsp.get_log_path()
+ )
+ vim.notify(msg, vim.log.levels.WARN)
+ end
+ end)
+end
+
+--- Creates and initializes a client with the given configuration.
+--- @param config vim.lsp.ClientConfig Configuration for the server.
+--- @return integer? client_id |vim.lsp.get_client_by_id()| Note: client may not be
+--- fully initialized. Use `on_init` to do any actions once
+--- the client has been initialized.
+--- @return string? # Error message, if any
+local function create_and_initialize_client(config)
+ local ok, res = pcall(require('vim.lsp.client').create, config)
+ if not ok then
+ return nil, res --[[@as string]]
+ end
+
+ local client = assert(res)
+
+ --- @diagnostic disable-next-line: invisible
+ table.insert(client._on_exit_cbs, on_client_exit)
+
+ all_clients[client.id] = client
+
+ client:initialize()
+
+ return client.id, nil
+end
+
+--- @class vim.lsp.Config : vim.lsp.ClientConfig
+---
+--- See `cmd` in [vim.lsp.ClientConfig].
+--- @field cmd? string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient
+---
+--- Filetypes the client will attach to, if activated by `vim.lsp.enable()`.
+--- If not provided, then the client will attach to all filetypes.
+--- @field filetypes? string[]
+---
+--- Directory markers (.e.g. '.git/') where the LSP server will base its workspaceFolders,
+--- rootUri, and rootPath on initialization. Unused if `root_dir` is provided.
+--- @field root_markers? string[]
+---
+--- Directory where the LSP server will base its workspaceFolders, rootUri, and rootPath on
+--- initialization. If a function, it accepts a single callback argument which must be called with
+--- the value of root_dir to use. The LSP server will not be started until the callback is called.
+--- @field root_dir? string|fun(cb:fun(string))
---
--- Predicate used to decide if a client should be re-used. Used on all
--- running clients. The default implementation re-uses a client if name and
--- root_dir matches.
--- @field reuse_client? fun(client: vim.lsp.Client, config: vim.lsp.ClientConfig): boolean
+
+--- Update the configuration for an LSP client.
+---
+--- Use name '*' to set default configuration for all clients.
+---
+--- Can also be table-assigned to redefine the configuration for a client.
+---
+--- Examples:
+---
+--- - Add a root marker for all clients:
+--- ```lua
+--- vim.lsp.config('*', {
+--- root_markers = { '.git' },
+--- })
+--- ```
+--- - Add additional capabilities to all clients:
+--- ```lua
+--- vim.lsp.config('*', {
+--- capabilities = {
+--- textDocument = {
+--- semanticTokens = {
+--- multilineTokenSupport = true,
+--- }
+--- }
+--- }
+--- })
+--- ```
+--- - (Re-)define the configuration for clangd:
+--- ```lua
+--- vim.lsp.config.clangd = {
+--- cmd = {
+--- 'clangd',
+--- '--clang-tidy',
+--- '--background-index',
+--- '--offset-encoding=utf-8',
+--- },
+--- root_markers = { '.clangd', 'compile_commands.json' },
+--- filetypes = { 'c', 'cpp' },
+--- }
+--- ```
+--- - Get configuration for luals:
+--- ```lua
+--- local cfg = vim.lsp.config.luals
+--- ```
+---
+--- @param name string
+--- @param cfg vim.lsp.Config
+--- @diagnostic disable-next-line:assign-type-mismatch
+function lsp.config(name, cfg)
+ local _, _ = name, cfg -- ignore unused
+ -- dummy proto for docs
+end
+
+lsp._enabled_configs = {} --- @type table<string,{resolved_config:vim.lsp.Config?}>
+
+--- If a config in vim.lsp.config() is accessed then the resolved config becomes invalid.
+--- @param name string
+local function invalidate_enabled_config(name)
+ if name == '*' then
+ for _, v in pairs(lsp._enabled_configs) do
+ v.resolved_config = nil
+ end
+ elseif lsp._enabled_configs[name] then
+ lsp._enabled_configs[name].resolved_config = nil
+ end
+end
+
+--- @nodoc
+--- @class vim.lsp.config
+--- @field [string] vim.lsp.Config
+--- @field package _configs table<string,vim.lsp.Config>
+lsp.config = setmetatable({ _configs = {} }, {
+ --- @param self vim.lsp.config
+ --- @param name string
+ --- @return vim.lsp.Config
+ __index = function(self, name)
+ validate('name', name, 'string')
+
+ local rconfig = lsp._enabled_configs[name] or {}
+ self._configs[name] = self._configs[name] or {}
+
+ if not rconfig.resolved_config then
+ -- Resolve configs from lsp/*.lua
+ -- Calls to vim.lsp.config in lsp/* have a lower precedence than calls from other sites.
+ local rtp_config = {} ---@type vim.lsp.Config
+ for _, v in ipairs(api.nvim_get_runtime_file(('lsp/%s.lua'):format(name), true)) do
+ local config = assert(loadfile(v))() ---@type any?
+ if type(config) == 'table' then
+ rtp_config = vim.tbl_deep_extend('force', rtp_config, config)
+ else
+ log.warn(string.format('%s does not return a table, ignoring', v))
+ end
+ end
+
+ rconfig.resolved_config = vim.tbl_deep_extend(
+ 'force',
+ lsp.config._configs['*'] or {},
+ rtp_config,
+ lsp.config._configs[name] or {}
+ )
+ rconfig.resolved_config.name = name
+ end
+
+ return rconfig.resolved_config
+ end,
+
+ --- @param self vim.lsp.config
+ --- @param name string
+ --- @param cfg vim.lsp.Config
+ __newindex = function(self, name, cfg)
+ validate('name', name, 'string')
+ validate('cfg', cfg, 'table')
+ invalidate_enabled_config(name)
+ self._configs[name] = cfg
+ end,
+
+ --- @param self vim.lsp.config
+ --- @param name string
+ --- @param cfg vim.lsp.Config
+ __call = function(self, name, cfg)
+ validate('name', name, 'string')
+ validate('cfg', cfg, 'table')
+ invalidate_enabled_config(name)
+ self[name] = vim.tbl_deep_extend('force', self._configs[name] or {}, cfg)
+ end,
+})
+
+local lsp_enable_autocmd_id --- @type integer?
+
+--- @param bufnr integer
+local function lsp_enable_callback(bufnr)
+ -- Only ever attach to buffers that represent an actual file.
+ if vim.bo[bufnr].buftype ~= '' then
+ return
+ end
+
+ --- @param config vim.lsp.Config
+ local function can_start(config)
+ if config.filetypes and not vim.tbl_contains(config.filetypes, vim.bo[bufnr].filetype) then
+ return false
+ elseif type(config.cmd) == 'table' and vim.fn.executable(config.cmd[1]) == 0 then
+ return false
+ end
+
+ return true
+ end
+
+ --- @param config vim.lsp.Config
+ local function start(config)
+ return vim.lsp.start(config, {
+ bufnr = bufnr,
+ reuse_client = config.reuse_client,
+ _root_markers = config.root_markers,
+ })
+ end
+
+ for name in vim.spairs(lsp._enabled_configs) do
+ local config = lsp.config[name]
+ validate('cmd', config.cmd, { 'function', 'table' })
+ validate('cmd', config.reuse_client, 'function', true)
+
+ if can_start(config) then
+ -- Deepcopy config so changes done in the client
+ -- do not propagate back to the enabled configs.
+ config = vim.deepcopy(config)
+
+ if type(config.root_dir) == 'function' then
+ ---@param root_dir string
+ config.root_dir(function(root_dir)
+ config.root_dir = root_dir
+ vim.schedule(function()
+ start(config)
+ end)
+ end)
+ else
+ start(config)
+ end
+ end
+ end
+end
+
+--- Enable an LSP server to automatically start when opening a buffer.
+---
+--- Uses configuration defined with `vim.lsp.config`.
+---
+--- Examples:
+---
+--- ```lua
+--- vim.lsp.enable('clangd')
+---
+--- vim.lsp.enable({'luals', 'pyright'})
+--- ```
+---
+--- @param name string|string[] Name(s) of client(s) to enable.
+--- @param enable? boolean `true|nil` to enable, `false` to disable.
+function lsp.enable(name, enable)
+ validate('name', name, { 'string', 'table' })
+
+ local names = vim._ensure_list(name) --[[@as string[] ]]
+ for _, nm in ipairs(names) do
+ if nm == '*' then
+ error('Invalid name')
+ end
+ lsp._enabled_configs[nm] = enable ~= false and {} or nil
+ end
+
+ if not next(lsp._enabled_configs) then
+ if lsp_enable_autocmd_id then
+ api.nvim_del_autocmd(lsp_enable_autocmd_id)
+ lsp_enable_autocmd_id = nil
+ end
+ return
+ end
+
+ -- Only ever create autocmd once to reuse computation of config merging.
+ lsp_enable_autocmd_id = lsp_enable_autocmd_id
+ or api.nvim_create_autocmd('FileType', {
+ group = api.nvim_create_augroup('nvim.lsp.enable', {}),
+ callback = function(args)
+ lsp_enable_callback(args.buf)
+ end,
+ })
+end
+
+--- @class vim.lsp.start.Opts
+--- @inlinedoc
+---
+--- Predicate used to decide if a client should be re-used. Used on all
+--- running clients. The default implementation re-uses a client if it has the
+--- same name and if the given workspace folders (or root_dir) are all included
+--- in the client's workspace folders.
+--- @field reuse_client? fun(client: vim.lsp.Client, config: vim.lsp.ClientConfig): boolean
---
--- Buffer handle to attach to if starting or re-using a client (0 for current).
--- @field bufnr? integer
---
+--- Whether to attach the client to a buffer (default true).
+--- If set to `false`, `reuse_client` and `bufnr` will be ignored.
+--- @field attach? boolean
+---
--- Suppress error reporting if the LSP server fails to start (default false).
--- @field silent? boolean
+---
+--- @field package _root_markers? string[]
--- Create a new LSP client and start a language server or reuses an already
--- running client if one is found matching `name` and `root_dir`.
@@ -237,10 +602,10 @@ end
--- })
--- ```
---
---- See |vim.lsp.start_client()| for all available options. The most important are:
+--- See |vim.lsp.ClientConfig| for all available options. The most important are:
---
--- - `name` arbitrary name for the LSP client. Should be unique per language server.
---- - `cmd` command string[] or function, described at |vim.lsp.start_client()|.
+--- - `cmd` command string[] or function.
--- - `root_dir` path to the project root. By default this is used to decide if an existing client
--- should be re-used. The example above uses |vim.fs.root()| to detect the root by traversing
--- the file system upwards starting from the current directory until either a `pyproject.toml`
@@ -260,36 +625,46 @@ end
--- `ftplugin/<filetype_name>.lua` (See |ftplugin-name|)
---
--- @param config vim.lsp.ClientConfig Configuration for the server.
---- @param opts vim.lsp.start.Opts? Optional keyword arguments
+--- @param opts vim.lsp.start.Opts? Optional keyword arguments.
--- @return integer? client_id
function lsp.start(config, opts)
opts = opts or {}
local reuse_client = opts.reuse_client or reuse_client_default
- local bufnr = resolve_bufnr(opts.bufnr)
+ local bufnr = vim._resolve_bufnr(opts.bufnr)
+
+ if not config.root_dir and opts._root_markers then
+ config = vim.deepcopy(config)
+ config.root_dir = vim.fs.root(bufnr, opts._root_markers)
+ end
for _, client in pairs(all_clients) do
if reuse_client(client, config) then
+ if opts.attach == false then
+ return client.id
+ end
+
if lsp.buf_attach_client(bufnr, client.id) then
return client.id
- else
- return nil
end
+ return
end
end
- local client_id, err = lsp.start_client(config)
+ local client_id, err = create_and_initialize_client(config)
if err then
if not opts.silent then
vim.notify(err, vim.log.levels.WARN)
end
- return nil
+ return
end
- if client_id and lsp.buf_attach_client(bufnr, client_id) then
+ if opts.attach == false then
return client_id
end
- return nil
+ if client_id and lsp.buf_attach_client(bufnr, client_id) then
+ return client_id
+ end
end
--- Consumes the latest progress messages from all clients and formats them as a string.
@@ -349,17 +724,17 @@ end
---@param bufnr integer
function lsp._set_defaults(client, bufnr)
if
- client.supports_method(ms.textDocument_definition) and is_empty_or_default(bufnr, 'tagfunc')
+ client:supports_method(ms.textDocument_definition) and is_empty_or_default(bufnr, 'tagfunc')
then
vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc'
end
if
- client.supports_method(ms.textDocument_completion) and is_empty_or_default(bufnr, 'omnifunc')
+ client:supports_method(ms.textDocument_completion) and is_empty_or_default(bufnr, 'omnifunc')
then
vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc'
end
if
- client.supports_method(ms.textDocument_rangeFormatting)
+ client:supports_method(ms.textDocument_rangeFormatting)
and is_empty_or_default(bufnr, 'formatprg')
and is_empty_or_default(bufnr, 'formatexpr')
then
@@ -367,90 +742,21 @@ function lsp._set_defaults(client, bufnr)
end
vim._with({ buf = bufnr }, function()
if
- client.supports_method(ms.textDocument_hover)
+ client:supports_method(ms.textDocument_hover)
and is_empty_or_default(bufnr, 'keywordprg')
and vim.fn.maparg('K', 'n', false, false) == ''
then
- vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = bufnr, desc = 'vim.lsp.buf.hover()' })
+ vim.keymap.set('n', 'K', function()
+ vim.lsp.buf.hover()
+ end, { buffer = bufnr, desc = 'vim.lsp.buf.hover()' })
end
end)
- if client.supports_method(ms.textDocument_diagnostic) then
+ if client:supports_method(ms.textDocument_diagnostic) then
lsp.diagnostic._enable(bufnr)
end
end
---- Reset defaults set by `set_defaults`.
---- Must only be called if the last client attached to a buffer exits.
-local function reset_defaults(bufnr)
- if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then
- vim.bo[bufnr].tagfunc = nil
- end
- if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then
- vim.bo[bufnr].omnifunc = nil
- end
- if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then
- vim.bo[bufnr].formatexpr = nil
- end
- vim._with({ buf = bufnr }, function()
- local keymap = vim.fn.maparg('K', 'n', false, true)
- if keymap and keymap.callback == vim.lsp.buf.hover and keymap.buffer == 1 then
- vim.keymap.del('n', 'K', { buffer = bufnr })
- end
- end)
-end
-
---- @param code integer
---- @param signal integer
---- @param client_id integer
-local function on_client_exit(code, signal, client_id)
- local client = all_clients[client_id]
-
- vim.schedule(function()
- for bufnr in pairs(client.attached_buffers) do
- if client and client.attached_buffers[bufnr] and api.nvim_buf_is_valid(bufnr) then
- api.nvim_exec_autocmds('LspDetach', {
- buffer = bufnr,
- modeline = false,
- data = { client_id = client_id },
- })
- end
-
- client.attached_buffers[bufnr] = nil
-
- if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then
- reset_defaults(bufnr)
- end
- end
-
- local namespace = vim.lsp.diagnostic.get_namespace(client_id)
- vim.diagnostic.reset(namespace)
- end)
-
- local name = client.name or 'unknown'
-
- -- Schedule the deletion of the client object so that it exists in the execution of LspDetach
- -- autocommands
- vim.schedule(function()
- all_clients[client_id] = nil
-
- -- Client can be absent if executable starts, but initialize fails
- -- init/attach won't have happened
- if client then
- changetracking.reset(client)
- end
- if code ~= 0 or (signal ~= 0 and signal ~= 15) then
- local msg = string.format(
- 'Client %s quit with exit code %s and signal %s. Check log for errors: %s',
- name,
- code,
- signal,
- lsp.get_log_path()
- )
- vim.notify(msg, vim.log.levels.WARN)
- end
- end)
-end
-
+--- @deprecated
--- Starts and initializes a client with the given configuration.
--- @param config vim.lsp.ClientConfig Configuration for the server.
--- @return integer? client_id |vim.lsp.get_client_by_id()| Note: client may not be
@@ -458,39 +764,26 @@ end
--- the client has been initialized.
--- @return string? # Error message, if any
function lsp.start_client(config)
- local ok, res = pcall(require('vim.lsp.client').create, config)
- if not ok then
- return nil, res --[[@as string]]
- end
-
- local client = assert(res)
-
- --- @diagnostic disable-next-line: invisible
- table.insert(client._on_exit_cbs, on_client_exit)
-
- all_clients[client.id] = client
-
- client:initialize()
-
- return client.id, nil
+ vim.deprecate('vim.lsp.start_client()', 'vim.lsp.start()', '0.13')
+ return create_and_initialize_client(config)
end
---Buffer lifecycle handler for textDocument/didSave
--- @param bufnr integer
local function text_document_did_save_handler(bufnr)
- bufnr = resolve_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
local uri = vim.uri_from_bufnr(bufnr)
local text = once(lsp._buf_get_full_text)
for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do
local name = api.nvim_buf_get_name(bufnr)
local old_name = changetracking._get_and_set_name(client, bufnr, name)
if old_name and name ~= old_name then
- client.notify(ms.textDocument_didClose, {
+ client:notify(ms.textDocument_didClose, {
textDocument = {
uri = vim.uri_from_fname(old_name),
},
})
- client.notify(ms.textDocument_didOpen, {
+ client:notify(ms.textDocument_didOpen, {
textDocument = {
version = 0,
uri = uri,
@@ -506,7 +799,7 @@ local function text_document_did_save_handler(bufnr)
if type(save_capability) == 'table' and save_capability.includeText then
included_text = text(bufnr)
end
- client.notify(ms.textDocument_didSave, {
+ client:notify(ms.textDocument_didSave, {
textDocument = {
uri = uri,
},
@@ -527,10 +820,10 @@ local function buf_detach_client(bufnr, client)
changetracking.reset_buf(client, bufnr)
- if client.supports_method(ms.textDocument_didClose) then
+ if client:supports_method(ms.textDocument_didClose) then
local uri = vim.uri_from_bufnr(bufnr)
local params = { textDocument = { uri = uri } }
- client.notify(ms.textDocument_didClose, params)
+ client:notify(ms.textDocument_didClose, params)
end
client.attached_buffers[bufnr] = nil
@@ -550,7 +843,7 @@ local function buf_attach(bufnr)
attached_buffers[bufnr] = true
local uri = vim.uri_from_bufnr(bufnr)
- local augroup = ('lsp_b_%d_save'):format(bufnr)
+ local augroup = ('nvim.lsp.b_%d_save'):format(bufnr)
local group = api.nvim_create_augroup(augroup, { clear = true })
api.nvim_create_autocmd('BufWritePre', {
group = group,
@@ -564,12 +857,12 @@ local function buf_attach(bufnr)
},
reason = protocol.TextDocumentSaveReason.Manual, ---@type integer
}
- if client.supports_method(ms.textDocument_willSave) then
- client.notify(ms.textDocument_willSave, params)
+ if client:supports_method(ms.textDocument_willSave) then
+ client:notify(ms.textDocument_willSave, params)
end
- if client.supports_method(ms.textDocument_willSaveWaitUntil) then
+ if client:supports_method(ms.textDocument_willSaveWaitUntil) then
local result, err =
- client.request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf)
+ client:request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf)
if result and result.result then
util.apply_text_edits(result.result, ctx.buf, client.offset_encoding)
elseif err then
@@ -603,8 +896,8 @@ local function buf_attach(bufnr)
local params = { textDocument = { uri = uri } }
for _, client in ipairs(clients) do
changetracking.reset_buf(client, bufnr)
- if client.supports_method(ms.textDocument_didClose) then
- client.notify(ms.textDocument_didClose, params)
+ if client:supports_method(ms.textDocument_didClose) then
+ client:notify(ms.textDocument_didClose, params)
end
end
for _, client in ipairs(clients) do
@@ -639,7 +932,7 @@ end
function lsp.buf_attach_client(bufnr, client_id)
validate('bufnr', bufnr, 'number', true)
validate('client_id', client_id, 'number')
- bufnr = resolve_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
if not api.nvim_buf_is_loaded(bufnr) then
log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr))
return false
@@ -662,7 +955,7 @@ function lsp.buf_attach_client(bufnr, client_id)
-- Send didOpen for the client if it is initialized. If it isn't initialized
-- then it will send didOpen on initialize.
if client.initialized then
- client:_on_attach(bufnr)
+ client:on_attach(bufnr)
end
return true
end
@@ -676,7 +969,7 @@ end
function lsp.buf_detach_client(bufnr, client_id)
validate('bufnr', bufnr, 'number', true)
validate('client_id', client_id, 'number')
- bufnr = resolve_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
local client = all_clients[client_id]
if not client or not client.attached_buffers[bufnr] then
@@ -740,13 +1033,13 @@ function lsp.stop_client(client_id, force)
for _, id in ipairs(ids) do
if type(id) == 'table' then
if id.stop then
- id.stop(force)
+ id:stop(force)
end
else
--- @cast id -vim.lsp.Client
local client = all_clients[id]
if client then
- client.stop(force)
+ client:stop(force)
end
end
end
@@ -782,7 +1075,7 @@ function lsp.get_clients(filter)
local clients = {} --- @type vim.lsp.Client[]
- local bufnr = filter.bufnr and resolve_bufnr(filter.bufnr)
+ local bufnr = filter.bufnr and vim._resolve_bufnr(filter.bufnr)
for _, client in pairs(all_clients) do
if
@@ -790,7 +1083,7 @@ function lsp.get_clients(filter)
and (filter.id == nil or client.id == filter.id)
and (filter.bufnr == nil or client.attached_buffers[bufnr])
and (filter.name == nil or client.name == filter.name)
- and (filter.method == nil or client.supports_method(filter.method, { bufnr = filter.bufnr }))
+ and (filter.method == nil or client:supports_method(filter.method, filter.bufnr))
and (filter._uninitialized or client.initialized)
then
clients[#clients + 1] = client
@@ -812,7 +1105,7 @@ api.nvim_create_autocmd('VimLeavePre', {
local active_clients = lsp.get_clients()
log.info('exit_handler', active_clients)
for _, client in pairs(all_clients) do
- client.stop()
+ client:stop()
end
local timeouts = {} --- @type table<integer,integer>
@@ -847,7 +1140,7 @@ api.nvim_create_autocmd('VimLeavePre', {
if not vim.wait(max_timeout, check_clients_closed, poll_time) then
for client_id, client in pairs(active_clients) do
if timeouts[client_id] ~= nil then
- client.stop(true)
+ client:stop(true)
end
end
end
@@ -878,16 +1171,16 @@ function lsp.buf_request(bufnr, method, params, handler, on_unsupported)
validate('handler', handler, 'function', true)
validate('on_unsupported', on_unsupported, 'function', true)
- bufnr = resolve_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
local method_supported = false
local clients = lsp.get_clients({ bufnr = bufnr })
local client_request_ids = {} --- @type table<integer,integer>
for _, client in ipairs(clients) do
- if client.supports_method(method, { bufnr = bufnr }) then
+ if client:supports_method(method, bufnr) then
method_supported = true
local cparams = type(params) == 'function' and params(client, bufnr) or params --[[@as table?]]
- local request_success, request_id = client.request(method, cparams, handler, bufnr)
+ local request_success, request_id = client:request(method, cparams, handler, bufnr)
-- This could only fail if the client shut down in the time since we looked
-- it up and we did the request, which should be rare.
if request_success then
@@ -910,7 +1203,7 @@ function lsp.buf_request(bufnr, method, params, handler, on_unsupported)
local function _cancel_all_requests()
for client_id, request_id in pairs(client_request_ids) do
local client = all_clients[client_id]
- client.cancel_request(request_id)
+ client:cancel_request(request_id)
end
end
@@ -1049,7 +1342,7 @@ function lsp.formatexpr(opts)
end
local bufnr = api.nvim_get_current_buf()
for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do
- if client.supports_method(ms.textDocument_rangeFormatting) then
+ if client:supports_method(ms.textDocument_rangeFormatting) then
local params = util.make_formatting_params()
local end_line = vim.fn.getline(end_lnum) --[[@as string]]
local end_col = vim.str_utfindex(end_line, client.offset_encoding)
@@ -1065,7 +1358,7 @@ function lsp.formatexpr(opts)
},
}
local response =
- client.request_sync(ms.textDocument_rangeFormatting, params, timeout_ms, bufnr)
+ client:request_sync(ms.textDocument_rangeFormatting, params, timeout_ms, bufnr)
if response and response.result then
lsp.util.apply_text_edits(response.result, bufnr, client.offset_encoding)
return 0
@@ -1092,6 +1385,55 @@ function lsp.tagfunc(pattern, flags)
return vim.lsp._tagfunc(pattern, flags)
end
+--- Provides an interface between the built-in client and a `foldexpr` function.
+---
+--- To use, check for the "textDocument/foldingRange" capability in an
+--- |LspAttach| autocommand. Example:
+---
+--- ```lua
+--- vim.api.nvim_create_autocmd('LspAttach', {
+--- callback = function(args)
+--- local client = vim.lsp.get_client_by_id(args.data.client_id)
+--- if client:supports_method('textDocument/foldingRange') then
+--- local win = vim.api.nvim_get_current_win()
+--- vim.wo[win][0].foldmethod = 'expr'
+--- vim.wo[win][0].foldexpr = 'v:lua.vim.lsp.foldexpr()'
+--- end
+--- end,
+--- })
+--- ```
+---
+---@param lnum integer line number
+function lsp.foldexpr(lnum)
+ return vim.lsp._folding_range.foldexpr(lnum)
+end
+
+--- Close all {kind} of folds in the the window with {winid}.
+---
+--- To automatically fold imports when opening a file, you can use an autocmd:
+---
+--- ```lua
+--- vim.api.nvim_create_autocmd('LspNotify', {
+--- callback = function(args)
+--- if args.data.method == 'textDocument/didOpen' then
+--- vim.lsp.foldclose('imports', vim.fn.bufwinid(args.buf))
+--- end
+--- end,
+--- })
+--- ```
+---
+---@param kind lsp.FoldingRangeKind Kind to close, one of "comment", "imports" or "region".
+---@param winid? integer Defaults to the current window.
+function lsp.foldclose(kind, winid)
+ return vim.lsp._folding_range.foldclose(kind, winid)
+end
+
+--- Provides a `foldtext` function that shows the `collapsedText` retrieved,
+--- defaults to the first folded line if `collapsedText` is not provided.
+function lsp.foldtext()
+ return vim.lsp._folding_range.foldtext()
+end
+
---Checks whether a client is stopped.
---
---@param client_id (integer)
@@ -1110,7 +1452,7 @@ end
function lsp.buf_get_clients(bufnr)
vim.deprecate('vim.lsp.buf_get_clients()', 'vim.lsp.get_clients()', '0.12')
local result = {} --- @type table<integer,vim.lsp.Client>
- for _, client in ipairs(lsp.get_clients({ bufnr = resolve_bufnr(bufnr) })) do
+ for _, client in ipairs(lsp.get_clients({ bufnr = vim._resolve_bufnr(bufnr) })) do
result[client.id] = client
end
return result
@@ -1164,7 +1506,7 @@ function lsp.for_each_buffer_client(bufnr, fn)
'lsp.get_clients({ bufnr = bufnr }) with regular loop',
'0.12'
)
- bufnr = resolve_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do
fn(client, client.id, bufnr)
@@ -1181,44 +1523,6 @@ function lsp.with(handler, override_config)
end
end
---- Helper function to use when implementing a handler.
---- This will check that all of the keys in the user configuration
---- are valid keys and make sense to include for this handler.
----
---- Will error on invalid keys (i.e. keys that do not exist in the options)
---- @param name string
---- @param options table<string,any>
---- @param user_config table<string,any>
-function lsp._with_extend(name, options, user_config)
- user_config = user_config or {}
-
- local resulting_config = {} --- @type table<string,any>
- for k, v in pairs(user_config) do
- if options[k] == nil then
- error(
- debug.traceback(
- string.format(
- 'Invalid option for `%s`: %s. Valid options are:\n%s',
- name,
- k,
- vim.inspect(vim.tbl_keys(options))
- )
- )
- )
- end
-
- resulting_config[k] = v
- end
-
- for k, v in pairs(options) do
- if resulting_config[k] == nil then
- resulting_config[k] = v
- end
- end
-
- return resulting_config
-end
-
--- Registry for client side commands.
--- This is an extension point for plugins to handle custom commands which are
--- not part of the core language server protocol specification.
@@ -1227,7 +1531,7 @@ end
--- and the value is a function which is called if any LSP action
--- (code action, code lenses, ...) triggers the command.
---
---- If a LSP response contains a command for which no matching entry is
+--- If an LSP response contains a command for which no matching entry is
--- available in this registry, the command will be executed via the LSP server
--- using `workspace/executeCommand`.
---
diff --git a/runtime/lua/vim/lsp/_changetracking.lua b/runtime/lua/vim/lsp/_changetracking.lua
index b2be53269f..265a74c8fa 100644
--- a/runtime/lua/vim/lsp/_changetracking.lua
+++ b/runtime/lua/vim/lsp/_changetracking.lua
@@ -18,14 +18,14 @@ local M = {}
---
--- None: One group for all clients
--- Full: One group for all clients
---- Incremental: One group per `offset_encoding`
+--- Incremental: One group per `position_encoding`
---
--- Sending changes can be debounced per buffer. To simplify the implementation the
--- smallest debounce interval is used and we don't group clients by different intervals.
---
--- @class vim.lsp.CTGroup
--- @field sync_kind integer TextDocumentSyncKind, considers config.flags.allow_incremental_sync
---- @field offset_encoding "utf-8"|"utf-16"|"utf-32"
+--- @field position_encoding "utf-8"|"utf-16"|"utf-32"
---
--- @class vim.lsp.CTBufferState
--- @field name string name of the buffer
@@ -40,13 +40,13 @@ local M = {}
--- @class vim.lsp.CTGroupState
--- @field buffers table<integer,vim.lsp.CTBufferState>
--- @field debounce integer debounce duration in ms
---- @field clients table<integer, table> clients using this state. {client_id, client}
+--- @field clients table<integer, vim.lsp.Client> clients using this state. {client_id, client}
---@param group vim.lsp.CTGroup
---@return string
local function group_key(group)
if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
- return tostring(group.sync_kind) .. '\0' .. group.offset_encoding
+ return tostring(group.sync_kind) .. '\0' .. group.position_encoding
end
return tostring(group.sync_kind)
end
@@ -64,7 +64,7 @@ local state_by_group = setmetatable({}, {
---@param client vim.lsp.Client
---@return vim.lsp.CTGroup
local function get_group(client)
- local allow_inc_sync = vim.F.if_nil(client.flags.allow_incremental_sync, true) --- @type boolean
+ local allow_inc_sync = vim.F.if_nil(client.flags.allow_incremental_sync, true)
local change_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change')
local sync_kind = change_capability or protocol.TextDocumentSyncKind.None
if not allow_inc_sync and change_capability == protocol.TextDocumentSyncKind.Incremental then
@@ -72,7 +72,7 @@ local function get_group(client)
end
return {
sync_kind = sync_kind,
- offset_encoding = client.offset_encoding,
+ position_encoding = client.offset_encoding,
}
end
@@ -273,8 +273,8 @@ local function send_changes(bufnr, sync_kind, state, buf_state)
end
local uri = vim.uri_from_bufnr(bufnr)
for _, client in pairs(state.clients) do
- if not client.is_stopped() and vim.lsp.buf_is_attached(bufnr, client.id) then
- client.notify(protocol.Methods.textDocument_didChange, {
+ if not client:is_stopped() and vim.lsp.buf_is_attached(bufnr, client.id) then
+ client:notify(protocol.Methods.textDocument_didChange, {
textDocument = {
uri = uri,
version = util.buf_versions[bufnr],
@@ -310,7 +310,7 @@ local function send_changes_for_group(bufnr, firstline, lastline, new_lastline,
-- The contents would further change and startline/endline may no longer fit
local changes = incremental_changes(
buf_state,
- group.offset_encoding,
+ group.position_encoding,
bufnr,
firstline,
lastline,
diff --git a/runtime/lua/vim/lsp/_folding_range.lua b/runtime/lua/vim/lsp/_folding_range.lua
new file mode 100644
index 0000000000..66eb81db6e
--- /dev/null
+++ b/runtime/lua/vim/lsp/_folding_range.lua
@@ -0,0 +1,373 @@
+local util = require('vim.lsp.util')
+local log = require('vim.lsp.log')
+local ms = require('vim.lsp.protocol').Methods
+local api = vim.api
+
+local M = {}
+
+---@class (private) vim.lsp.folding_range.BufState
+---
+---@field version? integer
+---
+--- Never use this directly, `renew()` the cached foldinfo
+--- then use on demand via `row_*` fields.
+---
+--- Index In the form of client_id -> ranges
+---@field client_ranges table<integer, lsp.FoldingRange[]?>
+---
+--- Index in the form of row -> [foldlevel, mark]
+---@field row_level table<integer, [integer, ">" | "<"?]?>
+---
+--- Index in the form of start_row -> kinds
+---@field row_kinds table<integer, table<lsp.FoldingRangeKind, true?>?>>
+---
+--- Index in the form of start_row -> collapsed_text
+---@field row_text table<integer, string?>
+
+---@type table<integer, vim.lsp.folding_range.BufState?>
+local bufstates = {}
+
+--- Renew the cached foldinfo in the buffer.
+---@param bufnr integer
+local function renew(bufnr)
+ local bufstate = assert(bufstates[bufnr])
+
+ ---@type table<integer, [integer, ">" | "<"?]?>
+ local row_level = {}
+ ---@type table<integer, table<lsp.FoldingRangeKind, true?>?>>
+ local row_kinds = {}
+ ---@type table<integer, string?>
+ local row_text = {}
+
+ for _, ranges in pairs(bufstate.client_ranges) do
+ for _, range in ipairs(ranges) do
+ local start_row = range.startLine
+ local end_row = range.endLine
+ -- Adding folds within a single line is not supported by Nvim.
+ if start_row ~= end_row then
+ row_text[start_row] = range.collapsedText
+
+ local kind = range.kind
+ if kind then
+ local kinds = row_kinds[start_row] or {}
+ kinds[kind] = true
+ row_kinds[start_row] = kinds
+ end
+
+ for row = start_row, end_row do
+ local level = row_level[row] or { 0 }
+ level[1] = level[1] + 1
+ row_level[row] = level
+ end
+ row_level[start_row][2] = '>'
+ row_level[end_row][2] = '<'
+ end
+ end
+ end
+
+ bufstate.row_level = row_level
+ bufstate.row_kinds = row_kinds
+ bufstate.row_text = row_text
+end
+
+--- Renew the cached foldinfo then force `foldexpr()` to be re-evaluated,
+--- without opening folds.
+---@param bufnr integer
+local function foldupdate(bufnr)
+ renew(bufnr)
+ for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
+ local wininfo = vim.fn.getwininfo(winid)[1]
+ if wininfo and wininfo.tabnr == vim.fn.tabpagenr() then
+ if vim.wo[winid].foldmethod == 'expr' then
+ vim._foldupdate(winid, 0, api.nvim_buf_line_count(bufnr))
+ end
+ end
+ end
+end
+
+--- Whether `foldupdate()` is scheduled for the buffer with `bufnr`.
+---
+--- Index in the form of bufnr -> true?
+---@type table<integer, true?>
+local scheduled_foldupdate = {}
+
+--- Schedule `foldupdate()` after leaving insert mode.
+---@param bufnr integer
+local function schedule_foldupdate(bufnr)
+ if not scheduled_foldupdate[bufnr] then
+ scheduled_foldupdate[bufnr] = true
+ api.nvim_create_autocmd('InsertLeave', {
+ buffer = bufnr,
+ once = true,
+ callback = function()
+ foldupdate(bufnr)
+ scheduled_foldupdate[bufnr] = nil
+ end,
+ })
+ end
+end
+
+---@param results table<integer,{err: lsp.ResponseError?, result: lsp.FoldingRange[]?}>
+---@type lsp.MultiHandler
+local function multi_handler(results, ctx)
+ local bufnr = assert(ctx.bufnr)
+ -- Handling responses from outdated buffer only causes performance overhead.
+ if util.buf_versions[bufnr] ~= ctx.version then
+ return
+ end
+
+ local bufstate = assert(bufstates[bufnr])
+ for client_id, result in pairs(results) do
+ if result.err then
+ log.error(result.err)
+ else
+ bufstate.client_ranges[client_id] = result.result
+ end
+ end
+ bufstate.version = ctx.version
+
+ if api.nvim_get_mode().mode:match('^i') then
+ -- `foldUpdate()` is guarded in insert mode.
+ schedule_foldupdate(bufnr)
+ else
+ foldupdate(bufnr)
+ end
+end
+
+---@param result lsp.FoldingRange[]?
+---@type lsp.Handler
+local function handler(err, result, ctx)
+ multi_handler({ [ctx.client_id] = { err = err, result = result } }, ctx)
+end
+
+--- Request `textDocument/foldingRange` from the server.
+--- `foldupdate()` is scheduled once after the request is completed.
+---@param bufnr integer
+---@param client? vim.lsp.Client The client whose server supports `foldingRange`.
+local function request(bufnr, client)
+ ---@type lsp.FoldingRangeParams
+ local params = { textDocument = util.make_text_document_params(bufnr) }
+
+ if client then
+ client:request(ms.textDocument_foldingRange, params, handler, bufnr)
+ return
+ end
+
+ if not next(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) then
+ return
+ end
+
+ vim.lsp.buf_request_all(bufnr, ms.textDocument_foldingRange, params, multi_handler)
+end
+
+-- NOTE:
+-- `bufstate` and event hooks are interdependent:
+-- * `bufstate` needs event hooks for correctness.
+-- * event hooks require the previous `bufstate` for updates.
+-- Since they are manually created and destroyed,
+-- we ensure their lifecycles are always synchronized.
+--
+-- TODO(ofseed):
+-- 1. Implement clearing `bufstate` and event hooks
+-- when no clients in the buffer support the corresponding method.
+-- 2. Then generalize this state management to other LSP modules.
+local augroup_setup = api.nvim_create_augroup('nvim.lsp.folding_range.setup', {})
+
+--- Initialize `bufstate` and event hooks, then request folding ranges.
+--- Manage their lifecycle within this function.
+---@param bufnr integer
+---@return vim.lsp.folding_range.BufState?
+local function setup(bufnr)
+ if not api.nvim_buf_is_loaded(bufnr) then
+ return
+ end
+
+ -- Register the new `bufstate`.
+ bufstates[bufnr] = {
+ client_ranges = {},
+ row_level = {},
+ row_kinds = {},
+ row_text = {},
+ }
+
+ -- Event hooks from `buf_attach` can't be removed externally.
+ -- Hooks and `bufstate` share the same lifecycle;
+ -- they should self-destroy if `bufstate == nil`.
+ api.nvim_buf_attach(bufnr, false, {
+ -- `on_detach` also runs on buffer reload (`:e`).
+ -- Ensure `bufstate` and hooks are cleared to avoid duplication or leftover states.
+ on_detach = function()
+ bufstates[bufnr] = nil
+ api.nvim_clear_autocmds({ buffer = bufnr, group = augroup_setup })
+ end,
+ -- Reset `bufstate` and request folding ranges.
+ on_reload = function()
+ bufstates[bufnr] = {
+ client_ranges = {},
+ row_level = {},
+ row_kinds = {},
+ row_text = {},
+ }
+ request(bufnr)
+ end,
+ --- Sync changed rows with their previous foldlevels before applying new ones.
+ on_bytes = function(_, _, _, start_row, _, _, old_row, _, _, new_row, _, _)
+ if bufstates[bufnr] == nil then
+ return true
+ end
+ local row_level = bufstates[bufnr].row_level
+ if next(row_level) == nil then
+ return
+ end
+ local row = new_row - old_row
+ if row > 0 then
+ vim._list_insert(row_level, start_row, start_row + math.abs(row) - 1, { -1 })
+ -- If the previous row ends a fold,
+ -- Nvim treats the first row after consecutive `-1`s as a new fold start,
+ -- which is not the desired behavior.
+ local prev_level = row_level[start_row - 1]
+ if prev_level and prev_level[2] == '<' then
+ row_level[start_row] = { prev_level[1] - 1 }
+ end
+ elseif row < 0 then
+ vim._list_remove(row_level, start_row, start_row + math.abs(row) - 1)
+ end
+ end,
+ })
+ api.nvim_create_autocmd('LspDetach', {
+ group = augroup_setup,
+ buffer = bufnr,
+ callback = function(args)
+ if not api.nvim_buf_is_loaded(bufnr) then
+ return
+ end
+
+ ---@type integer
+ local client_id = args.data.client_id
+ bufstates[bufnr].client_ranges[client_id] = nil
+
+ ---@type vim.lsp.Client[]
+ local clients = vim
+ .iter(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange }))
+ ---@param client vim.lsp.Client
+ :filter(function(client)
+ return client.id ~= client_id
+ end)
+ :totable()
+ if #clients == 0 then
+ bufstates[bufnr] = {
+ client_ranges = {},
+ row_level = {},
+ row_kinds = {},
+ row_text = {},
+ }
+ end
+
+ foldupdate(bufnr)
+ end,
+ })
+ api.nvim_create_autocmd('LspAttach', {
+ group = augroup_setup,
+ buffer = bufnr,
+ callback = function(args)
+ local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
+ if client:supports_method(vim.lsp.protocol.Methods.textDocument_foldingRange, bufnr) then
+ request(bufnr, client)
+ end
+ end,
+ })
+ api.nvim_create_autocmd('LspNotify', {
+ group = augroup_setup,
+ buffer = bufnr,
+ callback = function(args)
+ local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
+ if
+ client:supports_method(ms.textDocument_foldingRange, bufnr)
+ and (
+ args.data.method == ms.textDocument_didChange
+ or args.data.method == ms.textDocument_didOpen
+ )
+ then
+ request(bufnr, client)
+ end
+ end,
+ })
+
+ request(bufnr)
+
+ return bufstates[bufnr]
+end
+
+---@param kind lsp.FoldingRangeKind
+---@param winid integer
+local function foldclose(kind, winid)
+ vim._with({ win = winid }, function()
+ local bufnr = api.nvim_win_get_buf(winid)
+ local row_kinds = bufstates[bufnr].row_kinds
+ -- Reverse traverse to ensure that the smallest ranges are closed first.
+ for row = api.nvim_buf_line_count(bufnr) - 1, 0, -1 do
+ local kinds = row_kinds[row]
+ if kinds and kinds[kind] then
+ vim.cmd(row + 1 .. 'foldclose')
+ end
+ end
+ end)
+end
+
+---@param kind lsp.FoldingRangeKind
+---@param winid? integer
+function M.foldclose(kind, winid)
+ vim.validate('kind', kind, 'string')
+ vim.validate('winid', winid, 'number', true)
+
+ winid = winid or api.nvim_get_current_win()
+ local bufnr = api.nvim_win_get_buf(winid)
+ local bufstate = bufstates[bufnr]
+ if not bufstate then
+ return
+ end
+
+ if bufstate.version == util.buf_versions[bufnr] then
+ foldclose(kind, winid)
+ return
+ end
+ -- Schedule `foldclose()` if the buffer is not up-to-date.
+
+ if not next(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) then
+ return
+ end
+ ---@type lsp.FoldingRangeParams
+ local params = { textDocument = util.make_text_document_params(bufnr) }
+ vim.lsp.buf_request_all(bufnr, ms.textDocument_foldingRange, params, function(...)
+ multi_handler(...)
+ foldclose(kind, winid)
+ end)
+end
+
+---@return string
+function M.foldtext()
+ local bufnr = api.nvim_get_current_buf()
+ local lnum = vim.v.foldstart
+ local row = lnum - 1
+ local bufstate = bufstates[bufnr]
+ if bufstate and bufstate.row_text[row] then
+ return bufstate.row_text[row]
+ end
+ return vim.fn.getline(lnum)
+end
+
+---@param lnum? integer
+---@return string level
+function M.foldexpr(lnum)
+ local bufnr = api.nvim_get_current_buf()
+ local bufstate = bufstates[bufnr] or setup(bufnr)
+ if not bufstate then
+ return '0'
+ end
+
+ local row = (lnum or vim.v.lnum) - 1
+ local level = bufstate.row_level[row]
+ return level and (level[2] or '') .. (level[1] or '0') or '0'
+end
+
+return M
diff --git a/runtime/lua/vim/lsp/_meta.lua b/runtime/lua/vim/lsp/_meta.lua
index bf693ccc57..589a49c003 100644
--- a/runtime/lua/vim/lsp/_meta.lua
+++ b/runtime/lua/vim/lsp/_meta.lua
@@ -1,8 +1,8 @@
---@meta
error('Cannot require a meta file')
----@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext): ...any
----@alias lsp.MultiHandler fun(results: table<integer,{err: lsp.ResponseError?, result: any}>, context: lsp.HandlerContext): ...any
+---@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext, config?: table): ...any
+---@alias lsp.MultiHandler fun(results: table<integer,{err: lsp.ResponseError?, result: any}>, context: lsp.HandlerContext, config?: table): ...any
---@class lsp.HandlerContext
---@field method string
diff --git a/runtime/lua/vim/lsp/_snippet_grammar.lua b/runtime/lua/vim/lsp/_snippet_grammar.lua
index 9318fefcbc..f06d6e9afd 100644
--- a/runtime/lua/vim/lsp/_snippet_grammar.lua
+++ b/runtime/lua/vim/lsp/_snippet_grammar.lua
@@ -127,6 +127,7 @@ local function node(type)
end
-- stylua: ignore
+--- @diagnostic disable-next-line: missing-fields
local G = P({
'snippet';
snippet = Ct(Cg(
diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua
index f75d43f373..554f0cb991 100644
--- a/runtime/lua/vim/lsp/_tagfunc.lua
+++ b/runtime/lua/vim/lsp/_tagfunc.lua
@@ -6,12 +6,12 @@ local ms = lsp.protocol.Methods
---@param name string
---@param range lsp.Range
---@param uri string
----@param offset_encoding string
+---@param position_encoding string
---@return {name: string, filename: string, cmd: string, kind?: string}
-local function mk_tag_item(name, range, uri, offset_encoding)
+local function mk_tag_item(name, range, uri, position_encoding)
local bufnr = vim.uri_to_bufnr(uri)
-- This is get_line_byte_from_position is 0-indexed, call cursor expects a 1-indexed position
- local byte = util._get_line_byte_from_position(bufnr, range.start, offset_encoding) + 1
+ local byte = util._get_line_byte_from_position(bufnr, range.start, position_encoding) + 1
return {
name = name,
filename = vim.uri_to_fname(uri),
@@ -32,9 +32,9 @@ local function query_definition(pattern)
--- @param range lsp.Range
--- @param uri string
- --- @param offset_encoding string
- local add = function(range, uri, offset_encoding)
- table.insert(results, mk_tag_item(pattern, range, uri, offset_encoding))
+ --- @param position_encoding string
+ local add = function(range, uri, position_encoding)
+ table.insert(results, mk_tag_item(pattern, range, uri, position_encoding))
end
local remaining = #clients
@@ -59,7 +59,7 @@ local function query_definition(pattern)
remaining = remaining - 1
end
local params = util.make_position_params(win, client.offset_encoding)
- client.request(ms.textDocument_definition, params, on_response, bufnr)
+ client:request(ms.textDocument_definition, params, on_response, bufnr)
end
vim.wait(1000, function()
return remaining == 0
@@ -78,11 +78,11 @@ local function query_workspace_symbols(pattern)
local results = {}
for client_id, responses in pairs(assert(results_by_client)) do
local client = lsp.get_client_by_id(client_id)
- local offset_encoding = client and client.offset_encoding or 'utf-16'
+ local position_encoding = client and client.offset_encoding or 'utf-16'
local symbols = responses.result --[[@as lsp.SymbolInformation[]|nil]]
for _, symbol in pairs(symbols or {}) do
local loc = symbol.location
- local item = mk_tag_item(symbol.name, loc.range, loc.uri, offset_encoding)
+ local item = mk_tag_item(symbol.name, loc.range, loc.uri, position_encoding)
item.kind = lsp.protocol.SymbolKind[symbol.kind] or 'Unknown'
table.insert(results, item)
end
diff --git a/runtime/lua/vim/lsp/_transport.lua b/runtime/lua/vim/lsp/_transport.lua
new file mode 100644
index 0000000000..19ff2a8ab0
--- /dev/null
+++ b/runtime/lua/vim/lsp/_transport.lua
@@ -0,0 +1,182 @@
+local uv = vim.uv
+local log = require('vim.lsp.log')
+
+local is_win = vim.fn.has('win32') == 1
+
+--- Checks whether a given path exists and is a directory.
+---@param filename string path to check
+---@return boolean
+local function is_dir(filename)
+ local stat = uv.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+end
+
+--- @class (private) vim.lsp.rpc.Transport
+--- @field write fun(self: vim.lsp.rpc.Transport, msg: string)
+--- @field is_closing fun(self: vim.lsp.rpc.Transport): boolean
+--- @field terminate fun(self: vim.lsp.rpc.Transport)
+
+--- @class (private,exact) vim.lsp.rpc.Transport.Run : vim.lsp.rpc.Transport
+--- @field new fun(): vim.lsp.rpc.Transport.Run
+--- @field sysobj? vim.SystemObj
+local TransportRun = {}
+
+--- @return vim.lsp.rpc.Transport.Run
+function TransportRun.new()
+ return setmetatable({}, { __index = TransportRun })
+end
+
+--- @param cmd string[] Command to start the LSP server.
+--- @param extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams
+--- @param on_read fun(err: any, data: string)
+--- @param on_exit fun(code: integer, signal: integer)
+function TransportRun:run(cmd, extra_spawn_params, on_read, on_exit)
+ local function on_stderr(_, chunk)
+ if chunk then
+ log.error('rpc', cmd[1], 'stderr', chunk)
+ end
+ end
+
+ extra_spawn_params = extra_spawn_params or {}
+
+ if extra_spawn_params.cwd then
+ assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
+ end
+
+ local detached = not is_win
+ if extra_spawn_params.detached ~= nil then
+ detached = extra_spawn_params.detached
+ end
+
+ local ok, sysobj_or_err = pcall(vim.system, cmd, {
+ stdin = true,
+ stdout = on_read,
+ stderr = on_stderr,
+ cwd = extra_spawn_params.cwd,
+ env = extra_spawn_params.env,
+ detach = detached,
+ }, function(obj)
+ on_exit(obj.code, obj.signal)
+ end)
+
+ if not ok then
+ local err = sysobj_or_err --[[@as string]]
+ local sfx = err:match('ENOENT')
+ and '. The language server is either not installed, missing from PATH, or not executable.'
+ or string.format(' with error message: %s', err)
+
+ error(('Spawning language server with cmd: `%s` failed%s'):format(vim.inspect(cmd), sfx))
+ end
+
+ self.sysobj = sysobj_or_err --[[@as vim.SystemObj]]
+end
+
+function TransportRun:write(msg)
+ assert(self.sysobj):write(msg)
+end
+
+function TransportRun:is_closing()
+ return self.sysobj == nil or self.sysobj:is_closing()
+end
+
+function TransportRun:terminate()
+ assert(self.sysobj):kill(15)
+end
+
+--- @class (private,exact) vim.lsp.rpc.Transport.Connect : vim.lsp.rpc.Transport
+--- @field new fun(): vim.lsp.rpc.Transport.Connect
+--- @field handle? uv.uv_pipe_t|uv.uv_tcp_t
+--- Connect returns a PublicClient synchronously so the caller
+--- can immediately send messages before the connection is established
+--- -> Need to buffer them until that happens
+--- @field connected boolean
+--- @field closing boolean
+--- @field msgbuf vim.Ringbuf
+--- @field on_exit? fun(code: integer, signal: integer)
+local TransportConnect = {}
+
+--- @return vim.lsp.rpc.Transport.Connect
+function TransportConnect.new()
+ return setmetatable({
+ connected = false,
+ -- size should be enough because the client can't really do anything until initialization is done
+ -- which required a response from the server - implying the connection got established
+ msgbuf = vim.ringbuf(10),
+ closing = false,
+ }, { __index = TransportConnect })
+end
+
+--- @param host_or_path string
+--- @param port? integer
+--- @param on_read fun(err: any, data: string)
+--- @param on_exit? fun(code: integer, signal: integer)
+function TransportConnect:connect(host_or_path, port, on_read, on_exit)
+ self.on_exit = on_exit
+ self.handle = (
+ port and assert(uv.new_tcp(), 'Could not create new TCP socket')
+ or assert(uv.new_pipe(false), 'Pipe could not be opened.')
+ )
+
+ local function on_connect(err)
+ if err then
+ local address = not port and host_or_path or (host_or_path .. ':' .. port)
+ vim.schedule(function()
+ vim.notify(
+ string.format('Could not connect to %s, reason: %s', address, vim.inspect(err)),
+ vim.log.levels.WARN
+ )
+ end)
+ return
+ end
+ self.handle:read_start(on_read)
+ self.connected = true
+ for msg in self.msgbuf do
+ self.handle:write(msg)
+ end
+ end
+
+ if not port then
+ self.handle:connect(host_or_path, on_connect)
+ return
+ end
+
+ --- @diagnostic disable-next-line:param-type-mismatch bad UV typing
+ local info = uv.getaddrinfo(host_or_path, nil)
+ local resolved_host = info and info[1] and info[1].addr or host_or_path
+ self.handle:connect(resolved_host, port, on_connect)
+end
+
+function TransportConnect:write(msg)
+ if self.connected then
+ local _, err = self.handle:write(msg)
+ if err and not self.closing then
+ log.error('Error on handle:write: %q', err)
+ end
+ return
+ end
+
+ self.msgbuf:push(msg)
+end
+
+function TransportConnect:is_closing()
+ return self.closing
+end
+
+function TransportConnect:terminate()
+ if self.closing then
+ return
+ end
+ self.closing = true
+ if self.handle then
+ self.handle:shutdown()
+ self.handle:close()
+ end
+ if self.on_exit then
+ self.on_exit(0, 0)
+ end
+end
+
+return {
+ TransportRun = TransportRun,
+ TransportConnect = TransportConnect,
+}
diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua
index c4cdb5aea8..4711b3cc9b 100644
--- a/runtime/lua/vim/lsp/_watchfiles.lua
+++ b/runtime/lua/vim/lsp/_watchfiles.lua
@@ -116,7 +116,7 @@ function M.register(reg, client_id)
local params = {
changes = change_queues[client_id],
}
- client.notify(ms.workspace_didChangeWatchedFiles, params)
+ client:notify(ms.workspace_didChangeWatchedFiles, params)
queue_timers[client_id] = nil
change_queues[client_id] = nil
change_cache[client_id] = nil
@@ -174,6 +174,7 @@ function M.cancel(client_id)
cancel()
end
end
+ cancels[client_id] = nil
end
return M
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 6383855a30..48aa809ebd 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -20,7 +20,7 @@ local function client_positional_params(params)
end
end
-local hover_ns = api.nvim_create_namespace('vim_lsp_hover_range')
+local hover_ns = api.nvim_create_namespace('nvim.lsp.hover_range')
--- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts
--- @field silent? boolean
@@ -232,7 +232,7 @@ local function get_locations(method, opts)
end
for _, client in ipairs(clients) do
local params = util.make_position_params(win, client.offset_encoding)
- client.request(method, params, function(_, result)
+ client:request(method, params, function(_, result)
on_response(_, result, client)
end)
end
@@ -252,13 +252,13 @@ end
--- vim.lsp.buf.definition({ on_list = on_list })
--- vim.lsp.buf.references(nil, { on_list = on_list })
--- ```
+--- @field on_list? fun(t: vim.lsp.LocationOpts.OnList)
---
---- If you prefer loclist instead of qflist:
+--- Whether to use the |location-list| or the |quickfix| list in the default handler.
--- ```lua
--- vim.lsp.buf.definition({ loclist = true })
---- vim.lsp.buf.references(nil, { loclist = true })
+--- vim.lsp.buf.references(nil, { loclist = false })
--- ```
---- @field on_list? fun(t: vim.lsp.LocationOpts.OnList)
--- @field loclist? boolean
--- @class vim.lsp.LocationOpts.OnList
@@ -324,12 +324,11 @@ local function process_signature_help_results(results)
return signatures
end
-local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help')
+local sig_help_ns = api.nvim_create_namespace('nvim.lsp.signature_help')
--- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts
--- @field silent? boolean
--- TODO(lewis6991): support multiple clients
--- Displays signature information about the symbol under the cursor in a
--- floating window.
--- @param config? vim.lsp.buf.signature_help.Opts
@@ -356,6 +355,7 @@ function M.signature_help(config)
local ft = vim.bo[ctx.bufnr].filetype
local total = #signatures
+ local can_cycle = total > 1 and config.focusable
local idx = 0
--- @param update_win? integer
@@ -371,7 +371,7 @@ function M.signature_help(config)
return
end
- local sfx = total > 1 and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or ''
+ local sfx = can_cycle and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or ''
local title = string.format('Signature Help: %s%s', client.name, sfx)
if config.border then
config.title = title
@@ -402,7 +402,7 @@ function M.signature_help(config)
local fbuf, fwin = show_signature()
- if total > 1 then
+ if can_cycle then
vim.keymap.set('n', '<C-s>', function()
show_signature(fwin)
end, {
@@ -423,7 +423,7 @@ end
---
---@see vim.lsp.protocol.CompletionTriggerKind
function M.completion(context)
- vim.depends('vim.lsp.buf.completion', 'vim.lsp.commpletion.trigger', '0.12')
+ vim.depends('vim.lsp.buf.completion', 'vim.lsp.completion.trigger', '0.12')
return lsp.buf_request(
0,
ms.textDocument_completion,
@@ -450,10 +450,10 @@ local function range_from_selection(bufnr, mode)
-- A user can start visual selection at the end and move backwards
-- Normalize the range to start < end
if start_row == end_row and end_col < start_col then
- end_col, start_col = start_col, end_col
+ end_col, start_col = start_col, end_col --- @type integer, integer
elseif end_row < start_row then
- start_row, end_row = end_row, start_row
- start_col, end_col = end_col, start_col
+ start_row, end_row = end_row, start_row --- @type integer, integer
+ start_col, end_col = end_col, start_col --- @type integer, integer
end
if mode == 'V' then
start_col = 1
@@ -487,7 +487,7 @@ end
--- ```lua
--- -- Never request typescript-language-server for formatting
--- vim.lsp.buf.format {
---- filter = function(client) return client.name ~= "tsserver" end
+--- filter = function(client) return client.name ~= "ts_ls" end
--- }
--- ```
--- @field filter? fun(client: vim.lsp.Client): boolean?
@@ -519,7 +519,7 @@ end
--- @param opts? vim.lsp.buf.format.Opts
function M.format(opts)
opts = opts or {}
- local bufnr = opts.bufnr or api.nvim_get_current_buf()
+ local bufnr = vim._resolve_bufnr(opts.bufnr)
local mode = api.nvim_get_mode().mode
local range = opts.range
-- Try to use visual selection if no range is given
@@ -553,27 +553,34 @@ function M.format(opts)
--- @param client vim.lsp.Client
--- @param params lsp.DocumentFormattingParams
- --- @return lsp.DocumentFormattingParams
+ --- @return lsp.DocumentFormattingParams|lsp.DocumentRangeFormattingParams|lsp.DocumentRangesFormattingParams
local function set_range(client, params)
- local to_lsp_range = function(r) ---@return lsp.DocumentRangeFormattingParams|lsp.DocumentRangesFormattingParams
+ --- @param r {start:[integer,integer],end:[integer, integer]}
+ local function to_lsp_range(r)
return util.make_given_range_params(r.start, r['end'], bufnr, client.offset_encoding).range
end
+ local ret = params --[[@as lsp.DocumentFormattingParams|lsp.DocumentRangeFormattingParams|lsp.DocumentRangesFormattingParams]]
if passed_multiple_ranges then
- params.ranges = vim.tbl_map(to_lsp_range, range)
+ ret = params --[[@as lsp.DocumentRangesFormattingParams]]
+ --- @cast range {start:[integer,integer],end:[integer, integer]}
+ ret.ranges = vim.tbl_map(to_lsp_range, range)
elseif range then
- params.range = to_lsp_range(range)
+ ret = params --[[@as lsp.DocumentRangeFormattingParams]]
+ ret.range = to_lsp_range(range)
end
- return params
+ return ret
end
if opts.async then
+ --- @param idx? integer
+ --- @param client? vim.lsp.Client
local function do_format(idx, client)
- if not client then
+ if not idx or not client then
return
end
local params = set_range(client, util.make_formatting_params(opts.formatting_options))
- client.request(method, params, function(...)
+ client:request(method, params, function(...)
local handler = client.handlers[method] or lsp.handlers[method]
handler(...)
do_format(next(clients, idx))
@@ -584,7 +591,7 @@ function M.format(opts)
local timeout_ms = opts.timeout_ms or 1000
for _, client in pairs(clients) do
local params = set_range(client, util.make_formatting_params(opts.formatting_options))
- local result, err = client.request_sync(method, params, timeout_ms, bufnr)
+ local result, err = client:request_sync(method, params, timeout_ms, bufnr)
if result and result.result then
util.apply_text_edits(result.result, bufnr, client.offset_encoding)
elseif err then
@@ -615,7 +622,7 @@ end
---@param opts? vim.lsp.buf.rename.Opts Additional options:
function M.rename(new_name, opts)
opts = opts or {}
- local bufnr = opts.bufnr or api.nvim_get_current_buf()
+ local bufnr = vim._resolve_bufnr(opts.bufnr)
local clients = lsp.get_clients({
bufnr = bufnr,
name = opts.name,
@@ -636,38 +643,40 @@ function M.rename(new_name, opts)
local cword = vim.fn.expand('<cword>')
--- @param range lsp.Range
- --- @param offset_encoding string
- local function get_text_at_range(range, offset_encoding)
+ --- @param position_encoding string
+ local function get_text_at_range(range, position_encoding)
return api.nvim_buf_get_text(
bufnr,
range.start.line,
- util._get_line_byte_from_position(bufnr, range.start, offset_encoding),
+ util._get_line_byte_from_position(bufnr, range.start, position_encoding),
range['end'].line,
- util._get_line_byte_from_position(bufnr, range['end'], offset_encoding),
+ util._get_line_byte_from_position(bufnr, range['end'], position_encoding),
{}
)[1]
end
+ --- @param idx? integer
+ --- @param client? vim.lsp.Client
local function try_use_client(idx, client)
- if not client then
+ if not idx or not client then
return
end
--- @param name string
local function rename(name)
- local params = util.make_position_params(win, client.offset_encoding)
+ local params = util.make_position_params(win, client.offset_encoding) --[[@as lsp.RenameParams]]
params.newName = name
local handler = client.handlers[ms.textDocument_rename]
or lsp.handlers[ms.textDocument_rename]
- client.request(ms.textDocument_rename, params, function(...)
+ client:request(ms.textDocument_rename, params, function(...)
handler(...)
try_use_client(next(clients, idx))
end, bufnr)
end
- if client.supports_method(ms.textDocument_prepareRename) then
+ if client:supports_method(ms.textDocument_prepareRename) then
local params = util.make_position_params(win, client.offset_encoding)
- client.request(ms.textDocument_prepareRename, params, function(err, result)
+ client:request(ms.textDocument_prepareRename, params, function(err, result)
if err or result == nil then
if next(clients, idx) then
try_use_client(next(clients, idx))
@@ -706,7 +715,7 @@ function M.rename(new_name, opts)
end, bufnr)
else
assert(
- client.supports_method(ms.textDocument_rename),
+ client:supports_method(ms.textDocument_rename),
'Client must support textDocument/rename'
)
if new_name then
@@ -732,7 +741,7 @@ end
--- Lists all the references to the symbol under the cursor in the quickfix window.
---
----@param context (table|nil) Context for the request
+---@param context lsp.ReferenceContext? Context for the request
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
---@param opts? vim.lsp.ListOpts
function M.references(context, opts)
@@ -781,7 +790,7 @@ function M.references(context, opts)
params.context = context or {
includeDeclaration = true,
}
- client.request(ms.textDocument_references, params, function(_, result)
+ client:request(ms.textDocument_references, params, function(_, result)
local items = util.locations_to_items(result or {}, client.offset_encoding)
vim.list_extend(all_items, items)
remaining = remaining - 1
@@ -792,9 +801,10 @@ function M.references(context, opts)
end
end
---- Lists all symbols in the current buffer in the quickfix window.
+--- Lists all symbols in the current buffer in the |location-list|.
--- @param opts? vim.lsp.ListOpts
function M.document_symbol(opts)
+ opts = vim.tbl_deep_extend('keep', opts or {}, { loclist = true })
local params = { textDocument = util.make_text_document_params() }
request_with_opts(ms.textDocument_documentSymbol, params, opts)
end
@@ -813,7 +823,7 @@ local function request_with_id(client_id, method, params, handler, bufnr)
)
return
end
- client.request(method, params, handler, bufnr)
+ client:request(method, params, handler, bufnr)
end
--- @param item lsp.TypeHierarchyItem|lsp.CallHierarchyItem
@@ -880,7 +890,7 @@ local function hierarchy(method)
for _, client in ipairs(clients) do
local params = util.make_position_params(win, client.offset_encoding)
--- @param result lsp.CallHierarchyItem[]|lsp.TypeHierarchyItem[]?
- client.request(prepare_method, params, function(err, result, ctx)
+ client:request(prepare_method, params, function(err, result, ctx)
if err then
vim.notify(err.message, vim.log.levels.WARN)
elseif result then
@@ -1131,8 +1141,8 @@ local function on_code_action_results(results, opts)
local action = choice.action
local bufnr = assert(choice.ctx.bufnr, 'Must have buffer number')
- if not action.edit and client.supports_method(ms.codeAction_resolve) then
- client.request(ms.codeAction_resolve, action, function(err, resolved_action)
+ if not action.edit and client:supports_method(ms.codeAction_resolve) then
+ client:request(ms.codeAction_resolve, action, function(err, resolved_action)
if err then
if action.command then
apply_action(action, client, choice.ctx)
@@ -1224,6 +1234,7 @@ function M.code_action(opts)
for _, client in ipairs(clients) do
---@type lsp.CodeActionParams
local params
+
if opts.range then
assert(type(opts.range) == 'table', 'code_action range must be a table')
local start = assert(opts.range.start, 'range must have a `start` property')
@@ -1236,6 +1247,9 @@ function M.code_action(opts)
else
params = util.make_range_params(win, client.offset_encoding)
end
+
+ --- @cast params lsp.CodeActionParams
+
if context.diagnostics then
params.context = context
else
@@ -1253,7 +1267,7 @@ function M.code_action(opts)
})
end
- client.request(ms.textDocument_codeAction, params, on_result, bufnr)
+ client:request(ms.textDocument_codeAction, params, on_result, bufnr)
end
end
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index 11ecb87507..253ccc48f4 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -75,17 +75,17 @@ local validate = vim.validate
---
--- Map with language server specific settings.
--- See the {settings} in |vim.lsp.Client|.
---- @field settings? table
+--- @field settings? lsp.LSPObject
---
--- Table that maps string of clientside commands to user-defined functions.
---- Commands passed to start_client take precedence over the global command registry. Each key
+--- Commands passed to `start()` take precedence over the global command registry. Each key
--- must be a unique command name, and the value is a function which is called if any LSP action
--- (code action, code lenses, ...) triggers the command.
--- @field commands? table<string,fun(command: lsp.Command, ctx: table)>
---
--- Values to pass in the initialization request as `initializationOptions`. See `initialize` in
--- the LSP spec.
---- @field init_options? table
+--- @field init_options? lsp.LSPObject
---
--- Name in log messages.
--- (default: client-id)
@@ -94,7 +94,8 @@ local validate = vim.validate
--- Language ID as string. Defaults to the buffer filetype.
--- @field get_language_id? fun(bufnr: integer, filetype: string): string
---
---- The encoding that the LSP server expects. Client does not verify this is correct.
+--- Called "position encoding" in LSP spec, the encoding that the LSP server expects.
+--- Client does not verify this is correct.
--- @field offset_encoding? 'utf-8'|'utf-16'|'utf-32'
---
--- Callback invoked when the client operation throws an error. `code` is a number describing the error.
@@ -103,7 +104,7 @@ local validate = vim.validate
--- @field on_error? fun(code: integer, err: string)
---
--- Callback invoked before the LSP "initialize" phase, where `params` contains the parameters
---- being sent to the server and `config` is the config that was passed to |vim.lsp.start_client()|.
+--- being sent to the server and `config` is the config that was passed to |vim.lsp.start()|.
--- You can use this to modify parameters before they are sent.
--- @field before_init? fun(params: lsp.InitializeParams, config: vim.lsp.ClientConfig)
---
@@ -148,8 +149,10 @@ local validate = vim.validate
--- See |vim.lsp.rpc.start()|.
--- @field rpc vim.lsp.rpc.PublicClient
---
---- The encoding used for communicating with the server. You can modify this in
---- the `config`'s `on_init` method before text is sent to the server.
+--- Called "position encoding" in LSP spec,
+--- the encoding used for communicating with the server.
+--- You can modify this in the `config`'s `on_init` method
+--- before text is sent to the server.
--- @field offset_encoding string
---
--- The handlers used by the client as described in |lsp-handler|.
@@ -161,16 +164,20 @@ local validate = vim.validate
--- for an active request, or "cancel" for a cancel request. It will be
--- "complete" ephemerally while executing |LspRequest| autocmds when replies
--- are received from the server.
---- @field requests table<integer,{ type: string, bufnr: integer, method: string}>
+--- @field requests table<integer,{ type: string, bufnr: integer, method: string}?>
---
--- copy of the table that was passed by the user
---- to |vim.lsp.start_client()|.
+--- to |vim.lsp.start()|.
--- @field config vim.lsp.ClientConfig
---
--- Response from the server sent on `initialize` describing the server's
--- capabilities.
--- @field server_capabilities lsp.ServerCapabilities?
---
+--- Response from the server sent on `initialize` describing information about
+--- the server.
+--- @field server_info lsp.ServerInfo?
+---
--- A ring buffer (|vim.ringbuf()|) containing progress messages
--- sent by the server.
--- @field progress vim.lsp.Client.Progress
@@ -186,9 +193,6 @@ local validate = vim.validate
---
--- @field attached_buffers table<integer,true>
---
---- Buffers that should be attached to upon initialize()
---- @field package _buffers_to_attach table<integer,true>
----
--- @field private _log_prefix string
---
--- Track this so that we can escalate automatically if we've already tried a
@@ -207,7 +211,7 @@ local validate = vim.validate
--- Map with language server specific settings. These are returned to the
--- language server if requested via `workspace/configuration`. Keys are
--- case-sensitive.
---- @field settings table
+--- @field settings lsp.LSPObject
---
--- A table with flags for the client. The current (experimental) flags are:
--- @field flags vim.lsp.Client.Flags
@@ -219,70 +223,28 @@ local validate = vim.validate
--- @field private registrations table<string,lsp.Registration[]>
--- @field dynamic_capabilities lsp.DynamicCapabilities
---
---- Sends a request to the server.
---- This is a thin wrapper around {client.rpc.request} with some additional
---- checking.
---- If {handler} is not specified and if there's no respective global
---- handler, then an error will occur.
---- Returns: {status}, {client_id}?. {status} is a boolean indicating if
---- the notification was successful. If it is `false`, then it will always
---- be `false` (the client has shutdown).
---- If {status} is `true`, the function returns {request_id} as the second
---- result. You can use this with `client.cancel_request(request_id)` to cancel
---- the request.
---- @field request fun(method: string, params: table?, handler: lsp.Handler?, bufnr: integer?): boolean, integer?
----
---- Sends a request to the server and synchronously waits for the response.
---- This is a wrapper around {client.request}
---- Returns: { err=err, result=result }, a dict, where `err` and `result`
---- come from the |lsp-handler|. On timeout, cancel or error, returns `(nil,
---- err)` where `err` is a string describing the failure reason. If the request
---- was unsuccessful returns `nil`.
---- @field request_sync fun(method: string, params: table?, timeout_ms: integer?, bufnr: integer): {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dict
----
---- Sends a notification to an LSP server.
---- Returns: a boolean to indicate if the notification was successful. If
---- it is false, then it will always be false (the client has shutdown).
---- @field notify fun(method: string, params: table?): boolean
----
---- Cancels a request with a given request id.
---- Returns: same as `notify()`.
---- @field cancel_request fun(id: integer): boolean
----
---- Stops a client, optionally with force.
---- By default, it will just ask the server to shutdown without force.
---- If you request to stop a client which has previously been requested to
---- shutdown, it will automatically escalate and force shutdown.
---- @field stop fun(force?: boolean)
----
---- Runs the on_attach function from the client's config if it was defined.
---- Useful for buffer-local setup.
---- @field on_attach fun(bufnr: integer)
----
--- @field private _before_init_cb? vim.lsp.client.before_init_cb
--- @field private _on_attach_cbs vim.lsp.client.on_attach_cb[]
--- @field private _on_init_cbs vim.lsp.client.on_init_cb[]
--- @field private _on_exit_cbs vim.lsp.client.on_exit_cb[]
--- @field private _on_error_cb? fun(code: integer, err: string)
----
---- Checks if a client supports a given method.
---- Always returns true for unknown off-spec methods.
---- {opts} is a optional `{bufnr?: integer}` table.
---- Some language server capabilities can be file specific.
---- @field supports_method fun(method: string, opts?: {bufnr: integer?}): boolean
----
---- Checks whether a client is stopped.
---- Returns: true if the client is fully stopped.
---- @field is_stopped fun(): boolean
local Client = {}
Client.__index = Client
---- @param cls table
---- @param meth any
---- @return function
-local function method_wrapper(cls, meth)
- return function(...)
- return meth(cls, ...)
+--- @param obj table<string,any>
+--- @param cls table<string,function>
+--- @param name string
+local function method_wrapper(obj, cls, name)
+ local meth = assert(cls[name])
+ obj[name] = function(...)
+ local arg = select(1, ...)
+ if arg and getmetatable(arg) == cls then
+ -- First argument is self, call meth directly
+ return meth(...)
+ end
+ vim.deprecate('client.' .. name, 'client:' .. name, '0.13')
+ -- First argument is not self, insert it
+ return meth(obj, ...)
end
end
@@ -304,9 +266,6 @@ local valid_encodings = {
['utf8'] = 'utf-8',
['utf16'] = 'utf-16',
['utf32'] = 'utf-32',
- UTF8 = 'utf-8',
- UTF16 = 'utf-16',
- UTF32 = 'utf-32',
}
--- Normalizes {encoding} to valid LSP encoding names.
@@ -315,12 +274,12 @@ local valid_encodings = {
local function validate_encoding(encoding)
validate('encoding', encoding, 'string', true)
if not encoding then
- return valid_encodings.UTF16
+ return valid_encodings.utf16
end
return valid_encodings[encoding:lower()]
or error(
string.format(
- "Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'",
+ "Invalid position encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'",
encoding
)
)
@@ -346,7 +305,7 @@ local function default_get_language_id(_bufnr, filetype)
return filetype
end
---- Validates a client configuration as given to |vim.lsp.start_client()|.
+--- Validates a client configuration as given to |vim.lsp.start()|.
--- @param config vim.lsp.ClientConfig
local function validate_config(config)
validate('config', config, 'table')
@@ -404,31 +363,6 @@ local function get_name(id, config)
return tostring(id)
end
---- @param workspace_folders string|lsp.WorkspaceFolder[]?
---- @return lsp.WorkspaceFolder[]?
-local function get_workspace_folders(workspace_folders)
- if type(workspace_folders) == 'table' then
- return workspace_folders
- elseif type(workspace_folders) == 'string' then
- return {
- {
- uri = vim.uri_from_fname(workspace_folders),
- name = workspace_folders,
- },
- }
- end
-end
-
---- @generic T
---- @param x elem_or_list<T>?
---- @return T[]
-local function ensure_list(x)
- if type(x) == 'table' then
- return x
- end
- return { x }
-end
-
--- @nodoc
--- @param config vim.lsp.ClientConfig
--- @return vim.lsp.Client?
@@ -455,13 +389,13 @@ function Client.create(config)
settings = config.settings or {},
flags = config.flags or {},
get_language_id = config.get_language_id or default_get_language_id,
- capabilities = config.capabilities or lsp.protocol.make_client_capabilities(),
- workspace_folders = get_workspace_folders(config.workspace_folders or config.root_dir),
+ capabilities = config.capabilities,
+ workspace_folders = lsp._get_workspace_folders(config.workspace_folders or config.root_dir),
root_dir = config.root_dir,
_before_init_cb = config.before_init,
- _on_init_cbs = ensure_list(config.on_init),
- _on_exit_cbs = ensure_list(config.on_exit),
- _on_attach_cbs = ensure_list(config.on_attach),
+ _on_init_cbs = vim._ensure_list(config.on_init),
+ _on_exit_cbs = vim._ensure_list(config.on_exit),
+ _on_attach_cbs = vim._ensure_list(config.on_attach),
_on_error_cb = config.on_error,
_trace = get_trace(config.trace),
@@ -477,6 +411,9 @@ function Client.create(config)
messages = { name = name, messages = {}, progress = {}, status = {} },
}
+ self.capabilities =
+ vim.tbl_deep_extend('force', lsp.protocol.make_client_capabilities(), self.capabilities or {})
+
--- @class lsp.DynamicCapabilities
--- @nodoc
self.dynamic_capabilities = {
@@ -499,24 +436,23 @@ function Client.create(config)
end,
}
- self.request = method_wrapper(self, Client._request)
- self.request_sync = method_wrapper(self, Client._request_sync)
- self.notify = method_wrapper(self, Client._notify)
- self.cancel_request = method_wrapper(self, Client._cancel_request)
- self.stop = method_wrapper(self, Client._stop)
- self.is_stopped = method_wrapper(self, Client._is_stopped)
- self.on_attach = method_wrapper(self, Client._on_attach)
- self.supports_method = method_wrapper(self, Client._supports_method)
-
--- @type table<string|integer, string> title of unfinished progress sequences by token
self.progress.pending = {}
--- @type vim.lsp.rpc.Dispatchers
local dispatchers = {
- notification = method_wrapper(self, Client._notification),
- server_request = method_wrapper(self, Client._server_request),
- on_error = method_wrapper(self, Client._on_error),
- on_exit = method_wrapper(self, Client._on_exit),
+ notification = function(...)
+ return self:_notification(...)
+ end,
+ server_request = function(...)
+ return self:_server_request(...)
+ end,
+ on_error = function(...)
+ return self:_on_error(...)
+ end,
+ on_exit = function(...)
+ return self:_on_exit(...)
+ end,
}
-- Start the RPC client.
@@ -533,6 +469,15 @@ function Client.create(config)
setmetatable(self, Client)
+ method_wrapper(self, Client, 'request')
+ method_wrapper(self, Client, 'request_sync')
+ method_wrapper(self, Client, 'notify')
+ method_wrapper(self, Client, 'cancel_request')
+ method_wrapper(self, Client, 'stop')
+ method_wrapper(self, Client, 'is_stopped')
+ method_wrapper(self, Client, 'on_attach')
+ method_wrapper(self, Client, 'supports_method')
+
return self
end
@@ -615,8 +560,10 @@ function Client:initialize()
self.offset_encoding = self.server_capabilities.positionEncoding
end
+ self.server_info = result.serverInfo
+
if next(self.settings) then
- self:_notify(ms.workspace_didChangeConfiguration, { settings = self.settings })
+ self:notify(ms.workspace_didChangeConfiguration, { settings = self.settings })
end
-- If server is being restarted, make sure to re-attach to any previously attached buffers.
@@ -628,7 +575,7 @@ function Client:initialize()
for buf in pairs(reattach_bufs) do
-- The buffer may have been detached in the on_init callback.
if self.attached_buffers[buf] then
- self:_on_attach(buf)
+ self:on_attach(buf)
end
end
@@ -645,24 +592,62 @@ end
--- Returns the default handler if the user hasn't set a custom one.
---
--- @param method (string) LSP method name
---- @return lsp.Handler|nil handler for the given method, if defined, or the default from |vim.lsp.handlers|
+--- @return lsp.Handler? handler for the given method, if defined, or the default from |vim.lsp.handlers|
function Client:_resolve_handler(method)
return self.handlers[method] or lsp.handlers[method]
end
---- Returns the buffer number for the given {bufnr}.
----
---- @param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer
---- @return integer bufnr
-local function resolve_bufnr(bufnr)
- validate('bufnr', bufnr, 'number', true)
- if bufnr == nil or bufnr == 0 then
- return api.nvim_get_current_buf()
+--- @private
+--- @param id integer
+--- @param req_type 'pending'|'complete'|'cancel'|
+--- @param bufnr? integer (only required for req_type='pending')
+--- @param method? string (only required for req_type='pending')
+function Client:_process_request(id, req_type, bufnr, method)
+ local pending = req_type == 'pending'
+
+ validate('id', id, 'number')
+ if pending then
+ validate('bufnr', bufnr, 'number')
+ validate('method', method, 'string')
+ end
+
+ local cur_request = self.requests[id]
+
+ if pending and cur_request then
+ log.error(
+ self._log_prefix,
+ ('Cannot create request with id %d as one already exists'):format(id)
+ )
+ return
+ elseif not pending and not cur_request then
+ log.error(
+ self._log_prefix,
+ ('Cannot find request with id %d whilst attempting to %s'):format(id, req_type)
+ )
+ return
+ end
+
+ if cur_request then
+ bufnr = cur_request.bufnr
+ method = cur_request.method
end
- return bufnr
+
+ assert(bufnr and method)
+
+ local request = { type = req_type, bufnr = bufnr, method = method }
+
+ -- Clear 'complete' requests
+ -- Note 'pending' and 'cancelled' requests are cleared when the server sends a response
+ -- which is processed via the notify_reply_callback argument to rpc.request.
+ self.requests[id] = req_type ~= 'complete' and request or nil
+
+ api.nvim_exec_autocmds('LspRequest', {
+ buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
+ modeline = false,
+ data = { client_id = self.id, request_id = id, request = request },
+ })
end
---- @private
--- Sends a request to the server.
---
--- This is a thin wrapper around {client.rpc.request} with some additional
@@ -671,15 +656,14 @@ end
--- @param method string LSP method name.
--- @param params? table LSP request params.
--- @param handler? lsp.Handler Response |lsp-handler| for this method.
---- @param bufnr integer Buffer handle (0 for current).
---- @return boolean status, integer? request_id {status} is a bool indicating
---- whether the request was successful. If it is `false`, then it will
---- always be `false` (the client has shutdown). If it was
---- successful, then it will return {request_id} as the
---- second result. You can use this with `client.cancel_request(request_id)`
+--- @param bufnr? integer (default: 0) Buffer handle, or 0 for current.
+--- @return boolean status indicates whether the request was successful.
+--- If it is `false`, then it will always be `false` (the client has shutdown).
+--- @return integer? request_id Can be used with |Client:cancel_request()|.
+--- `nil` is request failed.
--- to cancel the-request.
--- @see |vim.lsp.buf_request_all()|
-function Client:_request(method, params, handler, bufnr)
+function Client:request(method, params, handler, bufnr)
if not handler then
handler = assert(
self:_resolve_handler(method),
@@ -688,37 +672,24 @@ function Client:_request(method, params, handler, bufnr)
end
-- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state
changetracking.flush(self, bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
local version = lsp.util.buf_versions[bufnr]
- bufnr = resolve_bufnr(bufnr)
log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr)
local success, request_id = self.rpc.request(method, params, function(err, result)
- local context = {
+ handler(err, result, {
method = method,
client_id = self.id,
bufnr = bufnr,
params = params,
version = version,
- }
- handler(err, result, context)
- end, function(request_id)
- local request = self.requests[request_id]
- request.type = 'complete'
- api.nvim_exec_autocmds('LspRequest', {
- buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
- modeline = false,
- data = { client_id = self.id, request_id = request_id, request = request },
})
- self.requests[request_id] = nil
+ end, function(request_id)
+ -- Called when the server sends a response to the request (including cancelled acknowledgment).
+ self:_process_request(request_id, 'complete')
end)
if success and request_id then
- local request = { type = 'pending', bufnr = bufnr, method = method }
- self.requests[request_id] = request
- api.nvim_exec_autocmds('LspRequest', {
- buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
- modeline = false,
- data = { client_id = self.id, request_id = request_id, request = request },
- })
+ self:_process_request(request_id, 'pending', bufnr, method)
end
return success, request_id
@@ -731,41 +702,39 @@ local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'err
---
--- @param ... string List to write to the buffer
local function err_message(...)
- local message = table.concat(vim.iter({ ... }):flatten():totable())
+ local chunks = { { table.concat(vim.iter({ ... }):flatten():totable()) } }
if vim.in_fast_event() then
vim.schedule(function()
- api.nvim_err_writeln(message)
+ api.nvim_echo(chunks, true, { err = true })
api.nvim_command('redraw')
end)
else
- api.nvim_err_writeln(message)
+ api.nvim_echo(chunks, true, { err = true })
api.nvim_command('redraw')
end
end
---- @private
--- Sends a request to the server and synchronously waits for the response.
---
---- This is a wrapper around {client.request}
+--- This is a wrapper around |Client:request()|
---
---- @param method (string) LSP method name.
---- @param params (table) LSP request params.
---- @param timeout_ms (integer|nil) Maximum time in milliseconds to wait for
+--- @param method string LSP method name.
+--- @param params table LSP request params.
+--- @param timeout_ms integer? Maximum time in milliseconds to wait for
--- a result. Defaults to 1000
---- @param bufnr (integer) Buffer handle (0 for current).
---- @return {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dict, where
---- `err` and `result` come from the |lsp-handler|.
---- On timeout, cancel or error, returns `(nil, err)` where `err` is a
---- string describing the failure reason. If the request was unsuccessful
---- returns `nil`.
+--- @param bufnr? integer (default: 0) Buffer handle, or 0 for current.
+--- @return {err: lsp.ResponseError?, result:any}? `result` and `err` from the |lsp-handler|.
+--- `nil` is the request was unsuccessful
+--- @return string? err On timeout, cancel or error, where `err` is a
+--- string describing the failure reason.
--- @see |vim.lsp.buf_request_sync()|
-function Client:_request_sync(method, params, timeout_ms, bufnr)
+function Client:request_sync(method, params, timeout_ms, bufnr)
local request_result = nil
local function _sync_handler(err, result)
request_result = { err = err, result = result }
end
- local success, request_id = self:_request(method, params, _sync_handler, bufnr)
+ local success, request_id = self:request(method, params, _sync_handler, bufnr)
if not success then
return nil
end
@@ -776,22 +745,20 @@ function Client:_request_sync(method, params, timeout_ms, bufnr)
if not wait_result then
if request_id then
- self:_cancel_request(request_id)
+ self:cancel_request(request_id)
end
return nil, wait_result_reason[reason]
end
return request_result
end
---- @package
--- Sends a notification to an LSP server.
---
--- @param method string LSP method name.
---- @param params table|nil LSP request params.
---- @return boolean status true if the notification was successful.
---- If it is false, then it will always be false
---- (the client has shutdown).
-function Client:_notify(method, params)
+--- @param params table? LSP request params.
+--- @return boolean status indicating if the notification was successful.
+--- If it is false, then the client has shutdown.
+function Client:notify(method, params)
if method ~= ms.textDocument_didChange then
changetracking.flush(self)
end
@@ -814,41 +781,32 @@ function Client:_notify(method, params)
return client_active
end
---- @private
--- Cancels a request with a given request id.
---
---- @param id (integer) id of request to cancel
---- @return boolean status true if notification was successful. false otherwise
---- @see |vim.lsp.client.notify()|
-function Client:_cancel_request(id)
- validate('id', id, 'number')
- local request = self.requests[id]
- if request and request.type == 'pending' then
- request.type = 'cancel'
- api.nvim_exec_autocmds('LspRequest', {
- buffer = api.nvim_buf_is_valid(request.bufnr) and request.bufnr or nil,
- modeline = false,
- data = { client_id = self.id, request_id = id, request = request },
- })
- end
+--- @param id integer id of request to cancel
+--- @return boolean status indicating if the notification was successful.
+--- @see |Client:notify()|
+function Client:cancel_request(id)
+ self:_process_request(id, 'cancel')
return self.rpc.notify(ms.dollar_cancelRequest, { id = id })
end
---- @private
--- Stops a client, optionally with force.
---
---- By default, it will just ask the - server to shutdown without force. If
+--- By default, it will just request the server to shutdown without force. If
--- you request to stop a client which has previously been requested to
--- shutdown, it will automatically escalate and force shutdown.
---
---- @param force boolean|nil
-function Client:_stop(force)
+--- @param force? boolean
+function Client:stop(force)
local rpc = self.rpc
if rpc.is_closing() then
return
end
+ vim.lsp._watchfiles.cancel(self.id)
+
if force or not self.initialized or self._graceful_shutdown_failed then
rpc.terminate()
return
@@ -863,7 +821,6 @@ function Client:_stop(force)
rpc.terminate()
self._graceful_shutdown_failed = true
end
- vim.lsp._watchfiles.cancel(self.id)
end)
end
@@ -945,20 +902,22 @@ end
--- @param bufnr? integer
--- @return lsp.Registration?
function Client:_get_registration(method, bufnr)
- bufnr = bufnr or vim.api.nvim_get_current_buf()
+ bufnr = vim._resolve_bufnr(bufnr)
for _, reg in ipairs(self.registrations[method] or {}) do
- if not reg.registerOptions or not reg.registerOptions.documentSelector then
+ local regoptions = reg.registerOptions --[[@as {documentSelector:lsp.TextDocumentFilter[]}]]
+ if not regoptions or not regoptions.documentSelector then
return reg
end
- local documentSelector = reg.registerOptions.documentSelector
+ local documentSelector = regoptions.documentSelector
local language = self:_get_language_id(bufnr)
local uri = vim.uri_from_bufnr(bufnr)
local fname = vim.uri_to_fname(uri)
for _, filter in ipairs(documentSelector) do
+ local flang, fscheme, fpat = filter.language, filter.scheme, filter.pattern
if
- not (filter.language and language ~= filter.language)
- and not (filter.scheme and not vim.startswith(uri, filter.scheme .. ':'))
- and not (filter.pattern and not vim.glob.to_lpeg(filter.pattern):match(fname))
+ not (flang and language ~= flang)
+ and not (fscheme and not vim.startswith(uri, fscheme .. ':'))
+ and not (type(fpat) == 'string' and not vim.glob.to_lpeg(fpat):match(fname))
then
return reg
end
@@ -966,12 +925,11 @@ function Client:_get_registration(method, bufnr)
end
end
---- @private
--- Checks whether a client is stopped.
---
--- @return boolean # true if client is stopped or in the process of being
--- stopped; false otherwise
-function Client:_is_stopped()
+function Client:is_stopped()
return self.rpc.is_closing()
end
@@ -983,7 +941,7 @@ end
--- @param handler? lsp.Handler only called if a server command
function Client:exec_cmd(command, context, handler)
context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]]
- context.bufnr = context.bufnr or api.nvim_get_current_buf()
+ context.bufnr = vim._resolve_bufnr(context.bufnr)
context.client_id = self.id
local cmdname = command.command
local fn = self.commands[cmdname] or lsp.commands[cmdname]
@@ -1013,7 +971,7 @@ function Client:exec_cmd(command, context, handler)
command = cmdname,
arguments = command.arguments,
}
- self.request(ms.workspace_executeCommand, params, handler, context.bufnr)
+ self:request(ms.workspace_executeCommand, params, handler, context.bufnr)
end
--- Default handler for the 'textDocument/didOpen' LSP notification.
@@ -1021,14 +979,14 @@ end
--- @param bufnr integer Number of the buffer, or 0 for current
function Client:_text_document_did_open_handler(bufnr)
changetracking.init(self, bufnr)
- if not self.supports_method(ms.textDocument_didOpen) then
+ if not self:supports_method(ms.textDocument_didOpen) then
return
end
if not api.nvim_buf_is_loaded(bufnr) then
return
end
- self.notify(ms.textDocument_didOpen, {
+ self:notify(ms.textDocument_didOpen, {
textDocument = {
version = lsp.util.buf_versions[bufnr],
uri = vim.uri_from_bufnr(bufnr),
@@ -1049,8 +1007,9 @@ function Client:_text_document_did_open_handler(bufnr)
end
--- Runs the on_attach function from the client's config if it was defined.
+--- Useful for buffer-local setup.
--- @param bufnr integer Buffer number
-function Client:_on_attach(bufnr)
+function Client:on_attach(bufnr)
self:_text_document_did_open_handler(bufnr)
lsp._set_defaults(self, bufnr)
@@ -1085,10 +1044,18 @@ function Client:write_error(code, err)
err_message(self._log_prefix, ': Error ', client_error, ': ', vim.inspect(err))
end
---- @private
+--- Checks if a client supports a given method.
+--- Always returns true for unknown off-spec methods.
+---
+--- Note: Some language server capabilities can be file specific.
--- @param method string
---- @param opts? {bufnr: integer?}
-function Client:_supports_method(method, opts)
+--- @param bufnr? integer
+function Client:supports_method(method, bufnr)
+ -- Deprecated form
+ if type(bufnr) == 'table' then
+ --- @diagnostic disable-next-line:no-unknown
+ bufnr = bufnr.bufnr
+ end
local required_capability = lsp._request_name_to_capability[method]
-- if we don't know about the method, assume that the client supports it.
if not required_capability then
@@ -1101,12 +1068,12 @@ function Client:_supports_method(method, opts)
local rmethod = lsp._resolve_to_request[method]
if rmethod then
if self:_supports_registration(rmethod) then
- local reg = self:_get_registration(rmethod, opts and opts.bufnr)
+ local reg = self:_get_registration(rmethod, bufnr)
return vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider') or false
end
else
if self:_supports_registration(method) then
- return self:_get_registration(method, opts and opts.bufnr) ~= nil
+ return self:_get_registration(method, bufnr) ~= nil
end
end
return false
@@ -1205,9 +1172,9 @@ function Client:_add_workspace_folder(dir)
end
end
- local wf = assert(get_workspace_folders(dir))
+ local wf = assert(lsp._get_workspace_folders(dir))
- self:_notify(ms.workspace_didChangeWorkspaceFolders, {
+ self:notify(ms.workspace_didChangeWorkspaceFolders, {
event = { added = wf, removed = {} },
})
@@ -1220,9 +1187,9 @@ end
--- Remove a directory to the workspace folders.
--- @param dir string?
function Client:_remove_workspace_folder(dir)
- local wf = assert(get_workspace_folders(dir))
+ local wf = assert(lsp._get_workspace_folders(dir))
- self:_notify(ms.workspace_didChangeWorkspaceFolders, {
+ self:notify(ms.workspace_didChangeWorkspaceFolders, {
event = { added = {}, removed = wf },
})
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index fdbdda695a..e36d8fee27 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -21,7 +21,7 @@ local lens_cache_by_buf = setmetatable({}, {
---client_id -> namespace
local namespaces = setmetatable({}, {
__index = function(t, key)
- local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key)
+ local value = api.nvim_create_namespace('nvim.lsp.codelens:' .. key)
rawset(t, key, value)
return value
end,
@@ -30,7 +30,7 @@ local namespaces = setmetatable({}, {
---@private
M.__namespaces = namespaces
-local augroup = api.nvim_create_augroup('vim_lsp_codelens', {})
+local augroup = api.nvim_create_augroup('nvim.lsp.codelens', {})
api.nvim_create_autocmd('LspDetach', {
group = augroup,
@@ -104,16 +104,12 @@ function M.run()
end
end
-local function resolve_bufnr(bufnr)
- return bufnr == 0 and api.nvim_get_current_buf() or bufnr
-end
-
--- Clear the lenses
---
---@param client_id integer|nil filter by client_id. All clients if nil
---@param bufnr integer|nil filter by buffer. All buffers if nil, 0 for current buffer
function M.clear(client_id, bufnr)
- bufnr = bufnr and resolve_bufnr(bufnr)
+ bufnr = bufnr and vim._resolve_bufnr(bufnr)
local buffers = bufnr and { bufnr }
or vim.tbl_filter(api.nvim_buf_is_loaded, api.nvim_list_bufs())
for _, iter_bufnr in pairs(buffers) do
@@ -231,7 +227,7 @@ local function resolve_lenses(lenses, bufnr, client_id, callback)
countdown()
else
assert(client)
- client.request(ms.codeLens_resolve, lens, function(_, result)
+ client:request(ms.codeLens_resolve, lens, function(_, result)
if api.nvim_buf_is_loaded(bufnr) and result and result.command then
lens.command = result.command
-- Eager display to have some sort of incremental feedback
@@ -296,7 +292,7 @@ end
--- @param opts? vim.lsp.codelens.refresh.Opts Optional fields
function M.refresh(opts)
opts = opts or {}
- local bufnr = opts.bufnr and resolve_bufnr(opts.bufnr)
+ local bufnr = opts.bufnr and vim._resolve_bufnr(opts.bufnr)
local buffers = bufnr and { bufnr }
or vim.tbl_filter(api.nvim_buf_is_loaded, api.nvim_list_bufs())
diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua
index 92bc110a97..cf6d07745f 100644
--- a/runtime/lua/vim/lsp/completion.lua
+++ b/runtime/lua/vim/lsp/completion.lua
@@ -127,8 +127,10 @@ end
--- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
---
--- @param item lsp.CompletionItem
+--- @param prefix string
+--- @param match fun(text: string, prefix: string):boolean
--- @return string
-local function get_completion_word(item)
+local function get_completion_word(item, prefix, match)
if item.insertTextFormat == protocol.InsertTextFormat.Snippet then
if item.textEdit then
-- Use label instead of text if text has different starting characters.
@@ -146,7 +148,12 @@ local function get_completion_word(item)
--
-- Typing `i` would remove the candidate because newText starts with `t`.
local text = parse_snippet(item.insertText or item.textEdit.newText)
- return #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
+ local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
+ if item.filterText and not match(word, prefix) then
+ return item.filterText
+ else
+ return word
+ end
elseif item.insertText and item.insertText ~= '' then
return parse_snippet(item.insertText)
else
@@ -224,6 +231,9 @@ end
---@param prefix string
---@return boolean
local function match_item_by_value(value, prefix)
+ if prefix == '' then
+ return true
+ end
if vim.o.completeopt:find('fuzzy') ~= nil then
return next(vim.fn.matchfuzzy({ value }, prefix)) ~= nil
end
@@ -276,7 +286,7 @@ function M._lsp_to_complete_items(result, prefix, client_id)
local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert')
for _, item in ipairs(items) do
if matches(item) then
- local word = get_completion_word(item)
+ local word = get_completion_word(item, prefix, match_item_by_value)
local hl_group = ''
if
item.deprecated
@@ -404,7 +414,7 @@ local function request(clients, bufnr, win, callback)
for _, client in pairs(clients) do
local client_id = client.id
local params = lsp.util.make_position_params(win, client.offset_encoding)
- local ok, request_id = client.request(ms.textDocument_completion, params, function(err, result)
+ local ok, request_id = client:request(ms.textDocument_completion, params, function(err, result)
responses[client_id] = { err = err, result = result }
remaining_requests = remaining_requests - 1
if remaining_requests == 0 then
@@ -421,7 +431,7 @@ local function request(clients, bufnr, win, callback)
for client_id, request_id in pairs(request_ids) do
local client = lsp.get_client_by_id(client_id)
if client then
- client.cancel_request(request_id)
+ client:cancel_request(request_id)
end
end
end
@@ -460,7 +470,7 @@ local function trigger(bufnr, clients)
local server_start_boundary --- @type integer?
for client_id, response in pairs(responses) do
if response.err then
- vim.notify_once(response.err.message, vim.log.levels.warn)
+ vim.notify_once(response.err.message, vim.log.levels.WARN)
end
local result = response.result
@@ -550,7 +560,7 @@ local function on_complete_done()
return
end
- local offset_encoding = client.offset_encoding or 'utf-16'
+ local position_encoding = client.offset_encoding or 'utf-16'
local resolve_provider = (client.server_capabilities.completionProvider or {}).resolveProvider
local function clear_word()
@@ -576,13 +586,13 @@ local function on_complete_done()
if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then
clear_word()
- lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, offset_encoding)
+ lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, position_encoding)
apply_snippet_and_command()
elseif resolve_provider and type(completion_item) == 'table' then
local changedtick = vim.b[bufnr].changedtick
--- @param result lsp.CompletionItem
- client.request(ms.completionItem_resolve, completion_item, function(err, result)
+ client:request(ms.completionItem_resolve, completion_item, function(err, result)
if changedtick ~= vim.b[bufnr].changedtick then
return
end
@@ -591,7 +601,7 @@ local function on_complete_done()
if err then
vim.notify_once(err.message, vim.log.levels.WARN)
elseif result and result.additionalTextEdits then
- lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, offset_encoding)
+ lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, position_encoding)
if result.command then
completion_item.command = result.command
end
@@ -605,6 +615,12 @@ local function on_complete_done()
end
end
+---@param bufnr integer
+---@return string
+local function get_augroup(bufnr)
+ return string.format('nvim.lsp.completion_%d', bufnr)
+end
+
--- @class vim.lsp.completion.BufferOpts
--- @field autotrigger? boolean Default: false When true, completion triggers automatically based on the server's `triggerCharacters`.
--- @field convert? fun(item: lsp.CompletionItem): table Transforms an LSP CompletionItem to |complete-items|.
@@ -629,8 +645,7 @@ local function enable_completions(client_id, bufnr, opts)
})
-- Set up autocommands.
- local group =
- api.nvim_create_augroup(string.format('vim/lsp/completion-%d', bufnr), { clear = true })
+ local group = api.nvim_create_augroup(get_augroup(bufnr), { clear = true })
api.nvim_create_autocmd('CompleteDone', {
group = group,
buffer = bufnr,
@@ -698,7 +713,7 @@ local function disable_completions(client_id, bufnr)
handle.clients[client_id] = nil
if not next(handle.clients) then
buf_handles[bufnr] = nil
- api.nvim_del_augroup_by_name(string.format('vim/lsp/completion-%d', bufnr))
+ api.nvim_del_augroup_by_name(get_augroup(bufnr))
else
for char, clients in pairs(handle.triggers) do
--- @param c vim.lsp.Client
@@ -716,7 +731,7 @@ end
--- @param bufnr integer Buffer handle, or 0 for the current buffer
--- @param opts? vim.lsp.completion.BufferOpts
function M.enable(enable, client_id, bufnr, opts)
- bufnr = (bufnr == 0 and api.nvim_get_current_buf()) or bufnr
+ bufnr = vim._resolve_bufnr(bufnr)
if enable then
enable_completions(client_id, bufnr, opts or {})
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 8fd30c7668..fe24928a69 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -5,7 +5,7 @@ local api = vim.api
local M = {}
-local augroup = api.nvim_create_augroup('vim_lsp_diagnostic', {})
+local augroup = api.nvim_create_augroup('nvim.lsp.diagnostic', {})
local DEFAULT_CLIENT_ID = -1
@@ -20,7 +20,7 @@ end
---@return lsp.DiagnosticSeverity
local function severity_vim_to_lsp(severity)
if type(severity) == 'string' then
- severity = vim.diagnostic.severity[severity]
+ severity = vim.diagnostic.severity[severity] --- @type integer
end
return severity
end
@@ -77,7 +77,7 @@ end
local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
local buf_lines = get_buf_lines(bufnr)
local client = vim.lsp.get_client_by_id(client_id)
- local offset_encoding = client and client.offset_encoding or 'utf-16'
+ local position_encoding = client and client.offset_encoding or 'utf-16'
--- @param diagnostic lsp.Diagnostic
--- @return vim.Diagnostic
return vim.tbl_map(function(diagnostic)
@@ -89,15 +89,16 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
string.format('Unsupported Markup message from LSP client %d', client_id),
vim.lsp.log_levels.ERROR
)
+ --- @diagnostic disable-next-line: undefined-field,no-unknown
message = diagnostic.message.value
end
local line = buf_lines and buf_lines[start.line + 1] or ''
--- @type vim.Diagnostic
return {
lnum = start.line,
- col = vim.str_byteindex(line, offset_encoding, start.character, false),
+ col = vim.str_byteindex(line, position_encoding, start.character, false),
end_lnum = _end.line,
- end_col = vim.str_byteindex(line, offset_encoding, _end.character, false),
+ end_col = vim.str_byteindex(line, position_encoding, _end.character, false),
severity = severity_lsp_to_vim(diagnostic.severity),
message = message,
source = diagnostic.source,
@@ -208,7 +209,7 @@ end
--- @param uri string
--- @param client_id? integer
---- @param diagnostics vim.Diagnostic[]
+--- @param diagnostics lsp.Diagnostic[]
--- @param is_pull boolean
local function handle_diagnostics(uri, client_id, diagnostics, is_pull)
local fname = vim.uri_to_fname(uri)
@@ -246,10 +247,18 @@ end
---
--- See |vim.diagnostic.config()| for configuration options.
---
----@param _ lsp.ResponseError?
+---@param error lsp.ResponseError?
---@param result lsp.DocumentDiagnosticReport
---@param ctx lsp.HandlerContext
-function M.on_diagnostic(_, result, ctx)
+function M.on_diagnostic(error, result, ctx)
+ if error ~= nil and error.code == protocol.ErrorCodes.ServerCancelled then
+ if error.data == nil or error.data.retriggerRequest ~= false then
+ local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
+ client:request(ctx.method, ctx.params)
+ end
+ return
+ end
+
if result == nil or result.kind == 'unchanged' then
return
end
@@ -348,9 +357,7 @@ end
---@param bufnr (integer) Buffer handle, or 0 for current
---@private
function M._enable(bufnr)
- if bufnr == nil or bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
if not bufstates[bufnr] then
bufstates[bufnr] = { enabled = true }
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 5c28d88b38..b35140dfad 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -47,7 +47,7 @@ RSC[ms.dollar_progress] = function(_, params, ctx)
local value = params.value
if type(value) == 'table' then
- kind = value.kind
+ kind = value.kind --- @type string
-- Carry over title of `begin` messages to `report` and `end` messages
-- So that consumers always have it available, even if they consume a
-- subset of the full sequence
@@ -247,12 +247,12 @@ local function response_to_list(map_result, entity, title_fn)
local items = map_result(result, ctx.bufnr)
local list = { title = title, items = items, context = ctx }
- if config.loclist then
- vim.fn.setloclist(0, {}, ' ', list)
- vim.cmd.lopen()
- elseif config.on_list then
+ if config.on_list then
assert(vim.is_callable(config.on_list), 'on_list is not a function')
config.on_list(list)
+ elseif config.loclist then
+ vim.fn.setloclist(0, {}, ' ', list)
+ vim.cmd.lopen()
else
vim.fn.setqflist({}, ' ', list)
vim.cmd('botright copen')
@@ -382,7 +382,7 @@ end
--- @diagnostic disable-next-line: deprecated
RCS[ms.textDocument_hover] = M.hover
-local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help')
+local sig_help_ns = api.nvim_create_namespace('nvim.lsp.signature_help')
--- @deprecated remove in 0.13
--- |lsp-handler| for the method "textDocument/signatureHelp".
@@ -582,9 +582,8 @@ NSC['window/showMessage'] = function(_, params, ctx)
if message_type == protocol.MessageType.Error then
err_message('LSP[', client_name, '] ', message)
else
- --- @type string
- local message_type_name = protocol.MessageType[message_type]
- api.nvim_out_write(string.format('LSP[%s][%s] %s\n', client_name, message_type_name, message))
+ message = ('LSP[%s][%s] %s\n'):format(client_name, protocol.MessageType[message_type], message)
+ api.nvim_echo({ { message } }, true, {})
end
return params
end
@@ -659,7 +658,8 @@ for k, fn in pairs(M) do
})
end
- if err then
+ -- ServerCancelled errors should be propagated to the request handler
+ if err and err.code ~= protocol.ErrorCodes.ServerCancelled then
-- LSP spec:
-- interface ResponseError:
-- code: integer;
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
index 0d314108fe..8af9f2f791 100644
--- a/runtime/lua/vim/lsp/health.lua
+++ b/runtime/lua/vim/lsp/health.lua
@@ -28,42 +28,48 @@ local function check_log()
report_fn(string.format('Log size: %d KB', log_size / 1000))
end
+--- @param f function
+--- @return string
+local function func_tostring(f)
+ local info = debug.getinfo(f, 'S')
+ return ('<function %s:%s>'):format(info.source, info.linedefined)
+end
+
local function check_active_clients()
vim.health.start('vim.lsp: Active Clients')
local clients = vim.lsp.get_clients()
if next(clients) then
for _, client in pairs(clients) do
+ local server_version = vim.tbl_get(client, 'server_info', 'version')
+ or '? (no serverInfo.version response)'
local cmd ---@type string
- if type(client.config.cmd) == 'table' then
- cmd = table.concat(client.config.cmd --[[@as table]], ' ')
- elseif type(client.config.cmd) == 'function' then
- cmd = tostring(client.config.cmd)
+ local ccmd = client.config.cmd
+ if type(ccmd) == 'table' then
+ cmd = vim.inspect(ccmd)
+ elseif type(ccmd) == 'function' then
+ cmd = func_tostring(ccmd)
end
local dirs_info ---@type string
if client.workspace_folders and #client.workspace_folders > 1 then
- dirs_info = string.format(
- ' Workspace folders:\n %s',
- vim
- .iter(client.workspace_folders)
- ---@param folder lsp.WorkspaceFolder
- :map(function(folder)
- return folder.name
- end)
- :join('\n ')
- )
+ local wfolders = {} --- @type string[]
+ for _, dir in ipairs(client.workspace_folders) do
+ wfolders[#wfolders + 1] = dir.name
+ end
+ dirs_info = ('- Workspace folders:\n %s'):format(table.concat(wfolders, '\n '))
else
dirs_info = string.format(
- ' Root directory: %s',
+ '- Root directory: %s',
client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~')
) or nil
end
report_info(table.concat({
string.format('%s (id: %d)', client.name, client.id),
+ string.format('- Version: %s', server_version),
dirs_info,
- string.format(' Command: %s', cmd),
- string.format(' Settings: %s', vim.inspect(client.settings, { newline = '\n ' })),
+ string.format('- Command: %s', cmd),
+ string.format('- Settings: %s', vim.inspect(client.settings, { newline = '\n ' })),
string.format(
- ' Attached buffers: %s',
+ '- Attached buffers: %s',
vim.iter(pairs(client.attached_buffers)):map(tostring):join(', ')
),
}, '\n'))
@@ -174,10 +180,45 @@ local function check_position_encodings()
end
end
+local function check_enabled_configs()
+ vim.health.start('vim.lsp: Enabled Configurations')
+
+ for name in vim.spairs(vim.lsp._enabled_configs) do
+ local config = vim.lsp.config[name]
+ local text = {} --- @type string[]
+ text[#text + 1] = ('%s:'):format(name)
+ for k, v in
+ vim.spairs(config --[[@as table<string,any>]])
+ do
+ local v_str --- @type string?
+ if k == 'name' then
+ v_str = nil
+ elseif k == 'filetypes' or k == 'root_markers' then
+ v_str = table.concat(v, ', ')
+ elseif type(v) == 'function' then
+ v_str = func_tostring(v)
+ else
+ v_str = vim.inspect(v, { newline = '\n ' })
+ end
+
+ if k == 'cmd' and type(v) == 'table' and vim.fn.executable(v[1]) == 0 then
+ report_warn(("'%s' is not executable. Configuration will not be used."):format(v[1]))
+ end
+
+ if v_str then
+ text[#text + 1] = ('- %s: %s'):format(k, v_str)
+ end
+ end
+ text[#text + 1] = ''
+ report_info(table.concat(text, '\n'))
+ end
+end
+
--- Performs a healthcheck for LSP
function M.check()
check_log()
check_active_clients()
+ check_enabled_configs()
check_watcher()
check_position_encodings()
end
diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua
index f1ae9a8e9e..37e1202d1d 100644
--- a/runtime/lua/vim/lsp/inlay_hint.lua
+++ b/runtime/lua/vim/lsp/inlay_hint.lua
@@ -29,8 +29,8 @@ local bufstates = vim.defaulttable(function(_)
})
end)
-local namespace = api.nvim_create_namespace('vim_lsp_inlayhint')
-local augroup = api.nvim_create_augroup('vim_lsp_inlayhint', {})
+local namespace = api.nvim_create_namespace('nvim.lsp.inlayhint')
+local augroup = api.nvim_create_augroup('nvim.lsp.inlayhint', {})
--- |lsp-handler| for the method `textDocument/inlayHint`
--- Store hints for a specific buffer and client
@@ -122,12 +122,12 @@ end
--- local hint = vim.lsp.inlay_hint.get({ bufnr = 0 })[1] -- 0 for current buffer
---
--- local client = vim.lsp.get_client_by_id(hint.client_id)
---- local resp = client.request_sync('inlayHint/resolve', hint.inlay_hint, 100, 0)
+--- local resp = client:request_sync('inlayHint/resolve', hint.inlay_hint, 100, 0)
--- local resolved_hint = assert(resp and resp.result, resp.err)
--- vim.lsp.util.apply_text_edits(resolved_hint.textEdits, 0, client.encoding)
---
--- location = resolved_hint.label[1].location
---- client.request('textDocument/hover', {
+--- client:request('textDocument/hover', {
--- textDocument = { uri = location.uri },
--- position = location.range.start,
--- })
@@ -149,8 +149,8 @@ function M.get(filter)
vim.list_extend(hints, M.get(vim.tbl_extend('keep', { bufnr = buf }, filter)))
end, vim.api.nvim_list_bufs())
return hints
- elseif bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
+ else
+ bufnr = vim._resolve_bufnr(bufnr)
end
local bufstate = bufstates[bufnr]
@@ -203,9 +203,7 @@ end
--- Clear inlay hints
---@param bufnr (integer) Buffer handle, or 0 for current
local function clear(bufnr)
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local bufstate = bufstates[bufnr]
local client_lens = (bufstate or {}).client_hints or {}
local client_ids = vim.tbl_keys(client_lens) --- @type integer[]
@@ -221,9 +219,7 @@ end
--- Disable inlay hints for a buffer
---@param bufnr (integer) Buffer handle, or 0 for current
local function _disable(bufnr)
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
clear(bufnr)
bufstates[bufnr] = nil
bufstates[bufnr].enabled = false
@@ -242,9 +238,7 @@ end
--- Enable inlay hints for a buffer
---@param bufnr (integer) Buffer handle, or 0 for current
local function _enable(bufnr)
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
bufstates[bufnr] = nil
bufstates[bufnr].enabled = true
_refresh(bufnr)
@@ -371,13 +365,10 @@ function M.is_enabled(filter)
filter = filter or {}
local bufnr = filter.bufnr
- vim.validate('bufnr', bufnr, 'number', true)
if bufnr == nil then
return globalstate.enabled
- elseif bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
end
- return bufstates[bufnr].enabled
+ return bufstates[vim._resolve_bufnr(bufnr)].enabled
end
--- Optional filters |kwargs|, or `nil` for all.
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 7db48b0c06..fbfd0cd6b0 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -15,7 +15,6 @@ local sysname = vim.uv.os_uname().sysname
--- @class vim.lsp.protocol.constants
--- @nodoc
local constants = {
- --- @enum lsp.DiagnosticSeverity
DiagnosticSeverity = {
-- Reports an error.
Error = 1,
@@ -27,7 +26,6 @@ local constants = {
Hint = 4,
},
- --- @enum lsp.DiagnosticTag
DiagnosticTag = {
-- Unused or unnecessary code
Unnecessary = 1,
@@ -35,7 +33,6 @@ local constants = {
Deprecated = 2,
},
- ---@enum lsp.MessageType
MessageType = {
-- An error message.
Error = 1,
@@ -50,7 +47,6 @@ local constants = {
},
-- The file event type.
- ---@enum lsp.FileChangeType
FileChangeType = {
-- The file got created.
Created = 1,
@@ -149,7 +145,6 @@ local constants = {
},
-- Represents reasons why a text document is saved.
- ---@enum lsp.TextDocumentSaveReason
TextDocumentSaveReason = {
-- Manually triggered, e.g. by the user pressing save, by starting debugging,
-- or by an API call.
@@ -174,6 +169,7 @@ local constants = {
-- Defined by the protocol.
RequestCancelled = -32800,
ContentModified = -32801,
+ ServerCancelled = -32802,
},
-- Describes the content type that a client supports in various
@@ -245,7 +241,6 @@ local constants = {
-- Defines whether the insert text in a completion item should be interpreted as
-- plain text or a snippet.
- --- @enum lsp.InsertTextFormat
InsertTextFormat = {
-- The primary text to be inserted is treated as a plain string.
PlainText = 1,
@@ -304,7 +299,6 @@ local constants = {
SourceOrganizeImports = 'source.organizeImports',
},
-- The reason why code actions were requested.
- ---@enum lsp.CodeActionTriggerKind
CodeActionTriggerKind = {
-- Code actions were explicitly requested by the user or by an extension.
Invoked = 1,
@@ -439,6 +433,13 @@ function protocol.make_client_capabilities()
properties = { 'command' },
},
},
+ foldingRange = {
+ dynamicRegistration = false,
+ lineFoldingOnly = true,
+ foldingRange = {
+ collapsedText = true,
+ },
+ },
formatting = {
dynamicRegistration = true,
},
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 6c8564845f..a0d1fe776b 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -1,18 +1,8 @@
-local uv = vim.uv
local log = require('vim.lsp.log')
local protocol = require('vim.lsp.protocol')
+local lsp_transport = require('vim.lsp._transport')
local validate, schedule_wrap = vim.validate, vim.schedule_wrap
-local is_win = vim.fn.has('win32') == 1
-
---- Checks whether a given path exists and is a directory.
----@param filename string path to check
----@return boolean
-local function is_dir(filename)
- local stat = uv.fs_stat(filename)
- return stat and stat.type == 'directory' or false
-end
-
--- Embeds the given string into a table and correctly computes `Content-Length`.
---
---@param message string
@@ -242,8 +232,11 @@ local default_dispatchers = {
end,
}
----@private
-function M.create_read_loop(handle_body, on_no_chunk, on_error)
+--- @private
+--- @param handle_body fun(body: string)
+--- @param on_exit? fun()
+--- @param on_error fun(err: any)
+function M.create_read_loop(handle_body, on_exit, on_error)
local parse_chunk = coroutine.wrap(request_parser_loop) --[[@as fun(chunk: string?): vim.lsp.rpc.Headers?, string?]]
parse_chunk()
return function(err, chunk)
@@ -253,8 +246,8 @@ function M.create_read_loop(handle_body, on_no_chunk, on_error)
end
if not chunk then
- if on_no_chunk then
- on_no_chunk()
+ if on_exit then
+ on_exit()
end
return
end
@@ -262,7 +255,7 @@ function M.create_read_loop(handle_body, on_no_chunk, on_error)
while true do
local headers, body = parse_chunk(chunk)
if headers then
- handle_body(body)
+ handle_body(assert(body))
chunk = ''
else
break
@@ -282,14 +275,14 @@ local Client = {}
---@private
function Client:encode_and_send(payload)
log.debug('rpc.send', payload)
- if self.transport.is_closing() then
+ if self.transport:is_closing() then
return false
end
local jsonstr = assert(
vim.json.encode(payload),
string.format("Couldn't encode payload '%s'", vim.inspect(payload))
)
- self.transport.write(format_message_with_content_length(jsonstr))
+ self.transport:write(format_message_with_content_length(jsonstr))
return true
end
@@ -323,7 +316,7 @@ end
---@param method string The invoked LSP method
---@param params table? Parameters for the invoked LSP method
---@param callback fun(err?: lsp.ResponseError, result: any) Callback to invoke
----@param notify_reply_callback fun(message_id: integer)|nil Callback to invoke as soon as a request is no longer pending
+---@param notify_reply_callback? fun(message_id: integer) Callback to invoke as soon as a request is no longer pending
---@return boolean success `true` if request could be sent, `false` if not
---@return integer? message_id if request could be sent, `nil` if not
function Client:request(method, params, callback, notify_reply_callback)
@@ -337,21 +330,16 @@ function Client:request(method, params, callback, notify_reply_callback)
method = method,
params = params,
})
- local message_callbacks = self.message_callbacks
- local notify_reply_callbacks = self.notify_reply_callbacks
- if result then
- if message_callbacks then
- message_callbacks[message_id] = schedule_wrap(callback)
- else
- return false, nil
- end
- if notify_reply_callback and notify_reply_callbacks then
- notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
- end
- return result, message_id
- else
- return false, nil
+
+ if not result then
+ return false
+ end
+
+ self.message_callbacks[message_id] = schedule_wrap(callback)
+ if notify_reply_callback then
+ self.notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
end
+ return result, message_id
end
---@package
@@ -370,7 +358,7 @@ end
---@param ... any
---@return boolean status
---@return any head
----@return any|nil ...
+---@return any? ...
function Client:pcall_handler(errkind, status, head, ...)
if not status then
self:on_error(errkind, head, ...)
@@ -385,7 +373,7 @@ end
---@param ... any
---@return boolean status
---@return any head
----@return any|nil ...
+---@return any? ...
function Client:try_call(errkind, fn, ...)
return self:pcall_handler(errkind, pcall(fn, ...))
end
@@ -394,7 +382,8 @@ end
-- time and log them. This would require storing the timestamp. I could call
-- them with an error then, perhaps.
----@package
+--- @package
+--- @param body string
function Client:handle_body(body)
local ok, decoded = pcall(vim.json.decode, body, { luanil = { object = true } })
if not ok then
@@ -406,7 +395,7 @@ function Client:handle_body(body)
if type(decoded) ~= 'table' then
self:on_error(M.client_errors.INVALID_SERVER_MESSAGE, decoded)
elseif type(decoded.method) == 'string' and decoded.id then
- local err --- @type lsp.ResponseError|nil
+ local err --- @type lsp.ResponseError?
-- Schedule here so that the users functions don't trigger an error and
-- we can still use the result.
vim.schedule(coroutine.wrap(function()
@@ -453,45 +442,36 @@ function Client:handle_body(body)
local result_id = assert(tonumber(decoded.id), 'response id must be a number')
-- Notify the user that a response was received for the request
- local notify_reply_callbacks = self.notify_reply_callbacks
- local notify_reply_callback = notify_reply_callbacks and notify_reply_callbacks[result_id]
+ local notify_reply_callback = self.notify_reply_callbacks[result_id]
if notify_reply_callback then
validate('notify_reply_callback', notify_reply_callback, 'function')
notify_reply_callback(result_id)
- notify_reply_callbacks[result_id] = nil
+ self.notify_reply_callbacks[result_id] = nil
end
- local message_callbacks = self.message_callbacks
-
-- Do not surface RequestCancelled to users, it is RPC-internal.
if decoded.error then
- local mute_error = false
+ assert(type(decoded.error) == 'table')
if decoded.error.code == protocol.ErrorCodes.RequestCancelled then
log.debug('Received cancellation ack', decoded)
- mute_error = true
- end
-
- if mute_error then
-- Clear any callback since this is cancelled now.
-- This is safe to do assuming that these conditions hold:
-- - The server will not send a result callback after this cancellation.
-- - If the server sent this cancellation ACK after sending the result, the user of this RPC
-- client will ignore the result themselves.
- if result_id and message_callbacks then
- message_callbacks[result_id] = nil
+ if result_id then
+ self.message_callbacks[result_id] = nil
end
return
end
end
- local callback = message_callbacks and message_callbacks[result_id]
+ local callback = self.message_callbacks[result_id]
if callback then
- message_callbacks[result_id] = nil
+ self.message_callbacks[result_id] = nil
validate('callback', callback, 'function')
if decoded.error then
- decoded.error = setmetatable(decoded.error, {
- __tostring = M.format_rpc_error,
- })
+ setmetatable(decoded.error, { __tostring = M.format_rpc_error })
end
self:try_call(
M.client_errors.SERVER_RESULT_CALLBACK_ERROR,
@@ -517,11 +497,6 @@ function Client:handle_body(body)
end
end
----@class (private) vim.lsp.rpc.Transport
----@field write fun(msg: string)
----@field is_closing fun(): boolean
----@field terminate fun()
-
---@param dispatchers vim.lsp.rpc.Dispatchers
---@param transport vim.lsp.rpc.Transport
---@return vim.lsp.rpc.Client
@@ -536,11 +511,20 @@ local function new_client(dispatchers, transport)
return setmetatable(state, { __index = Client })
end
----@class vim.lsp.rpc.PublicClient
----@field request fun(method: string, params: table?, callback: fun(err: lsp.ResponseError|nil, result: any), notify_reply_callback: fun(message_id: integer)|nil):boolean,integer? see |vim.lsp.rpc.request()|
----@field notify fun(method: string, params: any):boolean see |vim.lsp.rpc.notify()|
----@field is_closing fun(): boolean
----@field terminate fun()
+--- Client RPC object
+--- @class vim.lsp.rpc.PublicClient
+---
+--- See [vim.lsp.rpc.request()]
+--- @field request fun(method: string, params: table?, callback: fun(err?: lsp.ResponseError, result: any), notify_reply_callback?: fun(message_id: integer)):boolean,integer?
+---
+--- See [vim.lsp.rpc.notify()]
+--- @field notify fun(method: string, params: any): boolean
+---
+--- Indicates if the RPC is closing.
+--- @field is_closing fun(): boolean
+---
+--- Terminates the RPC client.
+--- @field terminate fun()
---@param client vim.lsp.rpc.Client
---@return vim.lsp.rpc.PublicClient
@@ -551,20 +535,20 @@ local function public_client(client)
---@private
function result.is_closing()
- return client.transport.is_closing()
+ return client.transport:is_closing()
end
---@private
function result.terminate()
- client.transport.terminate()
+ client.transport:terminate()
end
--- Sends a request to the LSP server and runs {callback} upon response.
---
---@param method (string) The invoked LSP method
---@param params (table?) Parameters for the invoked LSP method
- ---@param callback fun(err: lsp.ResponseError|nil, result: any) Callback to invoke
- ---@param notify_reply_callback fun(message_id: integer)|nil Callback to invoke as soon as a request is no longer pending
+ ---@param callback fun(err: lsp.ResponseError?, result: any) Callback to invoke
+ ---@param notify_reply_callback? fun(message_id: integer) Callback to invoke as soon as a request is no longer pending
---@return boolean success `true` if request could be sent, `false` if not
---@return integer? message_id if request could be sent, `nil` if not
function result.request(method, params, callback, notify_reply_callback)
@@ -610,6 +594,21 @@ local function merge_dispatchers(dispatchers)
return merged
end
+--- @param client vim.lsp.rpc.Client
+--- @param on_exit? fun()
+local function create_client_read_loop(client, on_exit)
+ --- @param body string
+ local function handle_body(body)
+ client:handle_body(body)
+ end
+
+ local function on_error(err)
+ client:on_error(M.client_errors.READ_ERROR, err)
+ end
+
+ return M.create_read_loop(handle_body, on_exit, on_error)
+end
+
--- Create a LSP RPC client factory that connects to either:
---
--- - a named pipe (windows)
@@ -617,83 +616,26 @@ end
--- - a host and port via TCP
---
--- Return a function that can be passed to the `cmd` field for
---- |vim.lsp.start_client()| or |vim.lsp.start()|.
+--- |vim.lsp.start()|.
---
---@param host_or_path string host to connect to or path to a pipe/domain socket
---@param port integer? TCP port to connect to. If absent the first argument must be a pipe
---@return fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient
function M.connect(host_or_path, port)
+ validate('host_or_path', host_or_path, 'string')
+ validate('port', port, 'number', true)
+
return function(dispatchers)
+ validate('dispatchers', dispatchers, 'table', true)
+
dispatchers = merge_dispatchers(dispatchers)
- local handle = (
- port == nil
- and assert(
- uv.new_pipe(false),
- string.format('Pipe with name %s could not be opened.', host_or_path)
- )
- or assert(uv.new_tcp(), 'Could not create new TCP socket')
- )
- local closing = false
- -- Connect returns a PublicClient synchronously so the caller
- -- can immediately send messages before the connection is established
- -- -> Need to buffer them until that happens
- local connected = false
- -- size should be enough because the client can't really do anything until initialization is done
- -- which required a response from the server - implying the connection got established
- local msgbuf = vim.ringbuf(10)
- local transport = {
- write = function(msg)
- if connected then
- local _, err = handle:write(msg)
- if err and not closing then
- log.error('Error on handle:write: %q', err)
- end
- else
- msgbuf:push(msg)
- end
- end,
- is_closing = function()
- return closing
- end,
- terminate = function()
- if not closing then
- closing = true
- handle:shutdown()
- handle:close()
- dispatchers.on_exit(0, 0)
- end
- end,
- }
+
+ local transport = lsp_transport.TransportConnect.new()
local client = new_client(dispatchers, transport)
- local function on_connect(err)
- if err then
- local address = port == nil and host_or_path or (host_or_path .. ':' .. port)
- vim.schedule(function()
- vim.notify(
- string.format('Could not connect to %s, reason: %s', address, vim.inspect(err)),
- vim.log.levels.WARN
- )
- end)
- return
- end
- local handle_body = function(body)
- client:handle_body(body)
- end
- handle:read_start(M.create_read_loop(handle_body, transport.terminate, function(read_err)
- client:on_error(M.client_errors.READ_ERROR, read_err)
- end))
- connected = true
- for msg in msgbuf do
- handle:write(msg)
- end
- end
- if port == nil then
- handle:connect(host_or_path, on_connect)
- else
- local info = uv.getaddrinfo(host_or_path, nil)
- local resolved_host = info and info[1] and info[1].addr or host_or_path
- handle:connect(resolved_host, port, on_connect)
- end
+ local on_read = create_client_read_loop(client, function()
+ transport:terminate()
+ end)
+ transport:connect(host_or_path, port, on_read, dispatchers.on_exit)
return public_client(client)
end
@@ -713,83 +655,19 @@ end
--- @param cmd string[] Command to start the LSP server.
--- @param dispatchers? vim.lsp.rpc.Dispatchers
--- @param extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams
---- @return vim.lsp.rpc.PublicClient : Client RPC object, with these methods:
---- - `notify()` |vim.lsp.rpc.notify()|
---- - `request()` |vim.lsp.rpc.request()|
---- - `is_closing()` returns a boolean indicating if the RPC is closing.
---- - `terminate()` terminates the RPC client.
+--- @return vim.lsp.rpc.PublicClient
function M.start(cmd, dispatchers, extra_spawn_params)
log.info('Starting RPC client', { cmd = cmd, extra = extra_spawn_params })
validate('cmd', cmd, 'table')
validate('dispatchers', dispatchers, 'table', true)
- extra_spawn_params = extra_spawn_params or {}
-
- if extra_spawn_params.cwd then
- assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
- end
-
dispatchers = merge_dispatchers(dispatchers)
- local sysobj ---@type vim.SystemObj
-
- local client = new_client(dispatchers, {
- write = function(msg)
- sysobj:write(msg)
- end,
- is_closing = function()
- return sysobj == nil or sysobj:is_closing()
- end,
- terminate = function()
- sysobj:kill(15)
- end,
- })
-
- local handle_body = function(body)
- client:handle_body(body)
- end
-
- local stdout_handler = M.create_read_loop(handle_body, nil, function(err)
- client:on_error(M.client_errors.READ_ERROR, err)
- end)
-
- local stderr_handler = function(_, chunk)
- if chunk then
- log.error('rpc', cmd[1], 'stderr', chunk)
- end
- end
-
- local detached = not is_win
- if extra_spawn_params.detached ~= nil then
- detached = extra_spawn_params.detached
- end
-
- local ok, sysobj_or_err = pcall(vim.system, cmd, {
- stdin = true,
- stdout = stdout_handler,
- stderr = stderr_handler,
- cwd = extra_spawn_params.cwd,
- env = extra_spawn_params.env,
- detach = detached,
- }, function(obj)
- dispatchers.on_exit(obj.code, obj.signal)
- end)
-
- if not ok then
- local err = sysobj_or_err --[[@as string]]
- local sfx --- @type string
- if string.match(err, 'ENOENT') then
- sfx = '. The language server is either not installed, missing from PATH, or not executable.'
- else
- sfx = string.format(' with error message: %s', err)
- end
- local msg =
- string.format('Spawning language server with cmd: `%s` failed%s', vim.inspect(cmd), sfx)
- error(msg)
- end
-
- sysobj = sysobj_or_err --[[@as vim.SystemObj]]
+ local transport = lsp_transport.TransportRun.new()
+ local client = new_client(dispatchers, transport)
+ local on_read = create_client_read_loop(client)
+ transport:run(cmd, extra_spawn_params, on_read, dispatchers.on_exit)
return public_client(client)
end
diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua
index 215e5f41aa..dd8b654856 100644
--- a/runtime/lua/vim/lsp/semantic_tokens.lua
+++ b/runtime/lua/vim/lsp/semantic_tokens.lua
@@ -139,7 +139,7 @@ local function tokens_to_ranges(data, bufnr, client, request)
if token_type then
local modifiers = modifiers_from_number(data[i + 4], token_modifiers)
- local end_char = start_char + data[i + 2]
+ local end_char = start_char + data[i + 2] --- @type integer LuaLS bug
local buf_line = lines and lines[line + 1] or ''
local start_col = vim.str_byteindex(buf_line, encoding, start_char, false)
local end_col = vim.str_byteindex(buf_line, encoding, end_char, false)
@@ -166,7 +166,7 @@ function STHighlighter.new(bufnr)
local self = setmetatable({}, { __index = STHighlighter })
self.bufnr = bufnr
- self.augroup = api.nvim_create_augroup('vim_lsp_semantic_tokens:' .. bufnr, { clear = true })
+ self.augroup = api.nvim_create_augroup('nvim.lsp.semantic_tokens:' .. bufnr, { clear = true })
self.client_state = {}
STHighlighter.active[bufnr] = self
@@ -225,7 +225,7 @@ function STHighlighter:attach(client_id)
local state = self.client_state[client_id]
if not state then
state = {
- namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens:' .. client_id),
+ namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens:' .. client_id),
active_request = {},
current_result = {},
}
@@ -273,7 +273,7 @@ function STHighlighter:send_request()
if client and current_result.version ~= version and active_request.version ~= version then
-- cancel stale in-flight request
if active_request.request_id then
- client.cancel_request(active_request.request_id)
+ client:cancel_request(active_request.request_id)
active_request = {}
state.active_request = active_request
end
@@ -288,7 +288,7 @@ function STHighlighter:send_request()
method = method .. '/delta'
params.previousResultId = current_result.result_id
end
- local success, request_id = client.request(method, params, function(err, response, ctx)
+ local success, request_id = client:request(method, params, function(err, response, ctx)
-- look client up again using ctx.client_id instead of using a captured
-- client object
local c = vim.lsp.get_client_by_id(ctx.client_id)
@@ -519,7 +519,7 @@ function STHighlighter:reset()
if state.active_request.request_id then
local client = vim.lsp.get_client_by_id(client_id)
assert(client)
- client.cancel_request(state.active_request.request_id)
+ client:cancel_request(state.active_request.request_id)
state.active_request = {}
end
end
@@ -547,7 +547,7 @@ function STHighlighter:mark_dirty(client_id)
if state.active_request.request_id then
local client = vim.lsp.get_client_by_id(client_id)
assert(client)
- client.cancel_request(state.active_request.request_id)
+ client:cancel_request(state.active_request.request_id)
state.active_request = {}
end
end
@@ -600,9 +600,7 @@ function M.start(bufnr, client_id, opts)
vim.validate('bufnr', bufnr, 'number')
vim.validate('client_id', client_id, 'number')
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
opts = opts or {}
assert(
@@ -655,9 +653,7 @@ function M.stop(bufnr, client_id)
vim.validate('bufnr', bufnr, 'number')
vim.validate('client_id', client_id, 'number')
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local highlighter = STHighlighter.active[bufnr]
if not highlighter then
@@ -691,9 +687,7 @@ end
--- - modifiers (table) token modifiers as a set. E.g., { static = true, readonly = true }
--- - client_id (integer)
function M.get_at_pos(bufnr, row, col)
- if bufnr == nil or bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local highlighter = STHighlighter.active[bufnr]
if not highlighter then
@@ -739,8 +733,7 @@ function M.force_refresh(bufnr)
vim.validate('bufnr', bufnr, 'number', true)
local buffers = bufnr == nil and vim.tbl_keys(STHighlighter.active)
- or bufnr == 0 and { api.nvim_get_current_buf() }
- or { bufnr }
+ or { vim._resolve_bufnr(bufnr) }
for _, buffer in ipairs(buffers) do
local highlighter = STHighlighter.active[buffer]
@@ -770,9 +763,7 @@ end
---@param hl_group (string) Highlight group name
---@param opts? vim.lsp.semantic_tokens.highlight_token.Opts Optional parameters:
function M.highlight_token(token, bufnr, client_id, hl_group, opts)
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local highlighter = STHighlighter.active[bufnr]
if not highlighter then
return
@@ -814,7 +805,7 @@ function M._refresh(err, _, ctx)
return vim.NIL
end
-local namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens')
+local namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens')
api.nvim_set_decoration_provider(namespace, {
on_win = function(_, _, bufnr, topline, botline)
local highlighter = STHighlighter.active[bufnr]
diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua
index 3df45ebff0..621f63b25f 100644
--- a/runtime/lua/vim/lsp/sync.lua
+++ b/runtime/lua/vim/lsp/sync.lua
@@ -48,21 +48,21 @@ local str_utfindex = vim.str_utfindex
local str_utf_start = vim.str_utf_start
local str_utf_end = vim.str_utf_end
--- Given a line, byte idx, alignment, and offset_encoding convert to the aligned
+-- Given a line, byte idx, alignment, and position_encoding convert to the aligned
-- utf-8 index and either the utf-16, or utf-32 index.
---@param line string the line to index into
---@param byte integer the byte idx
----@param offset_encoding string utf-8|utf-16|utf-32|nil (default: utf-8)
+---@param position_encoding string utf-8|utf-16|utf-32|nil (default: utf-8)
---@return integer byte_idx of first change position
---@return integer char_idx of first change position
-local function align_end_position(line, byte, offset_encoding)
+local function align_end_position(line, byte, position_encoding)
local char --- @type integer
-- If on the first byte, or an empty string: the trivial case
if byte == 1 or #line == 0 then
char = byte
-- Called in the case of extending an empty line "" -> "a"
elseif byte == #line + 1 then
- char = str_utfindex(line, offset_encoding) + 1
+ char = str_utfindex(line, position_encoding) + 1
else
-- Modifying line, find the nearest utf codepoint
local offset = str_utf_start(line, byte)
@@ -73,9 +73,9 @@ local function align_end_position(line, byte, offset_encoding)
end
if byte <= #line then
--- Convert to 0 based for input, and from 0 based for output
- char = str_utfindex(line, offset_encoding, byte - 1) + 1
+ char = str_utfindex(line, position_encoding, byte - 1) + 1
else
- char = str_utfindex(line, offset_encoding) + 1
+ char = str_utfindex(line, position_encoding) + 1
end
-- Extending line, find the nearest utf codepoint for the last valid character
end
@@ -93,7 +93,7 @@ end
---@param firstline integer firstline from on_lines, adjusted to 1-index
---@param lastline integer lastline from on_lines, adjusted to 1-index
---@param new_lastline integer new_lastline from on_lines, adjusted to 1-index
----@param offset_encoding string utf-8|utf-16|utf-32|nil (fallback to utf-8)
+---@param position_encoding string utf-8|utf-16|utf-32|nil (fallback to utf-8)
---@return vim.lsp.sync.Range result table include line_idx, byte_idx, and char_idx of first change position
local function compute_start_range(
prev_lines,
@@ -101,7 +101,7 @@ local function compute_start_range(
firstline,
lastline,
new_lastline,
- offset_encoding
+ position_encoding
)
local char_idx --- @type integer?
local byte_idx --- @type integer?
@@ -115,7 +115,7 @@ local function compute_start_range(
if line then
line_idx = firstline - 1
byte_idx = #line + 1
- char_idx = str_utfindex(line, offset_encoding) + 1
+ char_idx = str_utfindex(line, position_encoding) + 1
else
line_idx = firstline
byte_idx = 1
@@ -152,11 +152,11 @@ local function compute_start_range(
char_idx = 1
elseif start_byte_idx == #prev_line + 1 then
byte_idx = start_byte_idx
- char_idx = str_utfindex(prev_line, offset_encoding) + 1
+ char_idx = str_utfindex(prev_line, position_encoding) + 1
else
byte_idx = start_byte_idx + str_utf_start(prev_line, start_byte_idx)
--- Convert to 0 based for input, and from 0 based for output
- char_idx = vim.str_utfindex(prev_line, offset_encoding, byte_idx - 1) + 1
+ char_idx = vim.str_utfindex(prev_line, position_encoding, byte_idx - 1) + 1
end
-- Return the start difference (shared for new and prev lines)
@@ -174,7 +174,7 @@ end
---@param firstline integer
---@param lastline integer
---@param new_lastline integer
----@param offset_encoding string
+---@param position_encoding string
---@return vim.lsp.sync.Range prev_end_range
---@return vim.lsp.sync.Range curr_end_range
local function compute_end_range(
@@ -184,7 +184,7 @@ local function compute_end_range(
firstline,
lastline,
new_lastline,
- offset_encoding
+ position_encoding
)
-- A special case for the following `firstline == new_lastline` case where lines are deleted.
-- Even if the buffer has become empty, nvim behaves as if it has an empty line with eol.
@@ -193,7 +193,7 @@ local function compute_end_range(
return {
line_idx = lastline - 1,
byte_idx = #prev_line + 1,
- char_idx = str_utfindex(prev_line, offset_encoding) + 1,
+ char_idx = str_utfindex(prev_line, position_encoding) + 1,
}, { line_idx = 1, byte_idx = 1, char_idx = 1 }
end
-- If firstline == new_lastline, the first change occurred on a line that was deleted.
@@ -259,7 +259,7 @@ local function compute_end_range(
prev_end_byte_idx = 1
end
local prev_byte_idx, prev_char_idx =
- align_end_position(prev_line, prev_end_byte_idx, offset_encoding)
+ align_end_position(prev_line, prev_end_byte_idx, position_encoding)
local prev_end_range =
{ line_idx = prev_line_idx, byte_idx = prev_byte_idx, char_idx = prev_char_idx }
@@ -274,7 +274,7 @@ local function compute_end_range(
curr_end_byte_idx = 1
end
local curr_byte_idx, curr_char_idx =
- align_end_position(curr_line, curr_end_byte_idx, offset_encoding)
+ align_end_position(curr_line, curr_end_byte_idx, position_encoding)
curr_end_range =
{ line_idx = curr_line_idx, byte_idx = curr_byte_idx, char_idx = curr_char_idx }
end
@@ -317,7 +317,7 @@ local function extract_text(lines, start_range, end_range, line_ending)
end
end
--- rangelength depends on the offset encoding
+-- rangelength depends on the position encoding
-- bytes for utf-8 (clangd with extension)
-- codepoints for utf-16
-- codeunits for utf-32
@@ -326,10 +326,10 @@ end
---@param lines string[]
---@param start_range vim.lsp.sync.Range
---@param end_range vim.lsp.sync.Range
----@param offset_encoding string
+---@param position_encoding string
---@param line_ending string
---@return integer
-local function compute_range_length(lines, start_range, end_range, offset_encoding, line_ending)
+local function compute_range_length(lines, start_range, end_range, position_encoding, line_ending)
local line_ending_length = #line_ending
-- Single line case
if start_range.line_idx == end_range.line_idx then
@@ -339,7 +339,7 @@ local function compute_range_length(lines, start_range, end_range, offset_encodi
local start_line = lines[start_range.line_idx]
local range_length --- @type integer
if start_line and #start_line > 0 then
- range_length = str_utfindex(start_line, offset_encoding)
+ range_length = str_utfindex(start_line, position_encoding)
- start_range.char_idx
+ 1
+ line_ending_length
@@ -352,7 +352,7 @@ local function compute_range_length(lines, start_range, end_range, offset_encodi
for idx = start_range.line_idx + 1, end_range.line_idx - 1 do
-- Length full line plus newline character
if #lines[idx] > 0 then
- range_length = range_length + str_utfindex(lines[idx], offset_encoding) + #line_ending
+ range_length = range_length + str_utfindex(lines[idx], position_encoding) + #line_ending
else
range_length = range_length + line_ending_length
end
@@ -372,7 +372,7 @@ end
---@param firstline integer line to begin search for first difference
---@param lastline integer line to begin search in old_lines for last difference
---@param new_lastline integer line to begin search in new_lines for last difference
----@param offset_encoding string encoding requested by language server
+---@param position_encoding string encoding requested by language server
---@param line_ending string
---@return lsp.TextDocumentContentChangeEvent : see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
function M.compute_diff(
@@ -381,7 +381,7 @@ function M.compute_diff(
firstline,
lastline,
new_lastline,
- offset_encoding,
+ position_encoding,
line_ending
)
-- Find the start of changes between the previous and current buffer. Common between both.
@@ -393,7 +393,7 @@ function M.compute_diff(
firstline + 1,
lastline + 1,
new_lastline + 1,
- offset_encoding
+ position_encoding
)
-- Find the last position changed in the previous and current buffer.
-- prev_end_range is sent to the server as as the end of the changed range.
@@ -405,7 +405,7 @@ function M.compute_diff(
firstline + 1,
lastline + 1,
new_lastline + 1,
- offset_encoding
+ position_encoding
)
-- Grab the changed text of from start_range to curr_end_range in the current buffer.
@@ -414,7 +414,7 @@ function M.compute_diff(
-- Compute the range of the replaced text. Deprecated but still required for certain language servers
local range_length =
- compute_range_length(prev_lines, start_range, prev_end_range, offset_encoding, line_ending)
+ compute_range_length(prev_lines, start_range, prev_end_range, position_encoding, line_ending)
-- convert to 0 based indexing
local result = {
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 6eab0f3da4..e16a905c44 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -49,7 +49,8 @@ local function get_border_size(opts)
if not border_size[border] then
border_error(border)
end
- return unpack(border_size[border])
+ local r = border_size[border]
+ return r[1], r[2]
end
if 8 % #border ~= 0 then
@@ -192,9 +193,7 @@ local function get_lines(bufnr, rows)
rows = type(rows) == 'table' and rows or { rows }
-- This is needed for bufload and bufloaded
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local function buf_lines()
local lines = {} --- @type table<integer,string>
@@ -277,9 +276,9 @@ end
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
---@param position lsp.Position
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@return integer
-local function get_line_byte_from_position(bufnr, position, offset_encoding)
+local function get_line_byte_from_position(bufnr, position, position_encoding)
-- LSP's line and characters are 0-indexed
-- Vim's line and columns are 1-indexed
local col = position.character
@@ -287,7 +286,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- character
if col > 0 then
local line = get_line(bufnr, position.line) or ''
- return vim.str_byteindex(line, offset_encoding, col, false)
+ return vim.str_byteindex(line, position_encoding, col, false)
end
return col
end
@@ -295,12 +294,12 @@ end
--- Applies a list of text edits to a buffer.
---@param text_edits lsp.TextEdit[]
---@param bufnr integer Buffer id
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
-function M.apply_text_edits(text_edits, bufnr, offset_encoding)
+function M.apply_text_edits(text_edits, bufnr, position_encoding)
validate('text_edits', text_edits, 'table', false)
validate('bufnr', bufnr, 'number', false)
- validate('offset_encoding', offset_encoding, 'string', false)
+ validate('position_encoding', position_encoding, 'string', false)
if not next(text_edits) then
return
@@ -359,9 +358,9 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
-- Convert from LSP style ranges to Neovim style ranges.
local start_row = text_edit.range.start.line
- local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding)
+ local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, position_encoding)
local end_row = text_edit.range['end'].line
- local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding)
+ local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], position_encoding)
local text = vim.split(text_edit.newText, '\n', { plain = true })
local max = api.nvim_buf_line_count(bufnr)
@@ -430,14 +429,14 @@ end
---
---@param text_document_edit lsp.TextDocumentEdit
---@param index? integer: Optional index of the edit, if from a list of edits (or nil, if not from a list)
----@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'
+---@param position_encoding? 'utf-8'|'utf-16'|'utf-32'
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
-function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
+function M.apply_text_document_edit(text_document_edit, index, position_encoding)
local text_document = text_document_edit.textDocument
local bufnr = vim.uri_to_bufnr(text_document.uri)
- if offset_encoding == nil then
+ if position_encoding == nil then
vim.notify_once(
- 'apply_text_document_edit must be called with valid offset encoding',
+ 'apply_text_document_edit must be called with valid position encoding',
vim.log.levels.WARN
)
return
@@ -459,7 +458,7 @@ function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
return
end
- M.apply_text_edits(text_document_edit.edits, bufnr, offset_encoding)
+ M.apply_text_edits(text_document_edit.edits, bufnr, position_encoding)
end
local function path_components(path)
@@ -619,12 +618,12 @@ end
--- Applies a `WorkspaceEdit`.
---
---@param workspace_edit lsp.WorkspaceEdit
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32' (required)
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32' (required)
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
-function M.apply_workspace_edit(workspace_edit, offset_encoding)
- if offset_encoding == nil then
+function M.apply_workspace_edit(workspace_edit, position_encoding)
+ if position_encoding == nil then
vim.notify_once(
- 'apply_workspace_edit must be called with valid offset encoding',
+ 'apply_workspace_edit must be called with valid position encoding',
vim.log.levels.WARN
)
return
@@ -641,7 +640,7 @@ function M.apply_workspace_edit(workspace_edit, offset_encoding)
elseif change.kind then --- @diagnostic disable-line:undefined-field
error(string.format('Unsupported change: %q', vim.inspect(change)))
else
- M.apply_text_document_edit(change, idx, offset_encoding)
+ M.apply_text_document_edit(change, idx, position_encoding)
end
end
return
@@ -654,7 +653,7 @@ function M.apply_workspace_edit(workspace_edit, offset_encoding)
for uri, changes in pairs(all_changes) do
local bufnr = vim.uri_to_bufnr(uri)
- M.apply_text_edits(changes, bufnr, offset_encoding)
+ M.apply_text_edits(changes, bufnr, position_encoding)
end
end
@@ -877,15 +876,16 @@ function M.make_floating_popup_options(width, height, opts)
return {
anchor = anchor,
+ row = row + (opts.offset_y or 0),
col = col + (opts.offset_x or 0),
height = height,
focusable = opts.focusable,
- relative = opts.relative == 'mouse' and 'mouse' or 'cursor',
- row = row + (opts.offset_y or 0),
+ relative = (opts.relative == 'mouse' or opts.relative == 'editor') and opts.relative
+ or 'cursor',
style = 'minimal',
width = width,
border = opts.border or default_border,
- zindex = opts.zindex or 50,
+ zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1,
title = title,
title_pos = title_pos,
}
@@ -904,17 +904,20 @@ end
--- Shows document and optionally jumps to the location.
---
---@param location lsp.Location|lsp.LocationLink
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'?
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'?
---@param opts? vim.lsp.util.show_document.Opts
---@return boolean `true` if succeeded
-function M.show_document(location, offset_encoding, opts)
+function M.show_document(location, position_encoding, opts)
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then
return false
end
- if offset_encoding == nil then
- vim.notify_once('show_document must be called with valid offset encoding', vim.log.levels.WARN)
+ if position_encoding == nil then
+ vim.notify_once(
+ 'show_document must be called with valid position encoding',
+ vim.log.levels.WARN
+ )
return false
end
local bufnr = vim.uri_to_bufnr(uri)
@@ -946,7 +949,7 @@ function M.show_document(location, offset_encoding, opts)
if range then
-- Jump to new location (adjusting for encoding of characters)
local row = range.start.line
- local col = get_line_byte_from_position(bufnr, range.start, offset_encoding)
+ local col = get_line_byte_from_position(bufnr, range.start, position_encoding)
api.nvim_win_set_cursor(win, { row + 1, col })
vim._with({ win = win }, function()
-- Open folds under the cursor
@@ -961,12 +964,12 @@ end
---
---@deprecated use `vim.lsp.util.show_document` with `{focus=true}` instead
---@param location lsp.Location|lsp.LocationLink
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'?
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'?
---@param reuse_win boolean? Jump to existing window if buffer is already open.
---@return boolean `true` if the jump succeeded
-function M.jump_to_location(location, offset_encoding, reuse_win)
+function M.jump_to_location(location, position_encoding, reuse_win)
vim.deprecate('vim.lsp.util.jump_to_location', nil, '0.12')
- return M.show_document(location, offset_encoding, { reuse_win = reuse_win, focus = true })
+ return M.show_document(location, position_encoding, { reuse_win = reuse_win, focus = true })
end
--- Previews a location in a floating window
@@ -1355,7 +1358,7 @@ end
---@param bufnrs table list of buffers where the preview window will remain visible
---@see autocmd-events
local function close_preview_autocmd(events, winnr, bufnrs)
- local augroup = api.nvim_create_augroup('preview_window_' .. winnr, {
+ local augroup = api.nvim_create_augroup('nvim.preview_window_' .. winnr, {
clear = true,
})
@@ -1430,7 +1433,7 @@ function M._make_floating_popup_size(contents, opts)
if vim.tbl_isempty(line_widths) then
for _, line in ipairs(contents) do
local line_width = vim.fn.strdisplaywidth(line:gsub('%z', '\n'))
- height = height + math.ceil(line_width / wrap_at)
+ height = height + math.max(1, math.ceil(line_width / wrap_at))
end
else
for i = 1, #contents do
@@ -1493,7 +1496,7 @@ end
--- @field title_pos? 'left'|'center'|'right'
---
--- (default: `'cursor'`)
---- @field relative? 'mouse'|'cursor'
+--- @field relative? 'mouse'|'cursor'|'editor'
---
--- - "auto": place window based on which side of the cursor has more lines
--- - "above": place the window above the cursor unless there are not enough lines
@@ -1566,8 +1569,6 @@ function M.open_floating_preview(contents, syntax, opts)
if do_stylize then
local width = M._make_floating_popup_size(contents, opts)
contents = M._normalize_markdown(contents, { width = width })
- vim.bo[floating_bufnr].filetype = 'markdown'
- vim.treesitter.start(floating_bufnr)
else
-- Clean up input: trim empty lines
contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true })
@@ -1617,9 +1618,22 @@ function M.open_floating_preview(contents, syntax, opts)
api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr)
end
- if do_stylize then
- vim.wo[floating_winnr].conceallevel = 2
+ local augroup_name = ('nvim.closing_floating_preview_%d'):format(floating_winnr)
+ local ok =
+ pcall(api.nvim_get_autocmds, { group = augroup_name, pattern = tostring(floating_winnr) })
+ if not ok then
+ api.nvim_create_autocmd('WinClosed', {
+ group = api.nvim_create_augroup(augroup_name, {}),
+ pattern = tostring(floating_winnr),
+ callback = function()
+ if api.nvim_buf_is_valid(bufnr) then
+ vim.b[bufnr].lsp_floating_preview = nil
+ end
+ api.nvim_del_augroup_by_name(augroup_name)
+ end,
+ })
end
+
vim.wo[floating_winnr].foldenable = false -- Disable folding.
vim.wo[floating_winnr].wrap = opts.wrap -- Soft wrapping.
vim.wo[floating_winnr].breakindent = true -- Slightly better list presentation.
@@ -1628,11 +1642,17 @@ function M.open_floating_preview(contents, syntax, opts)
vim.bo[floating_bufnr].modifiable = false
vim.bo[floating_bufnr].bufhidden = 'wipe'
+ if do_stylize then
+ vim.wo[floating_winnr].conceallevel = 2
+ vim.bo[floating_bufnr].filetype = 'markdown'
+ vim.treesitter.start(floating_bufnr)
+ end
+
return floating_bufnr, floating_winnr
end
do --[[ References ]]
- local reference_ns = api.nvim_create_namespace('vim_lsp_references')
+ local reference_ns = api.nvim_create_namespace('nvim.lsp.references')
--- Removes document highlights from a buffer.
---
@@ -1645,18 +1665,18 @@ do --[[ References ]]
---
---@param bufnr integer Buffer id
---@param references lsp.DocumentHighlight[] objects to highlight
- ---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
+ ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
- function M.buf_highlight_references(bufnr, references, offset_encoding)
+ function M.buf_highlight_references(bufnr, references, position_encoding)
validate('bufnr', bufnr, 'number', true)
- validate('offset_encoding', offset_encoding, 'string', false)
+ validate('position_encoding', position_encoding, 'string', false)
for _, reference in ipairs(references) do
local range = reference.range
local start_line = range.start.line
local end_line = range['end'].line
- local start_idx = get_line_byte_from_position(bufnr, range.start, offset_encoding)
- local end_idx = get_line_byte_from_position(bufnr, range['end'], offset_encoding)
+ local start_idx = get_line_byte_from_position(bufnr, range.start, position_encoding)
+ local end_idx = get_line_byte_from_position(bufnr, range['end'], position_encoding)
local document_highlight_kind = {
[protocol.DocumentHighlightKind.Text] = 'LspReferenceText',
@@ -1690,16 +1710,16 @@ end)
--- |setloclist()|.
---
---@param locations lsp.Location[]|lsp.LocationLink[]
----@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'
+---@param position_encoding? 'utf-8'|'utf-16'|'utf-32'
--- default to first client of buffer
---@return vim.quickfix.entry[] # See |setqflist()| for the format
-function M.locations_to_items(locations, offset_encoding)
- if offset_encoding == nil then
+function M.locations_to_items(locations, position_encoding)
+ if position_encoding == nil then
vim.notify_once(
- 'locations_to_items must be called with valid offset encoding',
+ 'locations_to_items must be called with valid position encoding',
vim.log.levels.WARN
)
- offset_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding
+ position_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding
end
local items = {} --- @type vim.quickfix.entry[]
@@ -1736,8 +1756,8 @@ function M.locations_to_items(locations, offset_encoding)
local end_row = end_pos.line
local line = lines[row] or ''
local end_line = lines[end_row] or ''
- local col = vim.str_byteindex(line, offset_encoding, pos.character, false)
- local end_col = vim.str_byteindex(end_line, offset_encoding, end_pos.character, false)
+ local col = vim.str_byteindex(line, position_encoding, pos.character, false)
+ local end_col = vim.str_byteindex(end_line, position_encoding, end_pos.character, false)
items[#items + 1] = {
filename = filename,
@@ -1848,19 +1868,18 @@ function M.try_trim_markdown_code_blocks(lines)
end
---@param window integer?: window handle or 0 for current, defaults to current
----@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window`
-local function make_position_param(window, offset_encoding)
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
+local function make_position_param(window, position_encoding)
window = window or 0
local buf = api.nvim_win_get_buf(window)
local row, col = unpack(api.nvim_win_get_cursor(window))
- offset_encoding = offset_encoding or M._get_offset_encoding(buf)
row = row - 1
local line = api.nvim_buf_get_lines(buf, row, row + 1, true)[1]
if not line then
return { line = 0, character = 0 }
end
- col = vim.str_utfindex(line, offset_encoding, col, false)
+ col = vim.str_utfindex(line, position_encoding, col, false)
return { line = row, character = col }
end
@@ -1868,20 +1887,28 @@ end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
---@param window integer?: window handle or 0 for current, defaults to current
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window`
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@return lsp.TextDocumentPositionParams
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
-function M.make_position_params(window, offset_encoding)
+function M.make_position_params(window, position_encoding)
window = window or 0
local buf = api.nvim_win_get_buf(window)
- offset_encoding = offset_encoding or M._get_offset_encoding(buf)
+ if position_encoding == nil then
+ vim.notify_once(
+ 'position_encoding param is required in vim.lsp.util.make_position_params. Defaulting to position encoding of the first client.',
+ vim.log.levels.WARN
+ )
+ --- @diagnostic disable-next-line: deprecated
+ position_encoding = M._get_offset_encoding(buf)
+ end
return {
textDocument = M.make_text_document_params(buf),
- position = make_position_param(window, offset_encoding),
+ position = make_position_param(window, position_encoding),
}
end
--- Utility function for getting the encoding of the first LSP client on the given buffer.
+---@deprecated
---@param bufnr integer buffer handle or 0 for current, defaults to current
---@return string encoding first client if there is one, nil otherwise
function M._get_offset_encoding(bufnr)
@@ -1904,7 +1931,7 @@ function M._get_offset_encoding(bufnr)
offset_encoding = this_offset_encoding
elseif offset_encoding ~= this_offset_encoding then
vim.notify_once(
- 'warning: multiple different client offset_encodings detected for buffer, this is not supported yet',
+ 'warning: multiple different client offset_encodings detected for buffer, vim.lsp.util._get_offset_encoding() uses the offset_encoding from the first client',
vim.log.levels.WARN
)
end
@@ -1919,13 +1946,19 @@ end
--- `textDocument/rangeFormatting`.
---
---@param window integer? window handle or 0 for current, defaults to current
----@param offset_encoding "utf-8"|"utf-16"|"utf-32"? defaults to `offset_encoding` of first client of buffer of `window`
----@return table { textDocument = { uri = `current_file_uri` }, range = { start =
----`current_position`, end = `current_position` } }
-function M.make_range_params(window, offset_encoding)
+---@param position_encoding "utf-8"|"utf-16"|"utf-32"
+---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range }
+function M.make_range_params(window, position_encoding)
local buf = api.nvim_win_get_buf(window or 0)
- offset_encoding = offset_encoding or M._get_offset_encoding(buf)
- local position = make_position_param(window, offset_encoding)
+ if position_encoding == nil then
+ vim.notify_once(
+ 'position_encoding param is required in vim.lsp.util.make_range_params. Defaulting to position encoding of the first client.',
+ vim.log.levels.WARN
+ )
+ --- @diagnostic disable-next-line: deprecated
+ position_encoding = M._get_offset_encoding(buf)
+ end
+ local position = make_position_param(window, position_encoding)
return {
textDocument = M.make_text_document_params(buf),
range = { start = position, ['end'] = position },
@@ -1940,15 +1973,21 @@ end
---@param end_pos [integer,integer]? {row,col} mark-indexed position.
--- Defaults to the end of the last visual selection.
---@param bufnr integer? buffer handle or 0 for current, defaults to current
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of `bufnr`
----@return table { textDocument = { uri = `current_file_uri` }, range = { start =
----`start_position`, end = `end_position` } }
-function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
+---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range }
+function M.make_given_range_params(start_pos, end_pos, bufnr, position_encoding)
validate('start_pos', start_pos, 'table', true)
validate('end_pos', end_pos, 'table', true)
- validate('offset_encoding', offset_encoding, 'string', true)
- bufnr = bufnr or api.nvim_get_current_buf()
- offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
+ validate('position_encoding', position_encoding, 'string', true)
+ bufnr = vim._resolve_bufnr(bufnr)
+ if position_encoding == nil then
+ vim.notify_once(
+ 'position_encoding param is required in vim.lsp.util.make_given_range_params. Defaulting to position encoding of the first client.',
+ vim.log.levels.WARN
+ )
+ --- @diagnostic disable-next-line: deprecated
+ position_encoding = M._get_offset_encoding(bufnr)
+ end
--- @type [integer, integer]
local A = { unpack(start_pos or api.nvim_buf_get_mark(bufnr, '<')) }
--- @type [integer, integer]
@@ -1956,12 +1995,12 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
-- convert to 0-index
A[1] = A[1] - 1
B[1] = B[1] - 1
- -- account for offset_encoding.
+ -- account for position_encoding.
if A[2] > 0 then
- A[2] = M.character_offset(bufnr, A[1], A[2], offset_encoding)
+ A[2] = M.character_offset(bufnr, A[1], A[2], position_encoding)
end
if B[2] > 0 then
- B[2] = M.character_offset(bufnr, B[1], B[2], offset_encoding)
+ B[2] = M.character_offset(bufnr, B[1], B[2], position_encoding)
end
-- we need to offset the end character position otherwise we loose the last
-- character of the selection, as LSP end position is exclusive
@@ -2068,9 +2107,9 @@ end
---@param bufnr integer
---@param start_line integer
---@param end_line integer
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@return lsp.Range
-local function make_line_range_params(bufnr, start_line, end_line, offset_encoding)
+local function make_line_range_params(bufnr, start_line, end_line, position_encoding)
local last_line = api.nvim_buf_line_count(bufnr) - 1
---@type lsp.Position
@@ -2079,7 +2118,12 @@ local function make_line_range_params(bufnr, start_line, end_line, offset_encodi
if end_line == last_line and not vim.bo[bufnr].endofline then
end_pos = {
line = end_line,
- character = M.character_offset(bufnr, end_line, #get_line(bufnr, end_line), offset_encoding),
+ character = M.character_offset(
+ bufnr,
+ end_line,
+ #get_line(bufnr, end_line),
+ position_encoding
+ ),
}
else
end_pos = { line = end_line + 1, character = 0 }
@@ -2103,10 +2147,7 @@ end
---@param opts? vim.lsp.util._refresh.Opts Options table
function M._refresh(method, opts)
opts = opts or {}
- local bufnr = opts.bufnr
- if bufnr == nil or bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ local bufnr = vim._resolve_bufnr(opts.bufnr)
local clients = vim.lsp.get_clients({ bufnr = bufnr, method = method, id = opts.client_id })
@@ -2122,7 +2163,12 @@ function M._refresh(method, opts)
local first = vim.fn.line('w0', window)
local last = vim.fn.line('w$', window)
for _, client in ipairs(clients) do
- client.request(method, {
+ for rid, req in pairs(client.requests) do
+ if req.method == method and req.type == 'pending' and req.bufnr == bufnr then
+ client:cancel_request(rid)
+ end
+ end
+ client:request(method, {
textDocument = textDocument,
range = make_line_range_params(bufnr, first - 1, last - 1, client.offset_encoding),
}, nil, bufnr)
@@ -2131,7 +2177,7 @@ function M._refresh(method, opts)
end
else
for _, client in ipairs(clients) do
- client.request(method, {
+ client:request(method, {
textDocument = textDocument,
range = make_line_range_params(
bufnr,
diff --git a/runtime/lua/vim/provider/health.lua b/runtime/lua/vim/provider/health.lua
index 5ecb00f49b..fa01951b02 100644
--- a/runtime/lua/vim/provider/health.lua
+++ b/runtime/lua/vim/provider/health.lua
@@ -10,22 +10,20 @@ end
-- Attempts to construct a shell command from an args list.
-- Only for display, to help users debug a failed command.
+--- @param cmd string|string[]
local function shellify(cmd)
if type(cmd) ~= 'table' then
return cmd
end
- local escaped = {}
+ local escaped = {} --- @type string[]
for i, v in ipairs(cmd) do
- if v:match('[^A-Za-z_/.-]') then
- escaped[i] = vim.fn.shellescape(v)
- else
- escaped[i] = v
- end
+ escaped[i] = v:match('[^A-Za-z_/.-]') and vim.fn.shellescape(v) or v
end
return table.concat(escaped, ' ')
end
-- Handler for s:system() function.
+--- @param self {output: string, stderr: string, add_stderr_to_output: boolean}
local function system_handler(self, _, data, event)
if event == 'stderr' then
if self.add_stderr_to_output then
@@ -38,7 +36,7 @@ local function system_handler(self, _, data, event)
end
end
---- @param cmd table List of command arguments to execute
+--- @param cmd string|string[] List of command arguments to execute
--- @param args? table Optional arguments:
--- - stdin (string): Data to write to the job's stdin
--- - stderr (boolean): Append stderr to stdout
@@ -47,8 +45,8 @@ end
local function system(cmd, args)
args = args or {}
local stdin = args.stdin or ''
- local stderr = vim.F.if_nil(args.stderr, false)
- local ignore_error = vim.F.if_nil(args.ignore_error, false)
+ local stderr = args.stderr or false
+ local ignore_error = args.ignore_error or false
local shell_error_code = 0
local opts = {
@@ -530,13 +528,14 @@ local function version_info(python)
if rc ~= 0 or nvim_version == '' then
nvim_version = 'unable to find pynvim module version'
local base = vim.fs.basename(nvim_path)
- local metas = vim.fn.glob(base .. '-*/METADATA', true, 1)
- vim.list_extend(metas, vim.fn.glob(base .. '-*/PKG-INFO', true, 1))
- vim.list_extend(metas, vim.fn.glob(base .. '.egg-info/PKG-INFO', true, 1))
+ local metas = vim.fn.glob(base .. '-*/METADATA', true, true)
+ vim.list_extend(metas, vim.fn.glob(base .. '-*/PKG-INFO', true, true))
+ vim.list_extend(metas, vim.fn.glob(base .. '.egg-info/PKG-INFO', true, true))
metas = table.sort(metas, compare)
if metas and next(metas) ~= nil then
for line in io.lines(metas[1]) do
+ --- @cast line string
local version = line:match('^Version: (%S+)')
if version then
nvim_version = version
@@ -762,6 +761,7 @@ local function python()
-- subshells launched from Nvim.
local bin_dir = iswin and 'Scripts' or 'bin'
local venv_bins = vim.fn.glob(string.format('%s/%s/python*', virtual_env, bin_dir), true, true)
+ --- @param v string
venv_bins = vim.tbl_filter(function(v)
-- XXX: Remove irrelevant executables found in bin/.
return not v:match('python.*%-config')
@@ -809,6 +809,7 @@ local function python()
msg,
bin_dir,
table.concat(
+ --- @param v string
vim.tbl_map(function(v)
return vim.fs.basename(v)
end, venv_bins),
@@ -817,12 +818,15 @@ local function python()
)
end
local conj = '\nBut '
+ local msgs = {} --- @type string[]
for _, err in ipairs(errors) do
- msg = msg .. conj .. err
+ msgs[#msgs + 1] = msg
+ msgs[#msgs + 1] = conj
+ msgs[#msgs + 1] = err
conj = '\nAnd '
end
- msg = msg .. '\nSo invoking Python may lead to unexpected results.'
- health.warn(msg, vim.tbl_keys(hints))
+ msgs[#msgs + 1] = '\nSo invoking Python may lead to unexpected results.'
+ health.warn(table.concat(msgs), vim.tbl_keys(hints))
else
health.info(msg)
health.info(
diff --git a/runtime/lua/vim/re.lua b/runtime/lua/vim/re.lua
index 114f74eb80..e0a36703e3 100644
--- a/runtime/lua/vim/re.lua
+++ b/runtime/lua/vim/re.lua
@@ -1,3 +1,4 @@
+--- @diagnostic disable: no-unknown
--
-- Copyright 2007-2023, Lua.org & PUC-Rio (see 'lpeg.html' for license)
-- written by Roberto Ierusalimschy
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index 4f2373b182..f19533f474 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -7,8 +7,7 @@
-- so this wouldn't be a separate case to consider)
---@nodoc
----@diagnostic disable-next-line: lowercase-global
-vim = vim or {}
+_G.vim = _G.vim or {}
---@generic T
---@param orig T
@@ -737,6 +736,51 @@ function vim.list_slice(list, start, finish)
return new_list
end
+--- Efficiently insert items into the middle of a list.
+---
+--- Calling table.insert() in a loop will re-index the tail of the table on
+--- every iteration, instead this function will re-index the table exactly
+--- once.
+---
+--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
+---
+---@param t any[]
+---@param first integer
+---@param last integer
+---@param v any
+function vim._list_insert(t, first, last, v)
+ local n = #t
+
+ -- Shift table forward
+ for i = n - first, 0, -1 do
+ t[last + 1 + i] = t[first + i]
+ end
+
+ -- Fill in new values
+ for i = first, last do
+ t[i] = v
+ end
+end
+
+--- Efficiently remove items from middle of a list.
+---
+--- Calling table.remove() in a loop will re-index the tail of the table on
+--- every iteration, instead this function will re-index the table exactly
+--- once.
+---
+--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
+---
+---@param t any[]
+---@param first integer
+---@param last integer
+function vim._list_remove(t, first, last)
+ local n = #t
+ for i = 0, n - first do
+ t[first + i] = t[last + 1 + i]
+ t[last + 1 + i] = nil
+ end
+end
+
--- Trim whitespace (Lua pattern "%s") from both sides of a string.
---
---@see |lua-patterns|
@@ -914,7 +958,7 @@ do
--- function vim.startswith(s, prefix)
--- vim.validate('s', s, 'string')
--- vim.validate('prefix', prefix, 'string')
- --- ...
+ --- -- ...
--- end
--- ```
---
@@ -934,7 +978,7 @@ do
--- age={age, 'number'},
--- hobbies={hobbies, 'table'},
--- }
- --- ...
+ --- -- ...
--- end
--- ```
---
@@ -968,7 +1012,7 @@ do
--- best performance.
---
--- @param name string Argument name
- --- @param value string Argument value
+ --- @param value any Argument value
--- @param validator vim.validate.Validator
--- - (`string|string[]`): Any value that can be returned from |lua-type()| in addition to
--- `'callable'`: `'boolean'`, `'callable'`, `'function'`, `'nil'`, `'number'`, `'string'`, `'table'`,
@@ -1354,4 +1398,24 @@ function vim._with(context, f)
return vim._with_c(context, callback)
end
+--- @param bufnr? integer
+--- @return integer
+function vim._resolve_bufnr(bufnr)
+ if bufnr == nil or bufnr == 0 then
+ return vim.api.nvim_get_current_buf()
+ end
+ vim.validate('bufnr', bufnr, 'number')
+ return bufnr
+end
+
+--- @generic T
+--- @param x elem_or_list<T>?
+--- @return T[]
+function vim._ensure_list(x)
+ if type(x) == 'table' then
+ return x
+ end
+ return { x }
+end
+
return vim
diff --git a/runtime/lua/vim/snippet.lua b/runtime/lua/vim/snippet.lua
index af7e3c6d33..bfd439181e 100644
--- a/runtime/lua/vim/snippet.lua
+++ b/runtime/lua/vim/snippet.lua
@@ -1,6 +1,6 @@
local G = vim.lsp._snippet_grammar
-local snippet_group = vim.api.nvim_create_augroup('vim/snippet', {})
-local snippet_ns = vim.api.nvim_create_namespace('vim/snippet')
+local snippet_group = vim.api.nvim_create_augroup('nvim.snippet', {})
+local snippet_ns = vim.api.nvim_create_namespace('nvim.snippet')
local hl_group = 'SnippetTabstop'
local jump_forward_key = '<tab>'
local jump_backward_key = '<s-tab>'
diff --git a/runtime/lua/vim/text.lua b/runtime/lua/vim/text.lua
index d45c8021c6..f910ab3a1d 100644
--- a/runtime/lua/vim/text.lua
+++ b/runtime/lua/vim/text.lua
@@ -2,6 +2,18 @@
local M = {}
+local alphabet = '0123456789ABCDEF'
+local atoi = {} ---@type table<string, integer>
+local itoa = {} ---@type table<integer, string>
+do
+ for i = 1, #alphabet do
+ local char = alphabet:sub(i, i)
+ itoa[i - 1] = char
+ atoi[char] = i - 1
+ atoi[char:lower()] = i - 1
+ end
+end
+
--- Hex encode a string.
---
--- @param str string String to encode
@@ -9,7 +21,9 @@ local M = {}
function M.hexencode(str)
local enc = {} ---@type string[]
for i = 1, #str do
- enc[i] = string.format('%02X', str:byte(i, i + 1))
+ local byte = str:byte(i)
+ enc[2 * i - 1] = itoa[math.floor(byte / 16)]
+ enc[2 * i] = itoa[byte % 16]
end
return table.concat(enc)
end
@@ -26,8 +40,12 @@ function M.hexdecode(enc)
local str = {} ---@type string[]
for i = 1, #enc, 2 do
- local n = assert(tonumber(enc:sub(i, i + 1), 16))
- str[#str + 1] = string.char(n)
+ local u = atoi[enc:sub(i, i)]
+ local l = atoi[enc:sub(i + 1, i + 1)]
+ if not u or not l then
+ return nil, 'string must contain only hex characters'
+ end
+ str[(i + 1) / 2] = string.char(u * 16 + l)
end
return table.concat(str), nil
end
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
index dca89f413c..10638e10d8 100644
--- a/runtime/lua/vim/treesitter.lua
+++ b/runtime/lua/vim/treesitter.lua
@@ -32,9 +32,7 @@ M.minimum_language_version = vim._ts_get_minimum_language_version()
---
---@return vim.treesitter.LanguageTree object to use for parsing
function M._create_parser(bufnr, lang, opts)
- if bufnr == 0 then
- bufnr = vim.api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
vim.fn.bufload(bufnr)
@@ -63,8 +61,6 @@ function M._create_parser(bufnr, lang, opts)
{ on_bytes = bytes_cb, on_detach = detach_cb, on_reload = reload_cb, preview = true }
)
- self:parse()
-
return self
end
@@ -90,9 +86,7 @@ function M.get_parser(bufnr, lang, opts)
opts = opts or {}
local should_error = opts.error == nil or opts.error
- if bufnr == nil or bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
if not valid_lang(lang) then
lang = M.language.get_lang(vim.bo[bufnr].filetype)
@@ -155,7 +149,7 @@ end
--- Returns the node's range or an unpacked range table
---
----@param node_or_range (TSNode | table) Node or table of positions
+---@param node_or_range TSNode|Range4 Node or table of positions
---
---@return integer start_row
---@return integer start_col
@@ -163,7 +157,8 @@ end
---@return integer end_col
function M.get_node_range(node_or_range)
if type(node_or_range) == 'table' then
- return unpack(node_or_range)
+ --- @cast node_or_range -TSNode LuaLS bug
+ return M._range.unpack4(node_or_range)
else
return node_or_range:range(false)
end
@@ -244,23 +239,24 @@ function M.node_contains(node, range)
-- allow a table so nodes can be mocked
vim.validate('node', node, { 'userdata', 'table' })
vim.validate('range', range, M._range.validate, 'integer list with 4 or 6 elements')
- return M._range.contains({ node:range() }, range)
+ --- @diagnostic disable-next-line: missing-fields LuaLS bug
+ local nrange = { node:range() } --- @type Range4
+ return M._range.contains(nrange, range)
end
--- Returns a list of highlight captures at the given position
---
---- Each capture is represented by a table containing the capture name as a string as
---- well as a table of metadata (`priority`, `conceal`, ...; empty if none are defined).
+--- Each capture is represented by a table containing the capture name as a string, the capture's
+--- language, a table of metadata (`priority`, `conceal`, ...; empty if none are defined), and the
+--- id of the capture.
---
---@param bufnr integer Buffer number (0 for current buffer)
---@param row integer Position row
---@param col integer Position column
---
----@return {capture: string, lang: string, metadata: vim.treesitter.query.TSMetadata}[]
+---@return {capture: string, lang: string, metadata: vim.treesitter.query.TSMetadata, id: integer}[]
function M.get_captures_at_pos(bufnr, row, col)
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local buf_highlighter = M.highlighter.active[bufnr]
if not buf_highlighter then
@@ -291,12 +287,15 @@ function M.get_captures_at_pos(bufnr, row, col)
local iter = q:query():iter_captures(root, buf_highlighter.bufnr, row, row + 1)
- for capture, node, metadata in iter do
+ for id, node, metadata in iter do
if M.is_in_node_range(node, row, col) then
---@diagnostic disable-next-line: invisible
- local c = q._query.captures[capture] -- name of the capture in the query
- if c ~= nil then
- table.insert(matches, { capture = c, metadata = metadata, lang = tree:lang() })
+ local capture = q._query.captures[id] -- name of the capture in the query
+ if capture ~= nil then
+ table.insert(
+ matches,
+ { capture = capture, metadata = metadata, lang = tree:lang(), id = id }
+ )
end
end
end
@@ -361,11 +360,7 @@ end
function M.get_node(opts)
opts = opts or {}
- local bufnr = opts.bufnr
-
- if not bufnr or bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ local bufnr = vim._resolve_bufnr(opts.bufnr)
local row, col --- @type integer, integer
if opts.pos then
@@ -403,6 +398,8 @@ end
--- Note: By default, disables regex syntax highlighting, which may be required for some plugins.
--- In this case, add `vim.bo.syntax = 'on'` after the call to `start`.
---
+--- Note: By default, the highlighter parses code asynchronously, using a segment time of 3ms.
+---
--- Example:
---
--- ```lua
@@ -414,10 +411,10 @@ end
--- })
--- ```
---
----@param bufnr (integer|nil) Buffer to be highlighted (default: current buffer)
----@param lang (string|nil) Language of the parser (default: from buffer filetype)
+---@param bufnr integer? Buffer to be highlighted (default: current buffer)
+---@param lang string? Language of the parser (default: from buffer filetype)
function M.start(bufnr, lang)
- bufnr = bufnr or api.nvim_get_current_buf()
+ bufnr = vim._resolve_bufnr(bufnr)
local parser = assert(M.get_parser(bufnr, lang, { error = false }))
M.highlighter.new(parser)
end
@@ -426,7 +423,7 @@ end
---
---@param bufnr (integer|nil) Buffer to stop highlighting (default: current buffer)
function M.stop(bufnr)
- bufnr = (bufnr and bufnr ~= 0) and bufnr or api.nvim_get_current_buf()
+ bufnr = vim._resolve_bufnr(bufnr)
if M.highlighter.active[bufnr] then
M.highlighter.active[bufnr]:destroy()
diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua
index 7237d2e7d4..38318347a7 100644
--- a/runtime/lua/vim/treesitter/_fold.lua
+++ b/runtime/lua/vim/treesitter/_fold.lua
@@ -19,76 +19,36 @@ local api = vim.api
---The range on which to evaluate foldexpr.
---When in insert mode, the evaluation is deferred to InsertLeave.
---@field foldupdate_range? Range2
+---
+---The treesitter parser associated with this buffer.
+---@field parser? vim.treesitter.LanguageTree
local FoldInfo = {}
FoldInfo.__index = FoldInfo
---@private
-function FoldInfo.new()
+---@param bufnr integer
+function FoldInfo.new(bufnr)
return setmetatable({
levels0 = {},
levels = {},
+ parser = ts.get_parser(bufnr, nil, { error = false }),
}, FoldInfo)
end
---- Efficiently remove items from middle of a list a list.
----
---- Calling table.remove() in a loop will re-index the tail of the table on
---- every iteration, instead this function will re-index the table exactly
---- once.
----
---- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
----
----@param t any[]
----@param first integer
----@param last integer
-local function list_remove(t, first, last)
- local n = #t
- for i = 0, n - first do
- t[first + i] = t[last + 1 + i]
- t[last + 1 + i] = nil
- end
-end
-
---@package
---@param srow integer
---@param erow integer 0-indexed, exclusive
function FoldInfo:remove_range(srow, erow)
- list_remove(self.levels, srow + 1, erow)
- list_remove(self.levels0, srow + 1, erow)
-end
-
---- Efficiently insert items into the middle of a list.
----
---- Calling table.insert() in a loop will re-index the tail of the table on
---- every iteration, instead this function will re-index the table exactly
---- once.
----
---- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
----
----@param t any[]
----@param first integer
----@param last integer
----@param v any
-local function list_insert(t, first, last, v)
- local n = #t
-
- -- Shift table forward
- for i = n - first, 0, -1 do
- t[last + 1 + i] = t[first + i]
- end
-
- -- Fill in new values
- for i = first, last do
- t[i] = v
- end
+ vim._list_remove(self.levels, srow + 1, erow)
+ vim._list_remove(self.levels0, srow + 1, erow)
end
---@package
---@param srow integer
---@param erow integer 0-indexed, exclusive
function FoldInfo:add_range(srow, erow)
- list_insert(self.levels, srow + 1, erow, -1)
- list_insert(self.levels0, srow + 1, erow, -1)
+ vim._list_insert(self.levels, srow + 1, erow, -1)
+ vim._list_insert(self.levels0, srow + 1, erow, -1)
end
---@param range Range2
@@ -109,111 +69,122 @@ end
---@param info TS.FoldInfo
---@param srow integer?
---@param erow integer? 0-indexed, exclusive
----@param parse_injections? boolean
-local function compute_folds_levels(bufnr, info, srow, erow, parse_injections)
+---@param callback function?
+local function compute_folds_levels(bufnr, info, srow, erow, callback)
srow = srow or 0
erow = erow or api.nvim_buf_line_count(bufnr)
- local parser = assert(ts.get_parser(bufnr, nil, { error = false }))
-
- parser:parse(parse_injections and { srow, erow } or nil)
-
- local enter_counts = {} ---@type table<integer, integer>
- local leave_counts = {} ---@type table<integer, integer>
- local prev_start = -1
- local prev_stop = -1
+ local parser = info.parser
+ if not parser then
+ return
+ end
- parser:for_each_tree(function(tree, ltree)
- local query = ts.query.get(ltree:lang(), 'folds')
- if not query then
+ parser:parse(nil, function(_, trees)
+ if not trees then
return
end
- -- Collect folds starting from srow - 1, because we should first subtract the folds that end at
- -- srow - 1 from the level of srow - 1 to get accurate level of srow.
- for _, match, metadata in query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow) do
- for id, nodes in pairs(match) do
- if query.captures[id] == 'fold' then
- local range = ts.get_range(nodes[1], bufnr, metadata[id])
- local start, _, stop, stop_col = Range.unpack4(range)
-
- if #nodes > 1 then
- -- assumes nodes are ordered by range
- local end_range = ts.get_range(nodes[#nodes], bufnr, metadata[id])
- local _, _, end_stop, end_stop_col = Range.unpack4(end_range)
- stop = end_stop
- stop_col = end_stop_col
- end
+ local enter_counts = {} ---@type table<integer, integer>
+ local leave_counts = {} ---@type table<integer, integer>
+ local prev_start = -1
+ local prev_stop = -1
- if stop_col == 0 then
- stop = stop - 1
- end
+ parser:for_each_tree(function(tree, ltree)
+ local query = ts.query.get(ltree:lang(), 'folds')
+ if not query then
+ return
+ end
- local fold_length = stop - start + 1
-
- -- Fold only multiline nodes that are not exactly the same as previously met folds
- -- Checking against just the previously found fold is sufficient if nodes
- -- are returned in preorder or postorder when traversing tree
- if
- fold_length > vim.wo.foldminlines and not (start == prev_start and stop == prev_stop)
- then
- enter_counts[start + 1] = (enter_counts[start + 1] or 0) + 1
- leave_counts[stop + 1] = (leave_counts[stop + 1] or 0) + 1
- prev_start = start
- prev_stop = stop
+ -- Collect folds starting from srow - 1, because we should first subtract the folds that end at
+ -- srow - 1 from the level of srow - 1 to get accurate level of srow.
+ for _, match, metadata in query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow) do
+ for id, nodes in pairs(match) do
+ if query.captures[id] == 'fold' then
+ local range = ts.get_range(nodes[1], bufnr, metadata[id])
+ local start, _, stop, stop_col = Range.unpack4(range)
+
+ if #nodes > 1 then
+ -- assumes nodes are ordered by range
+ local end_range = ts.get_range(nodes[#nodes], bufnr, metadata[id])
+ local _, _, end_stop, end_stop_col = Range.unpack4(end_range)
+ stop = end_stop
+ stop_col = end_stop_col
+ end
+
+ if stop_col == 0 then
+ stop = stop - 1
+ end
+
+ local fold_length = stop - start + 1
+
+ -- Fold only multiline nodes that are not exactly the same as previously met folds
+ -- Checking against just the previously found fold is sufficient if nodes
+ -- are returned in preorder or postorder when traversing tree
+ if
+ fold_length > vim.wo.foldminlines and not (start == prev_start and stop == prev_stop)
+ then
+ enter_counts[start + 1] = (enter_counts[start + 1] or 0) + 1
+ leave_counts[stop + 1] = (leave_counts[stop + 1] or 0) + 1
+ prev_start = start
+ prev_stop = stop
+ end
end
end
end
- end
- end)
+ end)
- local nestmax = vim.wo.foldnestmax
- local level0_prev = info.levels0[srow] or 0
- local leave_prev = leave_counts[srow] or 0
-
- -- We now have the list of fold opening and closing, fill the gaps and mark where fold start
- for lnum = srow + 1, erow do
- local enter_line = enter_counts[lnum] or 0
- local leave_line = leave_counts[lnum] or 0
- local level0 = level0_prev - leave_prev + enter_line
-
- -- Determine if it's the start/end of a fold
- -- NB: vim's fold-expr interface does not have a mechanism to indicate that
- -- two (or more) folds start at this line, so it cannot distinguish between
- -- ( \n ( \n )) \n (( \n ) \n )
- -- versus
- -- ( \n ( \n ) \n ( \n ) \n )
- -- Both are represented by ['>1', '>2', '2', '>2', '2', '1'], and
- -- vim interprets as the second case.
- -- If it did have such a mechanism, (clamped - clamped_prev)
- -- would be the correct number of starts to pass on.
- local adjusted = level0 ---@type integer
- local prefix = ''
- if enter_line > 0 then
- prefix = '>'
- if leave_line > 0 then
- -- If this line ends a fold f1 and starts a fold f2, then move f1's end to the previous line
- -- so that f2 gets the correct level on this line. This may reduce the size of f1 below
- -- foldminlines, but we don't handle it for simplicity.
- adjusted = level0 - leave_line
- leave_line = 0
+ local nestmax = vim.wo.foldnestmax
+ local level0_prev = info.levels0[srow] or 0
+ local leave_prev = leave_counts[srow] or 0
+
+ -- We now have the list of fold opening and closing, fill the gaps and mark where fold start
+ for lnum = srow + 1, erow do
+ local enter_line = enter_counts[lnum] or 0
+ local leave_line = leave_counts[lnum] or 0
+ local level0 = level0_prev - leave_prev + enter_line
+
+ -- Determine if it's the start/end of a fold
+ -- NB: vim's fold-expr interface does not have a mechanism to indicate that
+ -- two (or more) folds start at this line, so it cannot distinguish between
+ -- ( \n ( \n )) \n (( \n ) \n )
+ -- versus
+ -- ( \n ( \n ) \n ( \n ) \n )
+ -- Both are represented by ['>1', '>2', '2', '>2', '2', '1'], and
+ -- vim interprets as the second case.
+ -- If it did have such a mechanism, (clamped - clamped_prev)
+ -- would be the correct number of starts to pass on.
+ local adjusted = level0 ---@type integer
+ local prefix = ''
+ if enter_line > 0 then
+ prefix = '>'
+ if leave_line > 0 then
+ -- If this line ends a fold f1 and starts a fold f2, then move f1's end to the previous line
+ -- so that f2 gets the correct level on this line. This may reduce the size of f1 below
+ -- foldminlines, but we don't handle it for simplicity.
+ adjusted = level0 - leave_line
+ leave_line = 0
+ end
end
- end
- -- Clamp at foldnestmax.
- local clamped = adjusted
- if adjusted > nestmax then
- prefix = ''
- clamped = nestmax
- end
+ -- Clamp at foldnestmax.
+ local clamped = adjusted
+ if adjusted > nestmax then
+ prefix = ''
+ clamped = nestmax
+ end
- -- Record the "real" level, so that it can be used as "base" of later compute_folds_levels().
- info.levels0[lnum] = adjusted
- info.levels[lnum] = prefix .. tostring(clamped)
+ -- Record the "real" level, so that it can be used as "base" of later compute_folds_levels().
+ info.levels0[lnum] = adjusted
+ info.levels[lnum] = prefix .. tostring(clamped)
- leave_prev = leave_line
- level0_prev = adjusted
- end
+ leave_prev = leave_line
+ level0_prev = adjusted
+ end
+
+ if callback then
+ callback()
+ end
+ end)
end
local M = {}
@@ -221,7 +192,7 @@ local M = {}
---@type table<integer,TS.FoldInfo>
local foldinfos = {}
-local group = api.nvim_create_augroup('treesitter/fold', {})
+local group = api.nvim_create_augroup('nvim.treesitter.fold', {})
--- Update the folds in the windows that contain the buffer and use expr foldmethod (assuming that
--- the user doesn't use different foldexpr for the same buffer).
@@ -298,12 +269,19 @@ local function schedule_if_loaded(bufnr, fn)
end
---@param bufnr integer
----@param foldinfo TS.FoldInfo
---@param tree_changes Range4[]
-local function on_changedtree(bufnr, foldinfo, tree_changes)
+local function on_changedtree(bufnr, tree_changes)
schedule_if_loaded(bufnr, function()
+ -- Buffer reload clears `foldinfos[bufnr]`, which may still be nil when callback is invoked.
+ local foldinfo = foldinfos[bufnr]
+ if not foldinfo then
+ return
+ end
+
local srow_upd, erow_upd ---@type integer?, integer?
local max_erow = api.nvim_buf_line_count(bufnr)
+ -- TODO(ribru17): Replace this with a proper .all() awaiter once #19624 is resolved
+ local iterations = 0
for _, change in ipairs(tree_changes) do
local srow, _, erow, ecol = Range.unpack4(change)
-- If a parser doesn't have any ranges explicitly set, treesitter will
@@ -317,24 +295,31 @@ local function on_changedtree(bufnr, foldinfo, tree_changes)
end
-- Start from `srow - foldminlines`, because this edit may have shrunken the fold below limit.
srow = math.max(srow - vim.wo.foldminlines, 0)
- compute_folds_levels(bufnr, foldinfo, srow, erow)
srow_upd = srow_upd and math.min(srow_upd, srow) or srow
erow_upd = erow_upd and math.max(erow_upd, erow) or erow
- end
- if #tree_changes > 0 then
- foldinfo:foldupdate(bufnr, srow_upd, erow_upd)
+ compute_folds_levels(bufnr, foldinfo, srow, erow, function()
+ iterations = iterations + 1
+ if iterations == #tree_changes then
+ foldinfo:foldupdate(bufnr, srow_upd, erow_upd)
+ end
+ end)
end
end)
end
---@param bufnr integer
----@param foldinfo TS.FoldInfo
---@param start_row integer
---@param old_row integer
---@param old_col integer
---@param new_row integer
---@param new_col integer
-local function on_bytes(bufnr, foldinfo, start_row, start_col, old_row, old_col, new_row, new_col)
+local function on_bytes(bufnr, start_row, start_col, old_row, old_col, new_row, new_col)
+ -- Buffer reload clears `foldinfos[bufnr]`, which may still be nil when callback is invoked.
+ local foldinfo = foldinfos[bufnr]
+ if not foldinfo then
+ return
+ end
+
-- extend the end to fully include the range
local end_row_old = start_row + old_row + 1
local end_row_new = start_row + new_row + 1
@@ -373,15 +358,16 @@ local function on_bytes(bufnr, foldinfo, start_row, start_col, old_row, old_col,
-- is invoked. For example, `J` with non-zero count triggers multiple on_bytes before executing
-- the scheduled callback. So we accumulate the edited ranges in `on_bytes_range`.
schedule_if_loaded(bufnr, function()
- if not foldinfo.on_bytes_range then
+ if not (foldinfo.on_bytes_range and foldinfos[bufnr]) then
return
end
local srow, erow = foldinfo.on_bytes_range[1], foldinfo.on_bytes_range[2]
foldinfo.on_bytes_range = nil
-- Start from `srow - foldminlines`, because this edit may have shrunken the fold below limit.
srow = math.max(srow - vim.wo.foldminlines, 0)
- compute_folds_levels(bufnr, foldinfo, srow, erow)
- foldinfo:foldupdate(bufnr, srow, erow)
+ compute_folds_levels(bufnr, foldinfo, srow, erow, function()
+ foldinfo:foldupdate(bufnr, srow, erow)
+ end)
end)
end
end
@@ -392,22 +378,30 @@ function M.foldexpr(lnum)
lnum = lnum or vim.v.lnum
local bufnr = api.nvim_get_current_buf()
- local parser = ts.get_parser(bufnr, nil, { error = false })
- if not parser then
- return '0'
- end
-
if not foldinfos[bufnr] then
- foldinfos[bufnr] = FoldInfo.new()
+ foldinfos[bufnr] = FoldInfo.new(bufnr)
+ api.nvim_create_autocmd({ 'BufUnload', 'VimEnter' }, {
+ buffer = bufnr,
+ once = true,
+ callback = function()
+ foldinfos[bufnr] = nil
+ end,
+ })
+
+ local parser = foldinfos[bufnr].parser
+ if not parser then
+ return '0'
+ end
+
compute_folds_levels(bufnr, foldinfos[bufnr])
parser:register_cbs({
on_changedtree = function(tree_changes)
- on_changedtree(bufnr, foldinfos[bufnr], tree_changes)
+ on_changedtree(bufnr, tree_changes)
end,
on_bytes = function(_, _, start_row, start_col, _, old_row, old_col, _, new_row, new_col, _)
- on_bytes(bufnr, foldinfos[bufnr], start_row, start_col, old_row, old_col, new_row, new_col)
+ on_bytes(bufnr, start_row, start_col, old_row, old_col, new_row, new_col)
end,
on_detach = function()
@@ -423,10 +417,17 @@ api.nvim_create_autocmd('OptionSet', {
pattern = { 'foldminlines', 'foldnestmax' },
desc = 'Refresh treesitter folds',
callback = function()
- for bufnr, _ in pairs(foldinfos) do
- foldinfos[bufnr] = FoldInfo.new()
- compute_folds_levels(bufnr, foldinfos[bufnr])
- foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr))
+ local buf = api.nvim_get_current_buf()
+ local bufs = vim.v.option_type == 'global' and vim.tbl_keys(foldinfos)
+ or foldinfos[buf] and { buf }
+ or {}
+ for _, bufnr in ipairs(bufs) do
+ foldinfos[bufnr] = FoldInfo.new(bufnr)
+ api.nvim_buf_call(bufnr, function()
+ compute_folds_levels(bufnr, foldinfos[bufnr], nil, nil, function()
+ foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr))
+ end)
+ end)
end
end,
})
diff --git a/runtime/lua/vim/treesitter/_meta/misc.lua b/runtime/lua/vim/treesitter/_meta/misc.lua
index 33701ef254..c532257f49 100644
--- a/runtime/lua/vim/treesitter/_meta/misc.lua
+++ b/runtime/lua/vim/treesitter/_meta/misc.lua
@@ -20,9 +20,15 @@ error('Cannot require a meta file')
---@class (exact) TSQueryInfo
---@field captures string[]
---@field patterns table<integer, (integer|string)[][]>
+---
+---@class TSLangInfo
+---@field fields string[]
+---@field symbols table<string,boolean>
+---@field _wasm boolean
+---@field _abi_version integer
--- @param lang string
---- @return table
+--- @return TSLangInfo
vim._ts_inspect_language = function(lang) end
---@return integer
diff --git a/runtime/lua/vim/treesitter/_meta/tsnode.lua b/runtime/lua/vim/treesitter/_meta/tsnode.lua
index d982b6a505..552905c3f0 100644
--- a/runtime/lua/vim/treesitter/_meta/tsnode.lua
+++ b/runtime/lua/vim/treesitter/_meta/tsnode.lua
@@ -68,12 +68,6 @@ function TSNode:named_child_count() end
--- @return TSNode?
function TSNode:named_child(index) end
---- Get the node's child that contains {descendant}.
---- @param descendant TSNode
---- @return TSNode?
---- @deprecated
-function TSNode:child_containing_descendant(descendant) end
-
--- Get the node's child that contains {descendant} (includes {descendant}).
---
--- For example, with the following node hierarchy:
@@ -109,17 +103,9 @@ function TSNode:end_() end
--- - end row
--- - end column
--- - end byte (if {include_bytes} is `true`)
---- @param include_bytes boolean?
-function TSNode:range(include_bytes) end
-
---- @nodoc
--- @param include_bytes false?
--- @return integer, integer, integer, integer
-function TSNode:range(include_bytes) end
-
---- @nodoc
---- @param include_bytes true
---- @return integer, integer, integer, integer, integer, integer
+--- @overload fun(self: TSNode, include_bytes: true): integer, integer, integer, integer, integer, integer
function TSNode:range(include_bytes) end
--- Get the node's type as a string.
diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua
index a825505378..3dfc6b0cfe 100644
--- a/runtime/lua/vim/treesitter/_query_linter.lua
+++ b/runtime/lua/vim/treesitter/_query_linter.lua
@@ -1,6 +1,6 @@
local api = vim.api
-local namespace = api.nvim_create_namespace('vim.treesitter.query_linter')
+local namespace = api.nvim_create_namespace('nvim.treesitter.query_linter')
local M = {}
@@ -138,7 +138,9 @@ local function lint_match(buf, match, query, lang_context, diagnostics)
-- perform language-independent checks only for first lang
if lang_context.is_first_lang and cap_id == 'error' then
local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ')
- add_lint_for_node(diagnostics, { node:range() }, 'Syntax error: ' .. node_text)
+ ---@diagnostic disable-next-line: missing-fields LuaLS varargs bug
+ local range = { node:range() } --- @type Range4
+ add_lint_for_node(diagnostics, range, 'Syntax error: ' .. node_text)
end
-- other checks rely on Neovim parser introspection
diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua
index 26817cdba5..24dd8243db 100644
--- a/runtime/lua/vim/treesitter/dev.lua
+++ b/runtime/lua/vim/treesitter/dev.lua
@@ -119,7 +119,7 @@ function TSTreeView:new(bufnr, lang)
end
local t = {
- ns = api.nvim_create_namespace('treesitter/dev-inspect'),
+ ns = api.nvim_create_namespace('nvim.treesitter.dev_inspect'),
nodes = nodes,
named = named,
---@type vim.treesitter.dev.TSTreeViewOpts
@@ -135,15 +135,7 @@ function TSTreeView:new(bufnr, lang)
return t
end
-local decor_ns = api.nvim_create_namespace('ts.dev')
-
----@param range Range4
----@return string
-local function range_to_string(range)
- ---@type integer, integer, integer, integer
- local row, col, end_row, end_col = unpack(range)
- return string.format('[%d, %d] - [%d, %d]', row, col, end_row, end_col)
-end
+local decor_ns = api.nvim_create_namespace('nvim.treesitter.dev')
---@param w integer
---@return boolean closed Whether the window was closed.
@@ -227,14 +219,17 @@ function TSTreeView:draw(bufnr)
local lang_hl_marks = {} ---@type table[]
for i, item in self:iter() do
- local range_str = range_to_string({ item.node:range() })
+ local range_str = ('[%d, %d] - [%d, %d]'):format(item.node:range())
local lang_str = self.opts.lang and string.format(' %s', item.lang) or ''
local text ---@type string
if item.node:named() then
- text = string.format('(%s', item.node:type())
+ text = string.format('(%s%s', item.node:missing() and 'MISSING ' or '', item.node:type())
else
text = string.format('%q', item.node:type()):gsub('\n', 'n')
+ if item.node:missing() then
+ text = string.format('(MISSING %s)', text)
+ end
end
if item.field then
text = string.format('%s: %s', item.field, text)
@@ -442,7 +437,7 @@ function M.inspect_tree(opts)
end,
})
- local group = api.nvim_create_augroup('treesitter/dev', {})
+ local group = api.nvim_create_augroup('nvim.treesitter.dev', {})
api.nvim_create_autocmd('CursorMoved', {
group = group,
@@ -547,7 +542,7 @@ function M.inspect_tree(opts)
})
end
-local edit_ns = api.nvim_create_namespace('treesitter/dev-edit')
+local edit_ns = api.nvim_create_namespace('nvim.treesitter.dev_edit')
---@param query_win integer
---@param base_win integer
@@ -633,7 +628,7 @@ function M.edit_query(lang)
-- can infer the language later.
api.nvim_buf_set_name(query_buf, string.format('%s/query_editor.scm', lang))
- local group = api.nvim_create_augroup('treesitter/dev-edit', {})
+ local group = api.nvim_create_augroup('nvim.treesitter.dev_edit', {})
api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, {
group = group,
buffer = query_buf,
diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua
index 8ce8652f7d..6dd47811bd 100644
--- a/runtime/lua/vim/treesitter/highlighter.lua
+++ b/runtime/lua/vim/treesitter/highlighter.lua
@@ -2,7 +2,7 @@ local api = vim.api
local query = vim.treesitter.query
local Range = require('vim.treesitter._range')
-local ns = api.nvim_create_namespace('treesitter/highlighter')
+local ns = api.nvim_create_namespace('nvim.treesitter.highlighter')
---@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch
@@ -69,6 +69,7 @@ end
---@field private _queries table<string,vim.treesitter.highlighter.Query>
---@field tree vim.treesitter.LanguageTree
---@field private redraw_count integer
+---@field parsing boolean true if we are parsing asynchronously
local TSHighlighter = {
active = {},
}
@@ -147,8 +148,6 @@ function TSHighlighter.new(tree, opts)
vim.opt_local.spelloptions:append('noplainbuffer')
end)
- self.tree:parse()
-
return self
end
@@ -161,7 +160,10 @@ function TSHighlighter:destroy()
vim.bo[self.bufnr].spelloptions = self.orig_spelloptions
vim.b[self.bufnr].ts_highlight = nil
if vim.g.syntax_on == 1 then
- api.nvim_exec_autocmds('FileType', { group = 'syntaxset', buffer = self.bufnr })
+ api.nvim_exec_autocmds(
+ 'FileType',
+ { group = 'syntaxset', buffer = self.bufnr, modeline = false }
+ )
end
end
end
@@ -299,6 +301,8 @@ local function on_line_impl(self, buf, line, is_spell_nav)
state.highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1)
end
+ local captures = state.highlighter_query:query().captures
+
while line >= state.next_row do
local capture, node, metadata, match = state.iter(line)
@@ -311,7 +315,7 @@ local function on_line_impl(self, buf, line, is_spell_nav)
if capture then
local hl = state.highlighter_query:get_hl_from_capture(capture)
- local capture_name = state.highlighter_query:query().captures[capture]
+ local capture_name = captures[capture]
local spell, spell_pri_offset = get_spell(capture_name)
@@ -382,19 +386,23 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
end
---@private
----@param _win integer
---@param buf integer
---@param topline integer
---@param botline integer
-function TSHighlighter._on_win(_, _win, buf, topline, botline)
+function TSHighlighter._on_win(_, _, buf, topline, botline)
local self = TSHighlighter.active[buf]
- if not self then
+ if not self or self.parsing then
return false
end
- self.tree:parse({ topline, botline + 1 })
- self:prepare_highlight_states(topline, botline + 1)
+ self.parsing = self.tree:parse({ topline, botline + 1 }, function(_, trees)
+ if trees and self.parsing then
+ self.parsing = false
+ api.nvim__redraw({ buf = buf, valid = false, flush = false })
+ end
+ end) == nil
self.redraw_count = self.redraw_count + 1
- return true
+ self:prepare_highlight_states(topline, botline)
+ return #self._highlight_states > 0
end
api.nvim_set_decoration_provider(ns, {
diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua
index 446051dfd7..16d19bfc5a 100644
--- a/runtime/lua/vim/treesitter/language.lua
+++ b/runtime/lua/vim/treesitter/language.lua
@@ -133,8 +133,9 @@ function M.add(lang, opts)
path = paths[1]
end
- return loadparser(path, lang, symbol_name) or nil,
- string.format('Cannot load parser %s for language "%s"', path, lang)
+ local res = loadparser(path, lang, symbol_name)
+ return res,
+ res == nil and string.format('Cannot load parser %s for language "%s"', path, lang) or nil
end
--- @param x string|string[]
@@ -174,7 +175,7 @@ end
--- (`"`).
---
---@param lang string Language
----@return table
+---@return TSLangInfo
function M.inspect(lang)
M.add(lang)
return vim._ts_inspect_language(lang)
diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua
index 4b42164dc8..ea745c4deb 100644
--- a/runtime/lua/vim/treesitter/languagetree.lua
+++ b/runtime/lua/vim/treesitter/languagetree.lua
@@ -44,6 +44,8 @@ local query = require('vim.treesitter.query')
local language = require('vim.treesitter.language')
local Range = require('vim.treesitter._range')
+local default_parse_timeout_ms = 3
+
---@alias TSCallbackName
---| 'changedtree'
---| 'bytes'
@@ -58,6 +60,8 @@ local Range = require('vim.treesitter._range')
---| 'on_child_added'
---| 'on_child_removed'
+---@alias ParserThreadState { timeout: integer? }
+
--- @type table<TSCallbackNameOn,TSCallbackName>
local TSCallbackNames = {
on_changedtree = 'changedtree',
@@ -76,8 +80,13 @@ local TSCallbackNames = {
---@field private _injections_processed boolean
---@field private _opts table Options
---@field private _parser TSParser Parser for language
----@field private _has_regions boolean
+---Table of regions for which the tree is currently running an async parse
+---@field private _ranges_being_parsed table<string, boolean>
+---Table of callback queues, keyed by each region for which the callbacks should be run
+---@field private _cb_queues table<string, fun(err?: string, trees?: table<integer, TSTree>)[]>
---@field private _regions table<integer, Range6[]>?
+---The total number of regions. Since _regions can have holes, we cannot simply read this value from #_regions.
+---@field private _num_regions integer
---List of regions this tree should manage and parse. If nil then regions are
---taken from _trees. This is mostly a short-lived cache for included_regions()
---@field private _lang string Language name
@@ -85,7 +94,8 @@ local TSCallbackNames = {
---@field private _source (integer|string) Buffer or string to parse
---@field private _trees table<integer, TSTree> Reference to parsed tree (one for each language).
---Each key is the index of region, which is synced with _regions and _valid.
----@field private _valid boolean|table<integer,boolean> If the parsed tree is valid
+---@field private _valid_regions table<integer,true> Set of valid region IDs.
+---@field private _is_entirely_valid boolean Whether the entire tree (excluding children) is valid.
---@field private _logger? fun(logtype: string, msg: string)
---@field private _logfile? file*
local LanguageTree = {}
@@ -117,7 +127,7 @@ function LanguageTree.new(source, lang, opts)
local injections = opts.injections or {}
- --- @type vim.treesitter.LanguageTree
+ --- @class vim.treesitter.LanguageTree
local self = {
_source = source,
_lang = lang,
@@ -126,10 +136,13 @@ function LanguageTree.new(source, lang, opts)
_opts = opts,
_injection_query = injections[lang] and query.parse(lang, injections[lang])
or query.get(lang, 'injections'),
- _has_regions = false,
_injections_processed = false,
- _valid = false,
+ _valid_regions = {},
+ _num_regions = 1,
+ _is_entirely_valid = false,
_parser = vim._create_ts_parser(lang),
+ _ranges_being_parsed = {},
+ _cb_queues = {},
_callbacks = {},
_callbacks_rec = {},
}
@@ -182,7 +195,7 @@ end
---Measure execution time of a function
---@generic R1, R2, R3
----@param f fun(): R1, R2, R2
+---@param f fun(): R1, R2, R3
---@return number, R1, R2, R3
local function tcall(f, ...)
local start = vim.uv.hrtime()
@@ -190,6 +203,7 @@ local function tcall(f, ...)
local r = { f(...) }
--- @type number
local duration = (vim.uv.hrtime() - start) / 1000000
+ --- @diagnostic disable-next-line: redundant-return-value
return duration, unpack(r)
end
@@ -231,7 +245,9 @@ end
--- tree in treesitter. Doesn't clear filesystem cache. Called often, so needs to be fast.
---@param reload boolean|nil
function LanguageTree:invalidate(reload)
- self._valid = false
+ self._valid_regions = {}
+ self._is_entirely_valid = false
+ self._parser:reset()
-- buffer was reloaded, reparse all trees
if reload then
@@ -258,20 +274,51 @@ function LanguageTree:trees()
end
--- Gets the language of this tree node.
+--- @return string
function LanguageTree:lang()
return self._lang
end
+--- @param region Range6[]
+--- @param range? boolean|Range
+--- @return boolean
+local function intercepts_region(region, range)
+ if #region == 0 then
+ return true
+ end
+
+ if range == nil then
+ return false
+ end
+
+ if type(range) == 'boolean' then
+ return range
+ end
+
+ for _, r in ipairs(region) do
+ if Range.intercepts(r, range) then
+ return true
+ end
+ end
+
+ return false
+end
+
--- Returns whether this LanguageTree is valid, i.e., |LanguageTree:trees()| reflects the latest
--- state of the source. If invalid, user should call |LanguageTree:parse()|.
----@param exclude_children boolean|nil whether to ignore the validity of children (default `false`)
+---@param exclude_children boolean? whether to ignore the validity of children (default `false`)
+---@param range Range? range to check for validity
---@return boolean
-function LanguageTree:is_valid(exclude_children)
- local valid = self._valid
+function LanguageTree:is_valid(exclude_children, range)
+ local valid_regions = self._valid_regions
- if type(valid) == 'table' then
- for i, _ in pairs(self:included_regions()) do
- if not valid[i] then
+ if not self._is_entirely_valid then
+ if not range then
+ return false
+ end
+ -- TODO: Efficiently search for possibly intersecting regions using a binary search
+ for i, region in pairs(self:included_regions()) do
+ if not valid_regions[i] and intercepts_region(region, range) then
return false
end
end
@@ -283,97 +330,81 @@ function LanguageTree:is_valid(exclude_children)
end
for _, child in pairs(self._children) do
- if not child:is_valid(exclude_children) then
+ if not child:is_valid(exclude_children, range) then
return false
end
end
end
- if type(valid) == 'boolean' then
- return valid
- end
-
- self._valid = true
return true
end
--- Returns a map of language to child tree.
+--- @return table<string,vim.treesitter.LanguageTree>
function LanguageTree:children()
return self._children
end
--- Returns the source content of the language tree (bufnr or string).
+--- @return integer|string
function LanguageTree:source()
return self._source
end
---- @param region Range6[]
---- @param range? boolean|Range
---- @return boolean
-local function intercepts_region(region, range)
- if #region == 0 then
- return true
- end
-
- if range == nil then
- return false
- end
-
- if type(range) == 'boolean' then
- return range
- end
-
- for _, r in ipairs(region) do
- if Range.intercepts(r, range) then
- return true
- end
- end
-
- return false
-end
-
--- @private
--- @param range boolean|Range?
+--- @param thread_state ParserThreadState
--- @return Range6[] changes
--- @return integer no_regions_parsed
--- @return number total_parse_time
-function LanguageTree:_parse_regions(range)
+--- @return boolean finished whether async parsing still needs time
+function LanguageTree:_parse_regions(range, thread_state)
local changes = {}
local no_regions_parsed = 0
local total_parse_time = 0
- if type(self._valid) ~= 'table' then
- self._valid = {}
- end
-
-- If there are no ranges, set to an empty list
-- so the included ranges in the parser are cleared.
for i, ranges in pairs(self:included_regions()) do
if
- not self._valid[i]
+ not self._valid_regions[i]
and (
intercepts_region(ranges, range)
or (self._trees[i] and intercepts_region(self._trees[i]:included_ranges(false), range))
)
then
self._parser:set_included_ranges(ranges)
+ self._parser:set_timeout(thread_state.timeout and thread_state.timeout * 1000 or 0) -- ms -> micros
+
local parse_time, tree, tree_changes =
tcall(self._parser.parse, self._parser, self._trees[i], self._source, true)
+ while true do
+ if tree then
+ break
+ end
+ coroutine.yield(changes, no_regions_parsed, total_parse_time, false)
- -- Pass ranges if this is an initial parse
- local cb_changes = self._trees[i] and tree_changes or tree:included_ranges(true)
+ parse_time, tree, tree_changes =
+ tcall(self._parser.parse, self._parser, self._trees[i], self._source, true)
+ end
- self:_do_callback('changedtree', cb_changes, tree)
+ self:_do_callback('changedtree', tree_changes, tree)
self._trees[i] = tree
vim.list_extend(changes, tree_changes)
total_parse_time = total_parse_time + parse_time
no_regions_parsed = no_regions_parsed + 1
- self._valid[i] = true
+ self._valid_regions[i] = true
+
+ -- _valid_regions can have holes, but that is okay because this equality is only true when it
+ -- has no holes (meaning all regions are valid)
+ if #self._valid_regions == self._num_regions then
+ self._is_entirely_valid = true
+ end
end
end
- return changes, no_regions_parsed, total_parse_time
+ return changes, no_regions_parsed, total_parse_time, true
end
--- @private
@@ -409,6 +440,98 @@ function LanguageTree:_add_injections()
return query_time
end
+--- @param range boolean|Range?
+--- @return string
+local function range_to_string(range)
+ return type(range) == 'table' and table.concat(range, ',') or tostring(range)
+end
+
+--- @private
+--- @param range boolean|Range?
+--- @param callback fun(err?: string, trees?: table<integer, TSTree>)
+function LanguageTree:_push_async_callback(range, callback)
+ local key = range_to_string(range)
+ self._cb_queues[key] = self._cb_queues[key] or {}
+ local queue = self._cb_queues[key]
+ queue[#queue + 1] = callback
+end
+
+--- @private
+--- @param range boolean|Range?
+--- @param err? string
+--- @param trees? table<integer, TSTree>
+function LanguageTree:_run_async_callbacks(range, err, trees)
+ local key = range_to_string(range)
+ for _, cb in ipairs(self._cb_queues[key]) do
+ cb(err, trees)
+ end
+ self._ranges_being_parsed[key] = nil
+ self._cb_queues[key] = nil
+end
+
+--- Run an asynchronous parse, calling {on_parse} when complete.
+---
+--- @private
+--- @param range boolean|Range?
+--- @param on_parse fun(err?: string, trees?: table<integer, TSTree>)
+--- @return table<integer, TSTree>? trees the list of parsed trees, if parsing completed synchronously
+function LanguageTree:_async_parse(range, on_parse)
+ self:_push_async_callback(range, on_parse)
+
+ -- If we are already running an async parse, just queue the callback.
+ local range_string = range_to_string(range)
+ if not self._ranges_being_parsed[range_string] then
+ self._ranges_being_parsed[range_string] = true
+ else
+ return
+ end
+
+ local source = self._source
+ local is_buffer_parser = type(source) == 'number'
+ local buf = is_buffer_parser and vim.b[source] or nil
+ local ct = is_buffer_parser and buf.changedtick or nil
+ local total_parse_time = 0
+ local redrawtime = vim.o.redrawtime
+
+ local thread_state = {} ---@type ParserThreadState
+
+ ---@type fun(): table<integer, TSTree>, boolean
+ local parse = coroutine.wrap(self._parse)
+
+ local function step()
+ if is_buffer_parser then
+ if
+ not vim.api.nvim_buf_is_valid(source --[[@as number]])
+ then
+ return nil
+ end
+
+ -- If buffer was changed in the middle of parsing, reset parse state
+ if buf.changedtick ~= ct then
+ ct = buf.changedtick
+ total_parse_time = 0
+ parse = coroutine.wrap(self._parse)
+ end
+ end
+
+ thread_state.timeout = not vim.g._ts_force_sync_parsing and default_parse_timeout_ms or nil
+ local parse_time, trees, finished = tcall(parse, self, range, thread_state)
+ total_parse_time = total_parse_time + parse_time
+
+ if finished then
+ self:_run_async_callbacks(range, nil, trees)
+ return trees
+ elseif total_parse_time > redrawtime then
+ self:_run_async_callbacks(range, 'TIMEOUT', nil)
+ return nil
+ else
+ vim.schedule(step)
+ end
+ end
+
+ return step()
+end
+
--- Recursively parse all regions in the language tree using |treesitter-parsers|
--- for the corresponding languages and run injection queries on the parsed trees
--- to determine whether child trees should be created and parsed.
@@ -420,11 +543,33 @@ end
--- Set to `true` to run a complete parse of the source (Note: Can be slow!)
--- Set to `false|nil` to only parse regions with empty ranges (typically
--- only the root tree without injections).
---- @return table<integer, TSTree>
-function LanguageTree:parse(range)
- if self:is_valid() then
+--- @param on_parse fun(err?: string, trees?: table<integer, TSTree>)? Function invoked when parsing completes.
+--- When provided and `vim.g._ts_force_sync_parsing` is not set, parsing will run
+--- asynchronously. The first argument to the function is a string representing the error type,
+--- in case of a failure (currently only possible for timeouts). The second argument is the list
+--- of trees returned by the parse (upon success), or `nil` if the parse timed out (determined
+--- by 'redrawtime').
+---
+--- If parsing was still able to finish synchronously (within 3ms), `parse()` returns the list
+--- of trees. Otherwise, it returns `nil`.
+--- @return table<integer, TSTree>?
+function LanguageTree:parse(range, on_parse)
+ if on_parse then
+ return self:_async_parse(range, on_parse)
+ end
+ local trees, _ = self:_parse(range, {})
+ return trees
+end
+
+--- @private
+--- @param range boolean|Range|nil
+--- @param thread_state ParserThreadState
+--- @return table<integer, TSTree> trees
+--- @return boolean finished
+function LanguageTree:_parse(range, thread_state)
+ if self:is_valid(nil, type(range) == 'table' and range or nil) then
self:_log('valid')
- return self._trees
+ return self._trees, true
end
local changes --- @type Range6[]?
@@ -435,15 +580,27 @@ function LanguageTree:parse(range)
local total_parse_time = 0
-- At least 1 region is invalid
- if not self:is_valid(true) then
- changes, no_regions_parsed, total_parse_time = self:_parse_regions(range)
+ if not self:is_valid(true, type(range) == 'table' and range or nil) then
+ ---@type fun(self: vim.treesitter.LanguageTree, range: boolean|Range?, thread_state: ParserThreadState): Range6[], integer, number, boolean
+ local parse_regions = coroutine.wrap(self._parse_regions)
+ while true do
+ local is_finished
+ changes, no_regions_parsed, total_parse_time, is_finished =
+ parse_regions(self, range, thread_state)
+ thread_state.timeout = thread_state.timeout
+ and math.max(thread_state.timeout - total_parse_time, 0)
+ if is_finished then
+ break
+ end
+ coroutine.yield(self._trees, false)
+ end
-- Need to run injections when we parsed something
if no_regions_parsed > 0 then
self._injections_processed = false
end
end
- if not self._injections_processed and range ~= false and range ~= nil then
+ if not self._injections_processed and range then
query_time = self:_add_injections()
self._injections_processed = true
end
@@ -457,10 +614,24 @@ function LanguageTree:parse(range)
})
for _, child in pairs(self._children) do
- child:parse(range)
+ if thread_state.timeout == 0 then
+ coroutine.yield(self._trees, false)
+ end
+
+ ---@type fun(): table<integer, TSTree>, boolean
+ local parse = coroutine.wrap(child._parse)
+
+ while true do
+ local ctime, _, child_finished = tcall(parse, child, range, thread_state)
+ if child_finished then
+ thread_state.timeout = thread_state.timeout and math.max(thread_state.timeout - ctime, 0)
+ break
+ end
+ coroutine.yield(self._trees, child_finished)
+ end
end
- return self._trees
+ return self._trees, true
end
--- Invokes the callback for each |LanguageTree| recursively.
@@ -504,7 +675,8 @@ function LanguageTree:add_child(lang)
return self._children[lang]
end
---- @package
+---Returns the parent tree. `nil` for the root tree.
+---@return vim.treesitter.LanguageTree?
function LanguageTree:parent()
return self._parent
end
@@ -551,38 +723,34 @@ end
---region is valid or not.
---@param fn fun(index: integer, region: Range6[]): boolean
function LanguageTree:_iter_regions(fn)
- if not self._valid then
+ if vim.deep_equal(self._valid_regions, {}) then
return
end
- local was_valid = type(self._valid) ~= 'table'
-
- if was_valid then
- self:_log('was valid', self._valid)
- self._valid = {}
+ if self._is_entirely_valid then
+ self:_log('was valid')
end
local all_valid = true
for i, region in pairs(self:included_regions()) do
- if was_valid or self._valid[i] then
- self._valid[i] = fn(i, region)
- if not self._valid[i] then
+ if self._valid_regions[i] then
+ -- Setting this to nil rather than false allows us to determine if all regions were parsed
+ -- just by checking the length of _valid_regions.
+ self._valid_regions[i] = fn(i, region) and true or nil
+ if not self._valid_regions[i] then
self:_log(function()
return 'invalidating region', i, region_tostr(region)
end)
end
end
- if not self._valid[i] then
+ if not self._valid_regions[i] then
all_valid = false
end
end
- -- Compress the valid value to 'true' if there are no invalid regions
- if all_valid then
- self._valid = all_valid
- end
+ self._is_entirely_valid = all_valid
end
--- Sets the included regions that should be parsed by this |LanguageTree|.
@@ -602,14 +770,13 @@ end
---@private
---@param new_regions (Range4|Range6|TSNode)[][] List of regions this tree should manage and parse.
function LanguageTree:set_included_regions(new_regions)
- self._has_regions = true
-
-- Transform the tables from 4 element long to 6 element long (with byte offset)
for _, region in ipairs(new_regions) do
for i, range in ipairs(region) do
if type(range) == 'table' and #range == 4 then
region[i] = Range.add_bytes(self._source, range --[[@as Range4]])
elseif type(range) == 'userdata' then
+ --- @diagnostic disable-next-line: missing-fields LuaLS varargs bug
region[i] = { range:range(true) }
end
end
@@ -633,6 +800,7 @@ function LanguageTree:set_included_regions(new_regions)
end
self._regions = new_regions
+ self._num_regions = #new_regions
end
---Gets the set of included regions managed by this LanguageTree. This can be different from the
@@ -646,18 +814,8 @@ function LanguageTree:included_regions()
return self._regions
end
- if not self._has_regions then
- -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} (the full range)
- return { {} }
- end
-
- local regions = {} ---@type Range6[][]
- for i, _ in pairs(self._trees) do
- regions[i] = self._trees[i]:included_ranges(true)
- end
-
- self._regions = regions
- return regions
+ -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} (the full range)
+ return { {} }
end
---@param node TSNode
@@ -821,7 +979,7 @@ end
--- @private
--- @return table<string, Range6[][]>
function LanguageTree:_get_injections()
- if not self._injection_query then
+ if not self._injection_query or #self._injection_query.captures == 0 then
return {}
end
@@ -907,7 +1065,15 @@ function LanguageTree:_edit(
)
end
- self._regions = nil
+ self._parser:reset()
+
+ if self._regions then
+ local regions = {} ---@type table<integer, Range6[]>
+ for i, tree in pairs(self._trees) do
+ regions[i] = tree:included_ranges(true)
+ end
+ self._regions = regions
+ end
local changed_range = {
start_row,
diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua
index 1677e8d364..10fb82e533 100644
--- a/runtime/lua/vim/treesitter/query.lua
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -1,17 +1,77 @@
+--- @brief 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.
+
local api = vim.api
local language = require('vim.treesitter.language')
local memoize = vim.func._memoize
+local MODELINE_FORMAT = '^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$'
+local EXTENDS_FORMAT = '^;+%s*extends%s*$'
+
local M = {}
+local function is_directive(name)
+ return string.sub(name, -1) == '!'
+end
+
+---@nodoc
+---@class vim.treesitter.query.ProcessedPredicate
+---@field [1] string predicate name
+---@field [2] boolean should match
+---@field [3] (integer|string)[] the original predicate
+
+---@alias vim.treesitter.query.ProcessedDirective (integer|string)[]
+
+---@nodoc
+---@class vim.treesitter.query.ProcessedPattern {
+---@field predicates vim.treesitter.query.ProcessedPredicate[]
+---@field directives vim.treesitter.query.ProcessedDirective[]
+
+--- Splits the query patterns into predicates and directives.
+---@param patterns table<integer, (integer|string)[][]>
+---@return table<integer, vim.treesitter.query.ProcessedPattern>
+local function process_patterns(patterns)
+ ---@type table<integer, vim.treesitter.query.ProcessedPattern>
+ local processed_patterns = {}
+
+ for k, pattern_list in pairs(patterns) do
+ ---@type vim.treesitter.query.ProcessedPredicate[]
+ local predicates = {}
+ ---@type vim.treesitter.query.ProcessedDirective[]
+ local directives = {}
+
+ for _, pattern in ipairs(pattern_list) do
+ -- Note: tree-sitter strips the leading # from predicates for us.
+ local pred_name = pattern[1]
+ ---@cast pred_name string
+
+ if is_directive(pred_name) then
+ table.insert(directives, pattern)
+ else
+ local should_match = true
+ if pred_name:match('^not%-') then
+ pred_name = pred_name:sub(5)
+ should_match = false
+ end
+ table.insert(predicates, { pred_name, should_match, pattern })
+ end
+ end
+
+ processed_patterns[k] = { predicates = predicates, directives = directives }
+ end
+
+ return processed_patterns
+end
+
---@nodoc
---Parsed query, see |vim.treesitter.query.parse()|
---
---@class vim.treesitter.Query
----@field lang string name of the language for this parser
+---@field lang string parser language name
---@field captures string[] list of (unique) capture names defined in query
----@field info vim.treesitter.QueryInfo contains information used in the query (e.g. captures, predicates, directives)
+---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives)
---@field query TSQuery userdata query object
+---@field private _processed_patterns table<integer, vim.treesitter.query.ProcessedPattern>
local Query = {}
Query.__index = Query
@@ -30,6 +90,7 @@ function Query.new(lang, ts_query)
patterns = query_info.patterns,
}
self.captures = self.info.captures
+ self._processed_patterns = process_patterns(self.info.patterns)
return self
end
@@ -109,9 +170,6 @@ function M.get_files(lang, query_name, is_included)
-- ;+ inherits: ({language},)*{language}
--
-- {language} ::= {lang} | ({lang})
- local MODELINE_FORMAT = '^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$'
- local EXTENDS_FORMAT = '^;+%s*extends%s*$'
-
for _, filename in ipairs(lang_files) do
local file, err = io.open(filename, 'r')
if not file then
@@ -184,8 +242,8 @@ local function read_query_files(filenames)
return table.concat(contents, '')
end
--- The explicitly set queries from |vim.treesitter.query.set()|
----@type table<string,table<string,vim.treesitter.Query>>
+-- The explicitly set query strings from |vim.treesitter.query.set()|
+---@type table<string,table<string,string>>
local explicit_queries = setmetatable({}, {
__index = function(t, k)
local lang_queries = {}
@@ -197,14 +255,27 @@ local explicit_queries = setmetatable({}, {
--- Sets the runtime query named {query_name} for {lang}
---
---- This allows users to override any runtime files and/or configuration
+--- This allows users to override or extend any runtime files and/or configuration
--- set by plugins.
---
+--- For example, you could enable spellchecking of `C` identifiers with the
+--- following code:
+--- ```lua
+--- vim.treesitter.query.set(
+--- 'c',
+--- 'highlights',
+--- [[;inherits c
+--- (identifier) @spell]])
+--- ]])
+--- ```
+---
---@param lang string Language to use for the query
---@param query_name string Name of the query (e.g., "highlights")
---@param text string Query text (unparsed).
function M.set(lang, query_name, text)
- explicit_queries[lang][query_name] = M.parse(lang, text)
+ --- @diagnostic disable-next-line: undefined-field LuaLS bad at generics
+ M.get:clear(lang, query_name)
+ explicit_queries[lang][query_name] = text
end
--- Returns the runtime query {query_name} for {lang}.
@@ -214,34 +285,82 @@ end
---
---@return vim.treesitter.Query? : Parsed query. `nil` if no query files are found.
M.get = memoize('concat-2', function(lang, query_name)
+ local query_string ---@type string
+
if explicit_queries[lang][query_name] then
- return explicit_queries[lang][query_name]
- end
+ local query_files = {}
+ local base_langs = {} ---@type string[]
- local query_files = M.get_files(lang, query_name)
- local query_string = read_query_files(query_files)
+ for line in explicit_queries[lang][query_name]:gmatch('([^\n]*)\n?') do
+ if not vim.startswith(line, ';') then
+ break
+ end
+
+ local lang_list = line:match(MODELINE_FORMAT)
+ if lang_list then
+ for _, incl_lang in ipairs(vim.split(lang_list, ',')) do
+ local is_optional = incl_lang:match('%(.*%)')
+
+ if is_optional then
+ add_included_lang(base_langs, lang, incl_lang:sub(2, #incl_lang - 1))
+ else
+ add_included_lang(base_langs, lang, incl_lang)
+ end
+ end
+ elseif line:match(EXTENDS_FORMAT) then
+ table.insert(base_langs, lang)
+ end
+ end
+
+ for _, base_lang in ipairs(base_langs) do
+ local base_files = M.get_files(base_lang, query_name, true)
+ vim.list_extend(query_files, base_files)
+ end
+
+ query_string = read_query_files(query_files) .. explicit_queries[lang][query_name]
+ else
+ local query_files = M.get_files(lang, query_name)
+ query_string = read_query_files(query_files)
+ end
if #query_string == 0 then
return nil
end
return M.parse(lang, query_string)
-end)
+end, false)
+
+api.nvim_create_autocmd('OptionSet', {
+ pattern = { 'runtimepath' },
+ group = api.nvim_create_augroup('nvim.treesitter.query_cache_reset', { clear = true }),
+ callback = function()
+ --- @diagnostic disable-next-line: undefined-field LuaLS bad at generics
+ M.get:clear()
+ end,
+})
---- 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 the `iter_captures` and `iter_matches` methods.
+--- Parses a {query} string and returns a `Query` object (|lua-treesitter-query|), which can be used
+--- to search the tree for the query patterns (via |Query:iter_captures()|, |Query:iter_matches()|),
+--- or inspect the query via these fields:
+--- - `captures`: a list of unique capture names defined in the query (alias: `info.captures`).
+--- - `info.patterns`: information about predicates.
---
---- Exposes `info` and `captures` with additional context about {query}.
---- - `captures` contains the list of unique capture names defined in {query}.
---- - `info.captures` also points to `captures`.
---- - `info.patterns` contains information about predicates.
+--- Example:
+--- ```lua
+--- local query = vim.treesitter.query.parse('vimdoc', [[
+--- ; query
+--- ((h1) @str
+--- (#trim! @str 1 1 1 1))
+--- ]])
+--- local tree = vim.treesitter.get_parser():parse()[1]
+--- for id, node, metadata in query:iter_captures(tree:root(), 0) do
+--- -- Print the node name and source text.
+--- vim.print({node:type(), vim.treesitter.get_node_text(node, vim.api.nvim_get_current_buf())})
+--- end
+--- ```
---
---@param lang string Language to use for the query
----@param query string Query in s-expr syntax
+---@param query string Query text, in s-expr syntax
---
---@return vim.treesitter.Query : Parsed query
---
@@ -250,7 +369,7 @@ M.parse = memoize('concat-2', function(lang, query)
assert(language.add(lang))
local ts_query = vim._ts_parse_query(lang, query)
return Query.new(lang, ts_query)
-end)
+end, false)
--- Implementations of predicates that can optionally be prefixed with "any-".
---
@@ -572,13 +691,17 @@ local directive_handlers = {
metadata[id].text = text:gsub(pattern, replacement)
end,
- -- Trim blank lines from end of the node
- -- Example: (#trim! @fold)
- -- TODO(clason): generalize to arbitrary whitespace removal
+ -- Trim whitespace from both sides of the node
+ -- Example: (#trim! @fold 1 1 1 1)
['trim!'] = function(match, _, bufnr, pred, metadata)
local capture_id = pred[2]
assert(type(capture_id) == 'number')
+ local trim_start_lines = pred[3] == '1'
+ local trim_start_cols = pred[4] == '1'
+ local trim_end_lines = pred[5] == '1' or not pred[3] -- default true for backwards compatibility
+ local trim_end_cols = pred[6] == '1'
+
local nodes = match[capture_id]
if not nodes or #nodes == 0 then
return
@@ -588,20 +711,45 @@ local directive_handlers = {
local start_row, start_col, end_row, end_col = node:range()
- -- Don't trim if region ends in middle of a line
- if end_col ~= 0 then
- return
+ local node_text = vim.split(vim.treesitter.get_node_text(node, bufnr), '\n')
+ if end_col == 0 then
+ -- get_node_text() will ignore the last line if the node ends at column 0
+ node_text[#node_text + 1] = ''
end
- while end_row >= start_row do
- -- As we only care when end_col == 0, always inspect one line above end_row.
- local end_line = api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)[1]
+ local end_idx = #node_text
+ local start_idx = 1
- if end_line ~= '' then
- break
+ if trim_end_lines then
+ while end_idx > 0 and node_text[end_idx]:find('^%s*$') do
+ end_idx = end_idx - 1
+ end_row = end_row - 1
+ -- set the end position to the last column of the next line, or 0 if we just trimmed the
+ -- last line
+ end_col = end_idx > 0 and #node_text[end_idx] or 0
end
+ end
+ if trim_end_cols then
+ if end_idx == 0 then
+ end_row = start_row
+ end_col = start_col
+ else
+ local whitespace_start = node_text[end_idx]:find('(%s*)$')
+ end_col = (whitespace_start - 1) + (end_idx == 1 and start_col or 0)
+ end
+ end
- end_row = end_row - 1
+ if trim_start_lines then
+ while start_idx <= end_idx and node_text[start_idx]:find('^%s*$') do
+ start_idx = start_idx + 1
+ start_row = start_row + 1
+ start_col = 0
+ end
+ end
+ if trim_start_cols and node_text[start_idx] then
+ local _, whitespace_end = node_text[start_idx]:find('^(%s*)')
+ whitespace_end = whitespace_end or 0
+ start_col = (start_idx == 1 and start_col or 0) + whitespace_end
end
-- If this produces an invalid range, we just skip it.
@@ -711,84 +859,50 @@ function M.list_predicates()
return vim.tbl_keys(predicate_handlers)
end
-local function xor(x, y)
- return (x or y) and not (x and y)
-end
-
-local function is_directive(name)
- return string.sub(name, -1) == '!'
-end
-
---@private
----@param match TSQueryMatch
+---@param pattern_i integer
+---@param predicates vim.treesitter.query.ProcessedPredicate[]
+---@param captures table<integer, TSNode[]>
---@param source integer|string
-function Query:match_preds(match, source)
- local _, pattern = match:info()
- local preds = self.info.patterns[pattern]
-
- if not preds then
- return true
- end
-
- local captures = match:captures()
-
- for _, pred in pairs(preds) do
- -- Here we only want to return if a predicate DOES NOT match, and
- -- continue on the other case. This way unknown predicates will not be considered,
- -- which allows some testing and easier user extensibility (#12173).
- -- Also, tree-sitter strips the leading # from predicates for us.
- local is_not = false
-
- -- Skip over directives... they will get processed after all the predicates.
- if not is_directive(pred[1]) then
- local pred_name = pred[1]
- if pred_name:match('^not%-') then
- pred_name = pred_name:sub(5)
- is_not = true
- end
-
- local handler = predicate_handlers[pred_name]
-
- if not handler then
- error(string.format('No handler for %s', pred[1]))
- return false
- end
-
- local pred_matches = handler(captures, pattern, source, pred)
+---@return boolean whether the predicates match
+function Query:_match_predicates(predicates, pattern_i, captures, source)
+ for _, predicate in ipairs(predicates) do
+ local processed_name = predicate[1]
+ local should_match = predicate[2]
+ local orig_predicate = predicate[3]
+
+ local handler = predicate_handlers[processed_name]
+ if not handler then
+ error(string.format('No handler for %s', orig_predicate[1]))
+ return false
+ end
- if not xor(is_not, pred_matches) then
- return false
- end
+ local does_match = handler(captures, pattern_i, source, orig_predicate)
+ if does_match ~= should_match then
+ return false
end
end
return true
end
---@private
----@param match TSQueryMatch
+---@param pattern_i integer
+---@param directives vim.treesitter.query.ProcessedDirective[]
+---@param source integer|string
+---@param captures table<integer, TSNode[]>
---@return vim.treesitter.query.TSMetadata metadata
-function Query:apply_directives(match, source)
+function Query:_apply_directives(directives, pattern_i, captures, source)
---@type vim.treesitter.query.TSMetadata
local metadata = {}
- local _, pattern = match:info()
- local preds = self.info.patterns[pattern]
-
- if not preds then
- return metadata
- end
- local captures = match:captures()
-
- for _, pred in pairs(preds) do
- if is_directive(pred[1]) then
- local handler = directive_handlers[pred[1]]
-
- if not handler then
- error(string.format('No handler for %s', pred[1]))
- end
+ for _, directive in pairs(directives) do
+ local handler = directive_handlers[directive[1]]
- handler(captures, pattern, source, pred, metadata)
+ if not handler then
+ error(string.format('No handler for %s', directive[1]))
end
+
+ handler(captures, pattern_i, source, directive, metadata)
end
return metadata
@@ -812,26 +926,22 @@ local function value_or_node_range(start, stop, node)
return start, stop
end
---- @param match TSQueryMatch
---- @return integer
-local function match_id_hash(_, match)
- return (match:info())
-end
-
---- Iterate over all captures from all matches inside {node}
+--- Iterates over all captures from all matches in {node}.
---
---- {source} is needed if the query contains predicates; then the caller
+--- {source} is required 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 relevant). {start} and {stop} 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 {stop} row values are used from the given node.
---
---- The iterator returns four values: a numeric id identifying the capture,
---- the captured node, metadata from any directives processing the match,
---- and the match itself.
---- The following example shows how to get captures by name:
+--- The iterator returns four values:
+--- 1. the numeric id identifying the capture
+--- 2. the captured node
+--- 3. metadata from any directives processing the match
+--- 4. the match itself
---
+--- Example: how to get captures by name:
--- ```lua
--- for id, node, metadata, match in query:iter_captures(tree:root(), bufnr, first, last) do
--- local name = query.captures[id] -- name of the capture in the query
@@ -847,8 +957,8 @@ end
---@param start? integer Starting line for the search. Defaults to `node:start()`.
---@param stop? integer Stopping line for the search (end-exclusive). Defaults to `node:end_()`.
---
----@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch):
---- capture id, capture node, metadata, match
+---@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch, TSTree):
+--- capture id, capture node, metadata, match, tree
---
---@note Captures are only returned if the query pattern of a specific capture contained predicates.
function Query:iter_captures(node, source, start, stop)
@@ -858,10 +968,14 @@ function Query:iter_captures(node, source, start, stop)
start, stop = value_or_node_range(start, stop, node)
+ -- Copy the tree to ensure it is valid during the entire lifetime of the iterator
+ local tree = node:tree():copy()
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, { match_limit = 256 })
- local apply_directives = memoize(match_id_hash, self.apply_directives, true)
- local match_preds = memoize(match_id_hash, self.match_preds, true)
+ -- For faster checks that a match is not in the cache.
+ local highest_cached_match_id = -1
+ ---@type table<integer, vim.treesitter.query.TSMetadata>
+ local match_cache = {}
local function iter(end_line)
local capture, captured_node, match = cursor:next_capture()
@@ -870,18 +984,39 @@ function Query:iter_captures(node, source, start, stop)
return
end
- if not match_preds(self, match, source) then
- local match_id = match:info()
- cursor:remove_match(match_id)
- if end_line and captured_node:range() > end_line then
- return nil, captured_node, nil, nil
- end
- return iter(end_line) -- tail call: try next match
+ local match_id, pattern_i = match:info()
+
+ --- @type vim.treesitter.query.TSMetadata
+ local metadata
+ if match_id <= highest_cached_match_id then
+ metadata = match_cache[match_id]
end
- local metadata = apply_directives(self, match, source)
+ if not metadata then
+ metadata = {}
+
+ local processed_pattern = self._processed_patterns[pattern_i]
+ if processed_pattern then
+ local captures = match:captures()
- return capture, captured_node, metadata, match
+ local predicates = processed_pattern.predicates
+ if not self:_match_predicates(predicates, pattern_i, captures, source) then
+ cursor:remove_match(match_id)
+ if end_line and captured_node:range() > end_line then
+ return nil, captured_node, nil, nil
+ end
+ return iter(end_line) -- tail call: try next match
+ end
+
+ local directives = processed_pattern.directives
+ metadata = self:_apply_directives(directives, pattern_i, captures, source)
+ end
+
+ highest_cached_match_id = math.max(highest_cached_match_id, match_id)
+ match_cache[match_id] = metadata
+ end
+
+ return capture, captured_node, metadata, match, tree
end
return iter
end
@@ -903,7 +1038,7 @@ end
--- -- `node` was captured by the `name` capture in the match
---
--- local node_data = metadata[id] -- Node level metadata
---- ... use the info here ...
+--- -- ... use the info here ...
--- end
--- end
--- end
@@ -922,7 +1057,7 @@ end
--- (last) node instead of the full list of matching nodes. This option is only for backward
--- compatibility and will be removed in a future release.
---
----@return (fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata): pattern id, match, metadata
+---@return (fun(): integer, table<integer, TSNode[]>, vim.treesitter.query.TSMetadata, TSTree): pattern id, match, metadata, tree
function Query:iter_matches(node, source, start, stop, opts)
opts = opts or {}
opts.match_limit = opts.match_limit or 256
@@ -933,6 +1068,8 @@ function Query:iter_matches(node, source, start, stop, opts)
start, stop = value_or_node_range(start, stop, node)
+ -- Copy the tree to ensure it is valid during the entire lifetime of the iterator
+ local tree = node:tree():copy()
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts)
local function iter()
@@ -942,17 +1079,22 @@ function Query:iter_matches(node, source, start, stop, opts)
return
end
- local match_id, pattern = match:info()
+ local match_id, pattern_i = match:info()
+ local processed_pattern = self._processed_patterns[pattern_i]
+ local captures = match:captures()
- if not self:match_preds(match, source) then
- cursor:remove_match(match_id)
- return iter() -- tail call: try next match
+ --- @type vim.treesitter.query.TSMetadata
+ local metadata = {}
+ if processed_pattern then
+ local predicates = processed_pattern.predicates
+ if not self:_match_predicates(predicates, pattern_i, captures, source) then
+ cursor:remove_match(match_id)
+ return iter() -- tail call: try next match
+ end
+ local directives = processed_pattern.directives
+ metadata = self:_apply_directives(directives, pattern_i, captures, source)
end
- local metadata = self:apply_directives(match, source)
-
- local captures = match:captures()
-
if opts.all == false then
-- Convert the match table into the old buggy version for backward
-- compatibility. This is slow, but we only do it when the caller explicitly opted into it by
@@ -961,11 +1103,11 @@ function Query:iter_matches(node, source, start, stop, opts)
for k, v in pairs(captures or {}) do
old_match[k] = v[#v]
end
- return pattern, old_match, metadata
+ return pattern_i, old_match, metadata
end
-- TODO(lewis6991): create a new function that returns {match, metadata}
- return pattern, captures, metadata
+ return pattern_i, captures, metadata, tree
end
return iter
end
diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua
index b4e4098b91..8b6f1a61ee 100644
--- a/runtime/lua/vim/uri.lua
+++ b/runtime/lua/vim/uri.lua
@@ -15,7 +15,7 @@ local PATTERNS = {
rfc2396 = "^A-Za-z0-9%-_.!~*'()",
-- RFC 2732
-- https://tools.ietf.org/html/rfc2732
- rfc2732 = "^A-Za-z0-9%-_.!~*'()[]",
+ rfc2732 = "^A-Za-z0-9%-_.!~*'()%[%]",
-- RFC 3986
-- https://tools.ietf.org/html/rfc3986#section-2.2
rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/",
@@ -60,9 +60,10 @@ end
---@param path string Path to file
---@return string URI
function M.uri_from_fname(path)
- local volume_path, fname = path:match('^([a-zA-Z]:)(.*)') ---@type string?
+ local volume_path, fname = path:match('^([a-zA-Z]:)(.*)') ---@type string?, string?
local is_windows = volume_path ~= nil
if is_windows then
+ assert(fname)
path = volume_path .. M.uri_encode(fname:gsub('\\', '/'))
else
path = M.uri_encode(path)
@@ -111,7 +112,7 @@ function M.uri_to_fname(uri)
uri = M.uri_decode(uri)
--TODO improve this.
if is_windows_file_uri(uri) then
- uri = uri:gsub('^file:/+', ''):gsub('/', '\\')
+ uri = uri:gsub('^file:/+', ''):gsub('/', '\\') --- @type string
else
uri = uri:gsub('^file:/+', '/') ---@type string
end
diff --git a/runtime/lua/vim/version.lua b/runtime/lua/vim/version.lua
index d64ef98d2d..06c54ac033 100644
--- a/runtime/lua/vim/version.lua
+++ b/runtime/lua/vim/version.lua
@@ -227,8 +227,7 @@ end
---@field to? vim.Version
local VersionRange = {}
---- @private
----
+---@nodoc
---@param version string|vim.Version
function VersionRange:has(version)
if type(version) == 'string' then
diff --git a/runtime/lua/vim/vimhelp.lua b/runtime/lua/vim/vimhelp.lua
index 5579cc0174..a494d311b1 100644
--- a/runtime/lua/vim/vimhelp.lua
+++ b/runtime/lua/vim/vimhelp.lua
@@ -7,7 +7,7 @@ local M = {}
--- Note: {patterns} is assumed to be sorted by occurrence in the file.
--- @param patterns {start:string,stop:string,match:string}[]
function M.highlight_groups(patterns)
- local ns = vim.api.nvim_create_namespace('vimhelp')
+ local ns = vim.api.nvim_create_namespace('nvim.vimhelp')
vim.api.nvim_buf_clear_namespace(0, ns, 0, -1)
local save_cursor = vim.fn.getcurpos()