aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2024-05-24 19:18:11 +0000
committerJosh Rahm <joshuarahm@gmail.com>2024-05-24 19:18:11 +0000
commitff7ed8f586589d620a806c3758fac4a47a8e7e15 (patch)
tree729bbcb92231538fa61dab6c3d890b025484b7f5 /runtime/lua/vim
parent376914f419eb08fdf4c1a63a77e1f035898a0f10 (diff)
parent28c04948a1c887a1cc0cb64de79fa32631700466 (diff)
downloadrneovim-ff7ed8f586589d620a806c3758fac4a47a8e7e15.tar.gz
rneovim-ff7ed8f586589d620a806c3758fac4a47a8e7e15.tar.bz2
rneovim-ff7ed8f586589d620a806c3758fac4a47a8e7e15.zip
Merge remote-tracking branch 'upstream/master' into mix_20240309
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/_comment.lua263
-rw-r--r--runtime/lua/vim/_defaults.lua562
-rw-r--r--runtime/lua/vim/_editor.lua126
-rw-r--r--runtime/lua/vim/_inspector.lua6
-rw-r--r--runtime/lua/vim/_meta/api.lua128
-rw-r--r--runtime/lua/vim/_meta/api_keysets.lua13
-rw-r--r--runtime/lua/vim/_meta/api_keysets_extra.lua2
-rw-r--r--runtime/lua/vim/_meta/base64.lua4
-rw-r--r--runtime/lua/vim/_meta/builtin.lua28
-rw-r--r--runtime/lua/vim/_meta/builtin_types.lua8
-rw-r--r--runtime/lua/vim/_meta/diff.lua85
-rw-r--r--runtime/lua/vim/_meta/lpeg.lua7
-rw-r--r--runtime/lua/vim/_meta/options.lua60
-rw-r--r--runtime/lua/vim/_meta/re.lua12
-rw-r--r--runtime/lua/vim/_meta/spell.lua6
-rw-r--r--runtime/lua/vim/_meta/vimfn.lua116
-rw-r--r--runtime/lua/vim/_meta/vvars.lua8
-rw-r--r--runtime/lua/vim/_options.lua2
-rw-r--r--runtime/lua/vim/_system.lua3
-rw-r--r--runtime/lua/vim/_watch.lua18
-rw-r--r--runtime/lua/vim/deprecated/health.lua42
-rw-r--r--runtime/lua/vim/diagnostic.lua323
-rw-r--r--runtime/lua/vim/filetype.lua204
-rw-r--r--runtime/lua/vim/filetype/detect.lua46
-rw-r--r--runtime/lua/vim/fs.lua310
-rw-r--r--runtime/lua/vim/func.lua5
-rw-r--r--runtime/lua/vim/func/_memoize.lua8
-rw-r--r--runtime/lua/vim/health.lua224
-rw-r--r--runtime/lua/vim/health/health.lua409
-rw-r--r--runtime/lua/vim/highlight.lua62
-rw-r--r--runtime/lua/vim/iter.lua265
-rw-r--r--runtime/lua/vim/keymap.lua40
-rw-r--r--runtime/lua/vim/loader.lua2
-rw-r--r--runtime/lua/vim/lsp.lua517
-rw-r--r--runtime/lua/vim/lsp/_meta/protocol.lua187
-rw-r--r--runtime/lua/vim/lsp/buf.lua242
-rw-r--r--runtime/lua/vim/lsp/client.lua95
-rw-r--r--runtime/lua/vim/lsp/codelens.lua18
-rw-r--r--runtime/lua/vim/lsp/handlers.lua111
-rw-r--r--runtime/lua/vim/lsp/health.lua23
-rw-r--r--runtime/lua/vim/lsp/inlay_hint.lua239
-rw-r--r--runtime/lua/vim/lsp/protocol.lua7
-rw-r--r--runtime/lua/vim/lsp/rpc.lua116
-rw-r--r--runtime/lua/vim/lsp/semantic_tokens.lua27
-rw-r--r--runtime/lua/vim/lsp/util.lua129
-rw-r--r--runtime/lua/vim/provider/health.lua792
-rw-r--r--runtime/lua/vim/secure.lua2
-rw-r--r--runtime/lua/vim/shared.lua145
-rw-r--r--runtime/lua/vim/snippet.lua94
-rw-r--r--runtime/lua/vim/termcap.lua2
-rw-r--r--runtime/lua/vim/text.lua4
-rw-r--r--runtime/lua/vim/treesitter.lua4
-rw-r--r--runtime/lua/vim/treesitter/_fold.lua207
-rw-r--r--runtime/lua/vim/treesitter/_meta.lua53
-rw-r--r--runtime/lua/vim/treesitter/_query_linter.lua2
-rw-r--r--runtime/lua/vim/treesitter/dev.lua2
-rw-r--r--runtime/lua/vim/treesitter/health.lua2
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua77
-rw-r--r--runtime/lua/vim/treesitter/language.lua3
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua98
-rw-r--r--runtime/lua/vim/treesitter/query.lua225
-rw-r--r--runtime/lua/vim/ui.lua41
62 files changed, 4707 insertions, 2154 deletions
diff --git a/runtime/lua/vim/_comment.lua b/runtime/lua/vim/_comment.lua
new file mode 100644
index 0000000000..044cd69716
--- /dev/null
+++ b/runtime/lua/vim/_comment.lua
@@ -0,0 +1,263 @@
+---@nodoc
+---@class vim._comment.Parts
+---@field left string Left part of comment
+---@field right string Right part of comment
+
+--- Get 'commentstring' at cursor
+---@param ref_position integer[]
+---@return string
+local function get_commentstring(ref_position)
+ local buf_cs = vim.bo.commentstring
+
+ local has_ts_parser, ts_parser = pcall(vim.treesitter.get_parser)
+ if not has_ts_parser then
+ return buf_cs
+ end
+
+ -- Try to get 'commentstring' associated with local tree-sitter language.
+ -- This is useful for injected languages (like markdown with code blocks).
+ local row, col = ref_position[1] - 1, ref_position[2]
+ local ref_range = { row, col, row, col + 1 }
+
+ -- - Get 'commentstring' from the deepest LanguageTree which both contains
+ -- reference range and has valid 'commentstring' (meaning it has at least
+ -- one associated 'filetype' with valid 'commentstring').
+ -- In simple cases using `parser:language_for_range()` would be enough, but
+ -- it fails for languages without valid 'commentstring' (like 'comment').
+ local ts_cs, res_level = nil, 0
+
+ ---@param lang_tree vim.treesitter.LanguageTree
+ local function traverse(lang_tree, level)
+ if not lang_tree:contains(ref_range) then
+ return
+ end
+
+ local lang = lang_tree:lang()
+ local filetypes = vim.treesitter.language.get_filetypes(lang)
+ for _, ft in ipairs(filetypes) do
+ local cur_cs = vim.filetype.get_option(ft, 'commentstring')
+ if cur_cs ~= '' and level > res_level then
+ ts_cs = cur_cs
+ end
+ end
+
+ for _, child_lang_tree in pairs(lang_tree:children()) do
+ traverse(child_lang_tree, level + 1)
+ end
+ end
+ traverse(ts_parser, 1)
+
+ return ts_cs or buf_cs
+end
+
+--- Compute comment parts from 'commentstring'
+---@param ref_position integer[]
+---@return vim._comment.Parts
+local function get_comment_parts(ref_position)
+ local cs = get_commentstring(ref_position)
+
+ if cs == nil or cs == '' then
+ vim.api.nvim_echo({ { "Option 'commentstring' is empty.", 'WarningMsg' } }, true, {})
+ return { left = '', right = '' }
+ end
+
+ if not (type(cs) == 'string' and cs:find('%%s') ~= nil) then
+ error(vim.inspect(cs) .. " is not a valid 'commentstring'.")
+ end
+
+ -- Structure of 'commentstring': <left part> <%s> <right part>
+ local left, right = cs:match('^(.-)%%s(.-)$')
+ return { left = left, right = right }
+end
+
+--- Make a function that checks if a line is commented
+---@param parts vim._comment.Parts
+---@return fun(line: string): boolean
+local function make_comment_check(parts)
+ local l_esc, r_esc = vim.pesc(parts.left), vim.pesc(parts.right)
+
+ -- Commented line has the following structure:
+ -- <whitespace> <trimmed left> <anything> <trimmed right> <whitespace>
+ local regex = '^%s-' .. vim.trim(l_esc) .. '.*' .. vim.trim(r_esc) .. '%s-$'
+
+ return function(line)
+ return line:find(regex) ~= nil
+ end
+end
+
+--- Compute comment-related information about lines
+---@param lines string[]
+---@param parts vim._comment.Parts
+---@return string indent
+---@return boolean is_commented
+local function get_lines_info(lines, parts)
+ local comment_check = make_comment_check(parts)
+
+ local is_commented = true
+ local indent_width = math.huge
+ ---@type string
+ local indent
+
+ for _, l in ipairs(lines) do
+ -- Update lines indent: minimum of all indents except blank lines
+ local _, indent_width_cur, indent_cur = l:find('^(%s*)')
+
+ -- Ignore blank lines completely when making a decision
+ if indent_width_cur < l:len() then
+ -- NOTE: Copying actual indent instead of recreating it with `indent_width`
+ -- allows to handle both tabs and spaces
+ if indent_width_cur < indent_width then
+ ---@diagnostic disable-next-line:cast-local-type
+ indent_width, indent = indent_width_cur, indent_cur
+ end
+
+ -- Update comment info: commented if every non-blank line is commented
+ if is_commented then
+ is_commented = comment_check(l)
+ end
+ end
+ end
+
+ -- `indent` can still be `nil` in case all `lines` are empty
+ return indent or '', is_commented
+end
+
+--- Compute whether a string is blank
+---@param x string
+---@return boolean is_blank
+local function is_blank(x)
+ return x:find('^%s*$') ~= nil
+end
+
+--- Make a function which comments a line
+---@param parts vim._comment.Parts
+---@param indent string
+---@return fun(line: string): string
+local function make_comment_function(parts, indent)
+ local prefix, nonindent_start, suffix = indent .. parts.left, indent:len() + 1, parts.right
+ local blank_comment = indent .. vim.trim(parts.left) .. vim.trim(parts.right)
+
+ return function(line)
+ if is_blank(line) then
+ return blank_comment
+ end
+ return prefix .. line:sub(nonindent_start) .. suffix
+ end
+end
+
+--- Make a function which uncomments a line
+---@param parts vim._comment.Parts
+---@return fun(line: string): string
+local function make_uncomment_function(parts)
+ local l_esc, r_esc = vim.pesc(parts.left), vim.pesc(parts.right)
+ local regex = '^(%s*)' .. l_esc .. '(.*)' .. r_esc .. '(%s-)$'
+ local regex_trimmed = '^(%s*)' .. vim.trim(l_esc) .. '(.*)' .. vim.trim(r_esc) .. '(%s-)$'
+
+ return function(line)
+ -- Try regex with exact comment parts first, fall back to trimmed parts
+ local indent, new_line, trail = line:match(regex)
+ if new_line == nil then
+ indent, new_line, trail = line:match(regex_trimmed)
+ end
+
+ -- Return original if line is not commented
+ if new_line == nil then
+ return line
+ end
+
+ -- Prevent trailing whitespace
+ if is_blank(new_line) then
+ indent, trail = '', ''
+ end
+
+ return indent .. new_line .. trail
+ end
+end
+
+--- Comment/uncomment buffer range
+---@param line_start integer
+---@param line_end integer
+---@param ref_position? integer[]
+local function toggle_lines(line_start, line_end, ref_position)
+ ref_position = ref_position or { line_start, 0 }
+ local parts = get_comment_parts(ref_position)
+ local lines = vim.api.nvim_buf_get_lines(0, line_start - 1, line_end, false)
+ local indent, is_comment = get_lines_info(lines, parts)
+
+ local f = is_comment and make_uncomment_function(parts) or make_comment_function(parts, indent)
+
+ -- Direct `nvim_buf_set_lines()` essentially removes both regular and
+ -- extended marks (squashes to empty range at either side of the region)
+ -- inside region. Use 'lockmarks' to preserve regular marks.
+ -- Preserving extmarks is not a universally good thing to do:
+ -- - Good for non-highlighting in text area extmarks (like showing signs).
+ -- - Debatable for highlighting in text area (like LSP semantic tokens).
+ -- Mostly because it causes flicker as highlighting is preserved during
+ -- comment toggling.
+ package.loaded['vim._comment']._lines = vim.tbl_map(f, lines)
+ local lua_cmd = string.format(
+ 'vim.api.nvim_buf_set_lines(0, %d, %d, false, package.loaded["vim._comment"]._lines)',
+ line_start - 1,
+ line_end
+ )
+ vim.cmd.lua({ lua_cmd, mods = { lockmarks = true } })
+ package.loaded['vim._comment']._lines = nil
+end
+
+--- Operator which toggles user-supplied range of lines
+---@param mode string?
+---|"'line'"
+---|"'char'"
+---|"'block'"
+local function operator(mode)
+ -- Used without arguments as part of expression mapping. Otherwise it is
+ -- called as 'operatorfunc'.
+ if mode == nil then
+ vim.o.operatorfunc = "v:lua.require'vim._comment'.operator"
+ return 'g@'
+ end
+
+ -- Compute target range
+ local mark_from, mark_to = "'[", "']"
+ local lnum_from, col_from = vim.fn.line(mark_from), vim.fn.col(mark_from)
+ local lnum_to, col_to = vim.fn.line(mark_to), vim.fn.col(mark_to)
+
+ -- Do nothing if "from" mark is after "to" (like in empty textobject)
+ if (lnum_from > lnum_to) or (lnum_from == lnum_to and col_from > col_to) then
+ return
+ end
+
+ -- NOTE: use cursor position as reference for possibly computing local
+ -- tree-sitter-based 'commentstring'. Recompute every time for a proper
+ -- dot-repeat. In Visual and sometimes Normal mode it uses start position.
+ toggle_lines(lnum_from, lnum_to, vim.api.nvim_win_get_cursor(0))
+ return ''
+end
+
+--- Select contiguous commented lines at cursor
+local function textobject()
+ local lnum_cur = vim.fn.line('.')
+ local parts = get_comment_parts({ lnum_cur, vim.fn.col('.') })
+ local comment_check = make_comment_check(parts)
+
+ if not comment_check(vim.fn.getline(lnum_cur)) then
+ return
+ end
+
+ -- Compute commented range
+ local lnum_from = lnum_cur
+ while (lnum_from >= 2) and comment_check(vim.fn.getline(lnum_from - 1)) do
+ lnum_from = lnum_from - 1
+ end
+
+ local lnum_to = lnum_cur
+ local n_lines = vim.api.nvim_buf_line_count(0)
+ while (lnum_to <= n_lines - 1) and comment_check(vim.fn.getline(lnum_to + 1)) do
+ lnum_to = lnum_to + 1
+ end
+
+ -- Select range linewise for operator to act upon
+ vim.cmd('normal! ' .. lnum_from .. 'GV' .. lnum_to .. 'G')
+end
+
+return { operator = operator, textobject = textobject, toggle_lines = toggle_lines }
diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua
index 91baee1a1e..5b964b84a0 100644
--- a/runtime/lua/vim/_defaults.lua
+++ b/runtime/lua/vim/_defaults.lua
@@ -1,3 +1,31 @@
+--- Default user commands
+do
+ vim.api.nvim_create_user_command('Inspect', function(cmd)
+ if cmd.bang then
+ vim.print(vim.inspect_pos())
+ else
+ vim.show_pos()
+ end
+ end, { desc = 'Inspect highlights and extmarks at the cursor', bang = true })
+
+ vim.api.nvim_create_user_command('InspectTree', function(cmd)
+ if cmd.mods ~= '' or cmd.count ~= 0 then
+ local count = cmd.count ~= 0 and cmd.count or ''
+ local new = cmd.mods ~= '' and 'new' or 'vnew'
+
+ vim.treesitter.inspect_tree({
+ command = ('%s %s%s'):format(cmd.mods, count, new),
+ })
+ else
+ vim.treesitter.inspect_tree()
+ end
+ end, { desc = 'Inspect treesitter language tree for buffer', count = true })
+
+ vim.api.nvim_create_user_command('EditQuery', function(cmd)
+ vim.treesitter.query.edit(cmd.fargs[1])
+ end, { desc = 'Edit treesitter query', nargs = '?' })
+end
+
--- Default mappings
do
--- Default maps for * and # in visual mode.
@@ -50,42 +78,126 @@ do
--- See |&-default|
vim.keymap.set('n', '&', ':&&<CR>', { desc = ':help &-default' })
- --- Use Q in visual mode to execute a macro on each line of the selection. #21422
+ --- Use Q in Visual mode to execute a macro on each line of the selection. #21422
+ --- This only make sense in linewise Visual mode. #28287
---
--- Applies to @x and includes @@ too.
vim.keymap.set(
'x',
'Q',
- ':normal! @<C-R>=reg_recorded()<CR><CR>',
- { silent = true, desc = ':help v_Q-default' }
+ "mode() == 'V' ? ':normal! @<C-R>=reg_recorded()<CR><CR>' : 'Q'",
+ { silent = true, expr = true, desc = ':help v_Q-default' }
)
vim.keymap.set(
'x',
'@',
- "':normal! @'.getcharstr().'<CR>'",
+ "mode() == 'V' ? ':normal! @'.getcharstr().'<CR>' : '@'",
{ silent = true, expr = true, desc = ':help v_@-default' }
)
- --- Map |gx| to call |vim.ui.open| on the identifier under the cursor
+
+ --- Map |gx| to call |vim.ui.open| on the <cfile> at cursor.
do
local function do_open(uri)
- local _, err = vim.ui.open(uri)
- if err then
- vim.notify(err, vim.log.levels.ERROR)
+ local cmd, err = vim.ui.open(uri)
+ local rv = cmd and cmd:wait(1000) or nil
+ if cmd and rv and rv.code ~= 0 then
+ err = ('vim.ui.open: command %s (%d): %s'):format(
+ (rv.code == 124 and 'timeout' or 'failed'),
+ rv.code,
+ vim.inspect(cmd.cmd)
+ )
end
+ return err
end
local gx_desc =
'Opens filepath or URI under cursor with the system handler (file explorer, web browser, …)'
vim.keymap.set({ 'n' }, 'gx', function()
- do_open(vim.fn.expand('<cfile>'))
+ local err = do_open(require('vim.ui')._get_url())
+ if err then
+ vim.notify(err, vim.log.levels.ERROR)
+ end
end, { desc = gx_desc })
vim.keymap.set({ 'x' }, 'gx', function()
local lines =
vim.fn.getregion(vim.fn.getpos('.'), vim.fn.getpos('v'), { type = vim.fn.mode() })
-- Trim whitespace on each line and concatenate.
- do_open(table.concat(vim.iter(lines):map(vim.trim):totable()))
+ local err = do_open(table.concat(vim.iter(lines):map(vim.trim):totable()))
+ if err then
+ vim.notify(err, vim.log.levels.ERROR)
+ end
end, { desc = gx_desc })
end
+
+ --- Default maps for built-in commenting.
+ ---
+ --- See |gc-default| and |gcc-default|.
+ do
+ local operator_rhs = function()
+ return require('vim._comment').operator()
+ end
+ vim.keymap.set({ 'n', 'x' }, 'gc', operator_rhs, { expr = true, desc = 'Toggle comment' })
+
+ local line_rhs = function()
+ return require('vim._comment').operator() .. '_'
+ end
+ vim.keymap.set('n', 'gcc', line_rhs, { expr = true, desc = 'Toggle comment line' })
+
+ local textobject_rhs = function()
+ require('vim._comment').textobject()
+ end
+ vim.keymap.set({ 'o' }, 'gc', textobject_rhs, { desc = 'Comment textobject' })
+ end
+
+ --- Default maps for LSP functions.
+ ---
+ --- These are mapped unconditionally to avoid different behavior depending on whether an LSP
+ --- client is attached. If no client is attached, or if a server does not support a capability, an
+ --- error message is displayed rather than exhibiting different behavior.
+ ---
+ --- See |grr|, |grn|, |gra|, |i_CTRL-S|.
+ do
+ vim.keymap.set('n', 'grn', function()
+ vim.lsp.buf.rename()
+ end, { desc = 'vim.lsp.buf.rename()' })
+
+ vim.keymap.set({ 'n', 'x' }, 'gra', function()
+ vim.lsp.buf.code_action()
+ end, { desc = 'vim.lsp.buf.code_action()' })
+
+ vim.keymap.set('n', 'grr', function()
+ vim.lsp.buf.references()
+ end, { desc = 'vim.lsp.buf.references()' })
+
+ vim.keymap.set('i', '<C-S>', function()
+ vim.lsp.buf.signature_help()
+ end, { desc = 'vim.lsp.buf.signature_help()' })
+ end
+
+ --- Map [d and ]d to move to the previous/next diagnostic. Map <C-W>d to open a floating window
+ --- for the diagnostic under the cursor.
+ ---
+ --- See |[d-default|, |]d-default|, and |CTRL-W_d-default|.
+ do
+ vim.keymap.set('n', ']d', function()
+ vim.diagnostic.goto_next({ float = false })
+ end, { desc = 'Jump to the next diagnostic' })
+
+ vim.keymap.set('n', '[d', function()
+ vim.diagnostic.goto_prev({ float = false })
+ end, { desc = 'Jump to the previous diagnostic' })
+
+ vim.keymap.set('n', '<C-W>d', function()
+ vim.diagnostic.open_float()
+ end, { desc = 'Show diagnostics under the cursor' })
+
+ vim.keymap.set(
+ 'n',
+ '<C-W><C-D>',
+ '<C-W>d',
+ { remap = true, desc = 'Show diagnostics under the cursor' }
+ )
+ end
end
--- Default menus
@@ -93,7 +205,6 @@ do
--- Right click popup menu
-- TODO VimScript, no l10n
vim.cmd([[
- aunmenu *
vnoremenu PopUp.Cut "+x
vnoremenu PopUp.Copy "+y
anoremenu PopUp.Paste "+gP
@@ -102,6 +213,7 @@ do
nnoremenu PopUp.Select\ All ggVG
vnoremenu PopUp.Select\ All gg0oG$
inoremenu PopUp.Select\ All <C-Home><C-O>VG
+ anoremenu PopUp.Inspect <Cmd>Inspect<CR>
anoremenu PopUp.-1- <Nop>
anoremenu PopUp.How-to\ disable\ mouse <Cmd>help disable-mouse<CR>
]])
@@ -128,7 +240,7 @@ do
end
local info = vim.api.nvim_get_chan_info(vim.bo[args.buf].channel)
local argv = info.argv or {}
- if #argv == 1 and argv[1] == vim.o.shell then
+ if table.concat(argv, ' ') == vim.o.shell then
vim.api.nvim_buf_delete(args.buf, { force = true })
end
end,
@@ -136,8 +248,9 @@ do
vim.api.nvim_create_autocmd('TermRequest', {
group = nvim_terminal_augroup,
- desc = 'Respond to OSC foreground/background color requests',
+ desc = 'Handles OSC foreground/background color requests',
callback = function(args)
+ --- @type integer
local channel = vim.bo[args.buf].channel
if channel == 0 then
return
@@ -181,232 +294,146 @@ do
return
end
vim.v.swapchoice = 'e' -- Choose "(E)dit".
- vim.notify(('W325: Ignoring swapfile from Nvim process %d'):format(info.pid))
+ vim.notify(
+ ('W325: Ignoring swapfile from Nvim process %d'):format(info.pid),
+ vim.log.levels.WARN
+ )
end,
})
-end
-
--- Only do the following when the TUI is attached
-local tty = nil
-for _, ui in ipairs(vim.api.nvim_list_uis()) do
- if ui.chan == 1 and ui.stdout_tty then
- tty = ui
- break
- end
-end
-if tty then
- 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
- -- Don't do anything if option is already set
- return
- end
-
- -- Wait until Nvim is finished starting to set the option to ensure the
- -- OptionSet event fires.
- if vim.v.vim_did_enter == 1 then
- vim.o[option] = value
- else
- vim.api.nvim_create_autocmd('VimEnter', {
- group = group,
- once = true,
- nested = true,
- callback = function()
- setoption(option, value)
- end,
- })
+ -- Only do the following when the TUI is attached
+ local tty = nil
+ for _, ui in ipairs(vim.api.nvim_list_uis()) do
+ if ui.chan == 1 and ui.stdout_tty then
+ tty = ui
+ break
end
end
- --- Guess value of 'background' based on terminal color.
- ---
- --- We write Operating System Command (OSC) 11 to the terminal to request the
- --- terminal's background color. We then wait for a response. If the response
- --- matches `rgba:RRRR/GGGG/BBBB/AAAA` where R, G, B, and A are hex digits, then
- --- compute the luminance[1] of the RGB color and classify it as light/dark
- --- accordingly. Note that the color components may have anywhere from one to
- --- four hex digits, and require scaling accordingly as values out of 4, 8, 12,
- --- or 16 bits. Also note the A(lpha) component is optional, and is parsed but
- --- ignored in the calculations.
- ---
- --- [1] https://en.wikipedia.org/wiki/Luma_%28video%29
- do
- --- Parse a string of hex characters as a color.
- ---
- --- The string can contain 1 to 4 hex characters. The returned value is
- --- between 0.0 and 1.0 (inclusive) representing the intensity of the color.
- ---
- --- For instance, if only a single hex char "a" is used, then this function
- --- returns 0.625 (10 / 16), while a value of "aa" would return 0.664 (170 /
- --- 256).
+ if tty then
+ 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 c string Color as a string of hex chars
- --- @return number? Intensity of the color
- local function parsecolor(c)
- if #c == 0 or #c > 4 then
- return nil
+ --- @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
+ -- Don't do anything if option is already set
+ return
end
- local val = tonumber(c, 16)
- if not val then
- return nil
+ -- Wait until Nvim is finished starting to set the option to ensure the
+ -- OptionSet event fires.
+ if vim.v.vim_did_enter == 1 then
+ --- @diagnostic disable-next-line:no-unknown
+ vim.o[option] = value
+ else
+ vim.api.nvim_create_autocmd('VimEnter', {
+ group = group,
+ once = true,
+ nested = true,
+ callback = function()
+ setoption(option, value)
+ end,
+ })
end
-
- local max = tonumber(string.rep('f', #c), 16)
- return val / max
end
- --- Parse an OSC 11 response
- ---
- --- Either of the two formats below are accepted:
- ---
- --- OSC 11 ; rgb:<red>/<green>/<blue>
- ---
- --- or
+ --- Guess value of 'background' based on terminal color.
---
- --- OSC 11 ; rgba:<red>/<green>/<blue>/<alpha>
+ --- We write Operating System Command (OSC) 11 to the terminal to request the
+ --- terminal's background color. We then wait for a response. If the response
+ --- matches `rgba:RRRR/GGGG/BBBB/AAAA` where R, G, B, and A are hex digits, then
+ --- compute the luminance[1] of the RGB color and classify it as light/dark
+ --- accordingly. Note that the color components may have anywhere from one to
+ --- four hex digits, and require scaling accordingly as values out of 4, 8, 12,
+ --- or 16 bits. Also note the A(lpha) component is optional, and is parsed but
+ --- ignored in the calculations.
---
- --- where
- ---
- --- <red>, <green>, <blue>, <alpha> := h | hh | hhh | hhhh
- ---
- --- The alpha component is ignored, if present.
- ---
- --- @param resp string OSC 11 response
- --- @return string? Red component
- --- @return string? Green component
- --- @return string? Blue component
- local function parseosc11(resp)
- local r, g, b
- r, g, b = resp:match('^\027%]11;rgb:(%x+)/(%x+)/(%x+)$')
- if not r and not g and not b then
- local a
- r, g, b, a = resp:match('^\027%]11;rgba:(%x+)/(%x+)/(%x+)/(%x+)$')
- if not a or #a > 4 then
- return nil, nil, nil
+ --- [1] https://en.wikipedia.org/wiki/Luma_%28video%29
+ do
+ --- Parse a string of hex characters as a color.
+ ---
+ --- The string can contain 1 to 4 hex characters. The returned value is
+ --- between 0.0 and 1.0 (inclusive) representing the intensity of the color.
+ ---
+ --- For instance, if only a single hex char "a" is used, then this function
+ --- returns 0.625 (10 / 16), while a value of "aa" would return 0.664 (170 /
+ --- 256).
+ ---
+ --- @param c string Color as a string of hex chars
+ --- @return number? Intensity of the color
+ local function parsecolor(c)
+ if #c == 0 or #c > 4 then
+ return nil
end
- end
- if r and g and b and #r <= 4 and #g <= 4 and #b <= 4 then
- return r, g, b
- end
-
- return nil, nil, nil
- end
-
- local timer = assert(vim.uv.new_timer())
-
- local id = vim.api.nvim_create_autocmd('TermResponse', {
- group = group,
- nested = true,
- callback = function(args)
- local resp = args.data ---@type string
- local r, g, b = parseosc11(resp)
- if r and g and b then
- local rr = parsecolor(r)
- local gg = parsecolor(g)
- local bb = parsecolor(b)
-
- 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)
- end
-
- return true
+ local val = tonumber(c, 16)
+ if not val then
+ return nil
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()
+ local max = tonumber(string.rep('f', #c), 16)
+ return val / max
end
- end)
- end
- --- If the TUI (term_has_truecolor) was able to determine that the host
- --- terminal supports truecolor, enable 'termguicolors'. Otherwise, query the
- --- terminal (using both XTGETTCAP and SGR + DECRQSS). If the terminal's
- --- response indicates that it does support truecolor enable 'termguicolors',
- --- but only if the user has not already disabled it.
- do
- if tty.rgb then
- -- The TUI was able to determine truecolor support
- setoption('termguicolors', true)
- else
- local caps = {} ---@type table<string, boolean>
- require('vim.termcap').query({ 'Tc', 'RGB', 'setrgbf', 'setrgbb' }, function(cap, found)
- if not found then
- return
+ --- Parse an OSC 11 response
+ ---
+ --- Either of the two formats below are accepted:
+ ---
+ --- OSC 11 ; rgb:<red>/<green>/<blue>
+ ---
+ --- or
+ ---
+ --- OSC 11 ; rgba:<red>/<green>/<blue>/<alpha>
+ ---
+ --- where
+ ---
+ --- <red>, <green>, <blue>, <alpha> := h | hh | hhh | hhhh
+ ---
+ --- The alpha component is ignored, if present.
+ ---
+ --- @param resp string OSC 11 response
+ --- @return string? Red component
+ --- @return string? Green component
+ --- @return string? Blue component
+ local function parseosc11(resp)
+ local r, g, b
+ r, g, b = resp:match('^\027%]11;rgb:(%x+)/(%x+)/(%x+)$')
+ if not r and not g and not b then
+ local a
+ r, g, b, a = resp:match('^\027%]11;rgba:(%x+)/(%x+)/(%x+)/(%x+)$')
+ if not a or #a > 4 then
+ return nil, nil, nil
+ end
end
- caps[cap] = true
- if caps.Tc or caps.RGB or (caps.setrgbf and caps.setrgbb) then
- setoption('termguicolors', true)
+ if r and g and b and #r <= 4 and #g <= 4 and #b <= 4 then
+ return r, g, b
end
- end)
- local timer = assert(vim.uv.new_timer())
+ return nil, nil, nil
+ end
- -- Arbitrary colors to set in the SGR sequence
- local r = 1
- local g = 2
- local b = 3
+ local timer = assert(vim.uv.new_timer())
local id = vim.api.nvim_create_autocmd('TermResponse', {
group = group,
nested = true,
callback = function(args)
local resp = args.data ---@type string
- local decrqss = resp:match('^\027P1%$r([%d;:]+)m$')
-
- if decrqss then
- -- The DECRQSS SGR response first contains attributes separated by
- -- semicolons, followed by the SGR itself with parameters separated
- -- by colons. Some terminals include "0" in the attribute list
- -- unconditionally; others do not. Our SGR sequence did not set any
- -- attributes, so there should be no attributes in the list.
- local attrs = vim.split(decrqss, ';')
- if #attrs ~= 1 and (#attrs ~= 2 or attrs[1] ~= '0') then
- return true
- end
-
- -- The returned SGR sequence should begin with 48:2
- local sgr = attrs[#attrs]:match('^48:2:([%d:]+)$')
- if not sgr then
- return true
- end
-
- -- The remaining elements of the SGR sequence should be the 3 colors
- -- we set. Some terminals also include an additional parameter
- -- (which can even be empty!), so handle those cases as well
- local params = vim.split(sgr, ':')
- if #params ~= 3 and (#params ~= 4 or (params[1] ~= '' and params[1] ~= '1')) then
- return true
- end
-
- if
- tonumber(params[#params - 2]) == r
- and tonumber(params[#params - 1]) == g
- and tonumber(params[#params]) == b
- then
- setoption('termguicolors', true)
+ local r, g, b = parseosc11(resp)
+ if r and g and b then
+ local rr = parsecolor(r)
+ local gg = parsecolor(g)
+ local bb = parsecolor(b)
+
+ 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)
end
return true
@@ -414,15 +441,7 @@ if tty then
end,
})
- -- Write SGR followed by DECRQSS. This sets the background color then
- -- immediately asks the terminal what the background color is. If the
- -- terminal responds to the DECRQSS with the same SGR sequence that we
- -- sent then the terminal supports truecolor.
- local decrqss = '\027P$qm\027\\'
- if os.getenv('TMUX') then
- decrqss = string.format('\027Ptmux;%s\027\\', decrqss:gsub('\027', '\027\027'))
- end
- io.stdout:write(string.format('\027[48;2;%d;%d;%dm%s', r, g, b, decrqss))
+ io.stdout:write('\027]11;?\007')
timer:start(1000, 0, function()
-- Delete the autocommand if no response was received
@@ -436,5 +455,114 @@ if tty then
end
end)
end
+
+ --- If the TUI (term_has_truecolor) was able to determine that the host
+ --- terminal supports truecolor, enable 'termguicolors'. Otherwise, query the
+ --- terminal (using both XTGETTCAP and SGR + DECRQSS). If the terminal's
+ --- response indicates that it does support truecolor enable 'termguicolors',
+ --- but only if the user has not already disabled it.
+ do
+ if tty.rgb then
+ -- The TUI was able to determine truecolor support
+ setoption('termguicolors', true)
+ else
+ local caps = {} ---@type table<string, boolean>
+ require('vim.termcap').query({ 'Tc', 'RGB', 'setrgbf', 'setrgbb' }, function(cap, found)
+ if not found then
+ return
+ end
+
+ caps[cap] = true
+ if caps.Tc or caps.RGB or (caps.setrgbf and caps.setrgbb) then
+ setoption('termguicolors', true)
+ end
+ end)
+
+ local timer = assert(vim.uv.new_timer())
+
+ -- Arbitrary colors to set in the SGR sequence
+ local r = 1
+ local g = 2
+ local b = 3
+
+ local id = vim.api.nvim_create_autocmd('TermResponse', {
+ group = group,
+ nested = true,
+ callback = function(args)
+ local resp = args.data ---@type string
+ local decrqss = resp:match('^\027P1%$r([%d;:]+)m$')
+
+ if decrqss then
+ -- The DECRQSS SGR response first contains attributes separated by
+ -- semicolons, followed by the SGR itself with parameters separated
+ -- by colons. Some terminals include "0" in the attribute list
+ -- unconditionally; others do not. Our SGR sequence did not set any
+ -- attributes, so there should be no attributes in the list.
+ local attrs = vim.split(decrqss, ';')
+ if #attrs ~= 1 and (#attrs ~= 2 or attrs[1] ~= '0') then
+ return false
+ end
+
+ -- The returned SGR sequence should begin with 48:2
+ local sgr = attrs[#attrs]:match('^48:2:([%d:]+)$')
+ if not sgr then
+ return false
+ end
+
+ -- The remaining elements of the SGR sequence should be the 3 colors
+ -- we set. Some terminals also include an additional parameter
+ -- (which can even be empty!), so handle those cases as well
+ local params = vim.split(sgr, ':')
+ if #params ~= 3 and (#params ~= 4 or (params[1] ~= '' and params[1] ~= '1')) then
+ return true
+ end
+
+ if
+ tonumber(params[#params - 2]) == r
+ and tonumber(params[#params - 1]) == g
+ and tonumber(params[#params]) == b
+ then
+ setoption('termguicolors', true)
+ end
+
+ return true
+ end
+ end,
+ })
+
+ -- Write SGR followed by DECRQSS. This sets the background color then
+ -- immediately asks the terminal what the background color is. If the
+ -- terminal responds to the DECRQSS with the same SGR sequence that we
+ -- sent then the terminal supports truecolor.
+ local decrqss = '\027P$qm\027\\'
+ if os.getenv('TMUX') then
+ decrqss = string.format('\027Ptmux;%s\027\\', decrqss:gsub('\027', '\027\027'))
+ end
+ -- Reset attributes first, as other code may have set attributes.
+ io.stdout:write(string.format('\027[0m\027[48;2;%d;%d;%dm%s', r, g, b, decrqss))
+
+ 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
+ end
+ end
+end
+
+--- Default options
+do
+ --- Default 'grepprg' to ripgrep if available.
+ if vim.fn.executable('rg') == 1 then
+ -- Use -uu to make ripgrep not check ignore files/skip dot-files
+ vim.o.grepprg = 'rg --vimgrep -uu '
+ vim.o.grepformat = '%f:%l:%c:%m'
end
end
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
index 6cf77b4648..5e9be509c8 100644
--- a/runtime/lua/vim/_editor.lua
+++ b/runtime/lua/vim/_editor.lua
@@ -74,7 +74,6 @@ vim.log = {
--- Examples:
---
--- ```lua
----
--- local on_exit = function(obj)
--- print(obj.code)
--- print(obj.signal)
@@ -122,6 +121,7 @@ vim.log = {
--- asynchronously. Receives SystemCompleted object, see return of SystemObj:wait().
---
--- @return vim.SystemObj Object with the fields:
+--- - cmd (string[]) Command name and args
--- - pid (integer) Process ID
--- - wait (fun(timeout: integer|nil): SystemCompleted) Wait for the process to complete. Upon
--- timeout the process is sent the KILL signal (9) and the exit code is set to 124. Cannot
@@ -655,11 +655,14 @@ local on_key_cbs = {} --- @type table<integer,function>
---
---@note {fn} will be removed on error.
---@note {fn} will not be cleared by |nvim_buf_clear_namespace()|
----@note {fn} will receive the keys after mappings have been evaluated
---
----@param fn fun(key: string)? Function invoked on every key press. |i_CTRL-V|
---- Passing in nil when {ns_id} is specified removes the
---- callback associated with namespace {ns_id}.
+---@param fn fun(key: string, typed: string)?
+--- Function invoked on every key press. |i_CTRL-V|
+--- {key} is the key after mappings have been applied, and
+--- {typed} is the key(s) before mappings are applied, which
+--- may be empty if {key} is produced by non-typed keys.
+--- When {fn} is nil and {ns_id} is specified, the callback
+--- associated with namespace {ns_id} is removed.
---@param ns_id integer? Namespace ID. If nil or 0, generates and returns a
--- new |nvim_create_namespace()| id.
---
@@ -685,11 +688,11 @@ end
--- Executes the on_key callbacks.
---@private
-function vim._on_key(char)
+function vim._on_key(buf, typed_buf)
local failed_ns_ids = {}
local failed_messages = {}
for k, v in pairs(on_key_cbs) do
- local ok, err_msg = pcall(v, char)
+ local ok, err_msg = pcall(v, buf, typed_buf)
if not ok then
vim.on_key(nil, k)
table.insert(failed_ns_ids, k)
@@ -1028,6 +1031,42 @@ function vim._cs_remote(rcid, server_addr, connect_error, args)
}
end
+do
+ local function truncated_echo(msg)
+ -- Truncate message to avoid hit-enter-prompt
+ local max_width = vim.o.columns * math.max(vim.o.cmdheight - 1, 0) + vim.v.echospace
+ local msg_truncated = string.sub(msg, 1, max_width)
+ vim.api.nvim_echo({ { msg_truncated, 'WarningMsg' } }, true, {})
+ end
+
+ local notified = false
+
+ function vim._truncated_echo_once(msg)
+ if not notified then
+ truncated_echo(msg)
+ notified = true
+ return true
+ end
+ return false
+ end
+end
+
+--- This is basically the same as debug.traceback(), except the full paths are shown.
+local function traceback()
+ local level = 4
+ local backtrace = { 'stack traceback:' }
+ while true do
+ local info = debug.getinfo(level, 'Sl')
+ if not info then
+ break
+ end
+ local msg = (' %s:%s'):format(info.source:sub(2), info.currentline)
+ table.insert(backtrace, msg)
+ level = level + 1
+ end
+ return table.concat(backtrace, '\n')
+end
+
--- Shows a deprecation message to the user.
---
---@param name string Deprecated feature (function, API, etc.).
@@ -1039,55 +1078,46 @@ end
---
---@return string|nil # Deprecated message, or nil if no message was shown.
function vim.deprecate(name, alternative, version, plugin, backtrace)
- vim.validate {
- name = { name, 'string' },
- alternative = { alternative, 'string', true },
- version = { version, 'string', true },
- plugin = { plugin, 'string', true },
- }
plugin = plugin or 'Nvim'
-
- -- Only issue warning if feature is hard-deprecated as specified by MAINTAIN.md.
- -- e.g., when planned to be removed in version = '0.12' (soft-deprecated since 0.10-dev),
- -- show warnings since 0.11, including 0.11-dev (hard_deprecated_since = 0.11-dev).
if plugin == 'Nvim' then
- local current_version = vim.version() ---@type vim.Version
- local removal_version = assert(vim.version.parse(version))
- local is_hard_deprecated ---@type boolean
-
- if removal_version.minor > 0 then
- local hard_deprecated_since = assert(vim.version._version({
- major = removal_version.major,
- minor = removal_version.minor - 1,
- patch = 0,
- prerelease = 'dev', -- Show deprecation warnings in devel (nightly) version as well
- }))
- is_hard_deprecated = (current_version >= hard_deprecated_since)
- else
- -- Assume there will be no next minor version before bumping up the major version;
- -- therefore we can always show a warning.
- assert(removal_version.minor == 0, vim.inspect(removal_version))
- is_hard_deprecated = true
- end
+ require('vim.deprecated.health').add(name, version, traceback(), alternative)
+
+ -- Only issue warning if feature is hard-deprecated as specified by MAINTAIN.md.
+ -- Example: if removal_version is 0.12 (soft-deprecated since 0.10-dev), show warnings starting at
+ -- 0.11, including 0.11-dev
+ local major, minor = version:match('(%d+)%.(%d+)')
+ major, minor = tonumber(major), tonumber(minor)
+ local hard_deprecated_since = string.format('nvim-%d.%d', major, minor - 1)
+ -- Assume there will be no next minor version before bumping up the major version
+ local is_hard_deprecated = minor == 0 or vim.fn.has(hard_deprecated_since) == 1
if not is_hard_deprecated then
return
end
- end
- local msg = ('%s is deprecated'):format(name)
- msg = alternative and ('%s, use %s instead.'):format(msg, alternative) or (msg .. '.')
- msg = ('%s%s\nThis feature will be removed in %s version %s'):format(
- msg,
- (plugin == 'Nvim' and ' :help deprecated' or ''),
- plugin,
- version
- )
- local displayed = vim.notify_once(msg, vim.log.levels.WARN)
- if displayed and backtrace ~= false then
- vim.notify(debug.traceback('', 2):sub(2), vim.log.levels.WARN)
+ local msg = ('%s is deprecated. Run ":checkhealth vim.deprecated" for more information'):format(
+ name
+ )
+
+ local displayed = vim._truncated_echo_once(msg)
+ return displayed and msg or nil
+ else
+ vim.validate {
+ name = { name, 'string' },
+ alternative = { alternative, 'string', true },
+ version = { version, 'string', true },
+ plugin = { plugin, 'string', true },
+ }
+
+ local msg = ('%s is deprecated'):format(name)
+ msg = alternative and ('%s, use %s instead.'):format(msg, alternative) or (msg .. '.')
+ msg = ('%s\nFeature will be removed in %s %s'):format(msg, plugin, version)
+ local displayed = vim.notify_once(msg, vim.log.levels.WARN)
+ if displayed and backtrace ~= false then
+ vim.notify(debug.traceback('', 2):sub(2), vim.log.levels.WARN)
+ end
+ return displayed and msg or nil
end
- return displayed and msg or nil
end
require('vim._options')
diff --git a/runtime/lua/vim/_inspector.lua b/runtime/lua/vim/_inspector.lua
index afbd6211cd..f5d1640c82 100644
--- a/runtime/lua/vim/_inspector.lua
+++ b/runtime/lua/vim/_inspector.lua
@@ -55,8 +55,8 @@ function vim.inspect_pos(bufnr, row, col, filter)
bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr
local results = {
- treesitter = {},
- syntax = {},
+ treesitter = {}, --- @type table[]
+ syntax = {}, --- @type table[]
extmarks = {},
semantic_tokens = {},
buffer = bufnr,
@@ -93,7 +93,7 @@ function vim.inspect_pos(bufnr, row, col, filter)
end
-- namespace id -> name map
- local nsmap = {}
+ local nsmap = {} --- @type table<integer,string>
for name, id in pairs(vim.api.nvim_get_namespaces()) do
nsmap[id] = name
end
diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua
index cb4c8749b8..6edf2a5a96 100644
--- a/runtime/lua/vim/_meta/api.lua
+++ b/runtime/lua/vim/_meta/api.lua
@@ -14,14 +14,21 @@ function vim.api.nvim__buf_debug_extmarks(buffer, keys, dot) end
--- @private
--- @param buffer integer
---- @param first integer
---- @param last integer
-function vim.api.nvim__buf_redraw_range(buffer, first, last) end
+--- @return table<string,any>
+function vim.api.nvim__buf_stats(buffer) end
--- @private
---- @param buffer integer
+--- EXPERIMENTAL: this API may change in the future.
+---
+--- Sets info for the completion item at the given index. If the info text was
+--- shown in a window, returns the window and buffer ids, or empty dict if not
+--- shown.
+---
+--- @param index integer Completion candidate index
+--- @param opts vim.api.keyset.complete_set Optional parameters.
+--- • info: (string) info text.
--- @return table<string,any>
-function vim.api.nvim__buf_stats(buffer) end
+function vim.api.nvim__complete_set(index, opts) end
--- @private
--- @return string
@@ -93,6 +100,32 @@ function vim.api.nvim__inspect_cell(grid, row, col) end
function vim.api.nvim__invalidate_glyph_cache() end
--- @private
+--- EXPERIMENTAL: this API may change in the future.
+---
+--- Instruct Nvim to redraw various components.
+---
+--- @param opts vim.api.keyset.redraw Optional parameters.
+--- • win: Target a specific `window-ID` as described below.
+--- • buf: Target a specific buffer number as described below.
+--- • flush: Update the screen with pending updates.
+--- • valid: When present mark `win`, `buf`, or all windows for
+--- redraw. When `true`, only redraw changed lines (useful for
+--- decoration providers). When `false`, forcefully redraw.
+--- • range: Redraw a range in `buf`, the buffer in `win` or the
+--- current buffer (useful for decoration providers). Expects a
+--- tuple `[first, last]` with the first and last line number of
+--- the range, 0-based end-exclusive `api-indexing`.
+--- • cursor: Immediately update cursor position on the screen in
+--- `win` or the current window.
+--- • statuscolumn: Redraw the 'statuscolumn' in `buf`, `win` or
+--- all windows.
+--- • statusline: Redraw the 'statusline' in `buf`, `win` or all
+--- windows.
+--- • winbar: Redraw the 'winbar' in `buf`, `win` or all windows.
+--- • tabline: Redraw the 'tabline'.
+function vim.api.nvim__redraw(opts) end
+
+--- @private
--- @return any[]
function vim.api.nvim__runtime_inspect() end
@@ -111,6 +144,36 @@ function vim.api.nvim__stats() end
--- @return any
function vim.api.nvim__unpack(str) end
+--- @private
+--- EXPERIMENTAL: this API will change in the future.
+---
+--- Scopes a namespace to the a window, so extmarks in the namespace will be
+--- active only in the given window.
+---
+--- @param window integer Window handle, or 0 for current window
+--- @param ns_id integer Namespace
+--- @return boolean
+function vim.api.nvim__win_add_ns(window, ns_id) end
+
+--- @private
+--- EXPERIMENTAL: this API will change in the future.
+---
+--- Unscopes a namespace (un-binds it from the given scope).
+---
+--- @param window integer Window handle, or 0 for current window
+--- @param ns_id integer the namespace to remove
+--- @return boolean
+function vim.api.nvim__win_del_ns(window, ns_id) end
+
+--- @private
+--- EXPERIMENTAL: this API will change in the future.
+---
+--- Gets the namespace scopes for a given window.
+---
+--- @param window integer Window handle, or 0 for current window
+--- @return integer[]
+function vim.api.nvim__win_get_ns(window) end
+
--- Adds a highlight to buffer.
---
--- Useful for plugins that dynamically generate highlights to a buffer (like
@@ -623,8 +686,8 @@ function vim.api.nvim_buf_line_count(buffer) end
--- • url: A URL to associate with this extmark. In the TUI, the
--- OSC 8 control sequence is used to generate a clickable
--- hyperlink to this URL.
---- • scoped: boolean that indicates that the extmark should only
---- be displayed in the namespace scope. (experimental)
+--- • scoped: boolean (EXPERIMENTAL) enables "scoping" for the
+--- extmark. See `nvim__win_add_ns()`
--- @return integer
function vim.api.nvim_buf_set_extmark(buffer, ns_id, line, col, opts) end
@@ -669,7 +732,7 @@ function vim.api.nvim_buf_set_lines(buffer, start, end_, strict_indexing, replac
--- @return boolean
function vim.api.nvim_buf_set_mark(buffer, name, line, col, opts) end
---- Sets the full file name for a buffer
+--- Sets the full file name for a buffer, like `:file_f`
---
--- @param buffer integer Buffer handle, or 0 for current buffer
--- @param name string Buffer name
@@ -822,16 +885,6 @@ function vim.api.nvim_command(command) end
--- @return string
function vim.api.nvim_command_output(command) end
---- Set info for the completion candidate index. if the info was shown in a
---- window, then the window and buffer ids are returned for further
---- customization. If the text was not shown, an empty dict is returned.
----
---- @param index integer the completion candidate index
---- @param opts vim.api.keyset.complete_set Optional parameters.
---- • info: (string) info text.
---- @return table<string,any>
-function vim.api.nvim_complete_set(index, opts) end
-
--- Create or get an autocommand group `autocmd-groups`.
---
--- To get an existing group id, do:
@@ -897,8 +950,8 @@ function vim.api.nvim_create_augroup(name, opts) end
--- • callback (function|string) optional: Lua function (or
--- Vimscript function name, if string) called when the event(s)
--- is triggered. Lua callback can return a truthy value (not
---- `false` or `nil`) to delete the autocommand. Receives a
---- table argument with these keys:
+--- `false` or `nil`) to delete the autocommand. Receives one
+--- argument, a table with these keys: *event-args*
--- • id: (number) autocommand id
--- • event: (string) name of the triggered event
--- `autocmd-events`
@@ -907,7 +960,7 @@ function vim.api.nvim_create_augroup(name, opts) end
--- • buf: (number) expanded value of <abuf>
--- • file: (string) expanded value of <afile>
--- • data: (any) arbitrary data passed from
---- `nvim_exec_autocmds()`
+--- `nvim_exec_autocmds()` *event-data*
--- • command (string) optional: Vim command to execute on event.
--- Cannot be used with {callback}
--- • once (boolean) optional: defaults to false. Run the
@@ -1718,9 +1771,8 @@ function vim.api.nvim_open_term(buffer, opts) end
--- • footer_pos: Footer position. Must be set with `footer`
--- option. Value can be one of "left", "center", or "right".
--- Default is `"left"`.
---- • noautocmd: If true then no buffer-related autocommand
---- events such as `BufEnter`, `BufLeave` or `BufWinEnter` may
---- fire from calling this function.
+--- • noautocmd: If true then all autocommands are blocked for
+--- the duration of the call.
--- • fixed: If true when anchor is NW or SW, the float window
--- would be kept fixed even if the window would be truncated.
--- • hide: If true the floating window will be hidden.
@@ -2092,13 +2144,6 @@ function vim.api.nvim_tabpage_set_var(tabpage, name, value) end
--- @param win integer Window handle, must already belong to {tabpage}
function vim.api.nvim_tabpage_set_win(tabpage, win) end
---- Adds the namespace scope to the window.
----
---- @param window integer Window handle, or 0 for current window
---- @param ns_id integer the namespace to add
---- @return boolean
-function vim.api.nvim_win_add_ns(window, ns_id) end
-
--- Calls a function with window as temporary current window.
---
--- @param window integer Window handle, or 0 for current window
@@ -2151,12 +2196,6 @@ function vim.api.nvim_win_get_cursor(window) end
--- @return integer
function vim.api.nvim_win_get_height(window) end
---- Gets all the namespaces scopes associated with a window.
----
---- @param window integer Window handle, or 0 for current window
---- @return integer[]
-function vim.api.nvim_win_get_ns(window) end
-
--- Gets the window number
---
--- @param window integer Window handle, or 0 for current window
@@ -2210,24 +2249,17 @@ function vim.api.nvim_win_hide(window) end
--- @return boolean
function vim.api.nvim_win_is_valid(window) end
---- Removes the namespace scope from the window.
----
---- @param window integer Window handle, or 0 for current window
---- @param ns_id integer the namespace to remove
---- @return boolean
-function vim.api.nvim_win_remove_ns(window, ns_id) end
-
--- Sets the current buffer in a window, without side effects
---
--- @param window integer Window handle, or 0 for current window
--- @param buffer integer Buffer handle
function vim.api.nvim_win_set_buf(window, buffer) end
---- Configures window layout. Currently only for floating and external windows
---- (including changing a split window to those layouts).
+--- Configures window layout. Cannot be used to move the last window in a
+--- tabpage to a different one.
---
---- When reconfiguring a floating window, absent option keys will not be
---- changed. `row`/`col` and `relative` must be reconfigured together.
+--- When reconfiguring a window, absent option keys will not be changed.
+--- `row`/`col` and `relative` must be reconfigured together.
---
--- @param window integer Window handle, or 0 for current window
--- @param config vim.api.keyset.win_config Map defining the window configuration, see `nvim_open_win()`
diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua
index 37e4372196..f7cd92a3b2 100644
--- a/runtime/lua/vim/_meta/api_keysets.lua
+++ b/runtime/lua/vim/_meta/api_keysets.lua
@@ -207,6 +207,18 @@ error('Cannot require a meta file')
--- @field buf? integer
--- @field filetype? string
+--- @class vim.api.keyset.redraw
+--- @field flush? boolean
+--- @field cursor? boolean
+--- @field valid? boolean
+--- @field statuscolumn? boolean
+--- @field statusline? boolean
+--- @field tabline? boolean
+--- @field winbar? boolean
+--- @field range? any[]
+--- @field win? integer
+--- @field buf? integer
+
--- @class vim.api.keyset.runtime
--- @field is_lua? boolean
--- @field do_source? boolean
@@ -253,7 +265,6 @@ error('Cannot require a meta file')
--- @field undo_restore? boolean
--- @field url? string
--- @field scoped? boolean
---- @field _subpriority? integer
--- @class vim.api.keyset.user_command
--- @field addr? any
diff --git a/runtime/lua/vim/_meta/api_keysets_extra.lua b/runtime/lua/vim/_meta/api_keysets_extra.lua
index d61dd2c02f..76b56b04e7 100644
--- a/runtime/lua/vim/_meta/api_keysets_extra.lua
+++ b/runtime/lua/vim/_meta/api_keysets_extra.lua
@@ -124,7 +124,7 @@ error('Cannot require a meta file')
--- @field commalist boolean
--- @field flaglist boolean
--- @field was_set boolean
---- @field last_set_id integer
+--- @field last_set_sid integer
--- @field last_set_linenr integer
--- @field last_set_chan integer
--- @field type 'string'|'boolean'|'number'
diff --git a/runtime/lua/vim/_meta/base64.lua b/runtime/lua/vim/_meta/base64.lua
index f25b4af234..8ba59e1703 100644
--- a/runtime/lua/vim/_meta/base64.lua
+++ b/runtime/lua/vim/_meta/base64.lua
@@ -3,11 +3,11 @@
--- Encode {str} using Base64.
---
--- @param str string String to encode
---- @return string Encoded string
+--- @return string : Encoded string
function vim.base64.encode(str) end
--- Decode a Base64 encoded string.
---
--- @param str string Base64 encoded string
---- @return string Decoded string
+--- @return string : Decoded string
function vim.base64.decode(str) end
diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua
index 9a67667f02..75737bd040 100644
--- a/runtime/lua/vim/_meta/builtin.lua
+++ b/runtime/lua/vim/_meta/builtin.lua
@@ -115,19 +115,19 @@ function vim.stricmp(a, b) end
--- Convert UTF-32 or UTF-16 {index} to byte index. If {use_utf16} is not
--- supplied, it defaults to false (use UTF-32). Returns the byte index.
---
---- Invalid UTF-8 and NUL is treated like by |vim.str_byteindex()|.
+--- Invalid UTF-8 and NUL is treated like in |vim.str_utfindex()|.
--- An {index} in the middle of a UTF-16 sequence is rounded upwards to
--- the end of that sequence.
--- @param str string
---- @param index number
---- @param use_utf16? any
+--- @param index integer
+--- @param use_utf16? boolean
function vim.str_byteindex(str, index, use_utf16) end
--- Gets a list of the starting byte positions of each UTF-8 codepoint in the given string.
---
--- Embedded NUL bytes are treated as terminating the string.
--- @param str string
---- @return table
+--- @return integer[]
function vim.str_utf_pos(str) end
--- Gets the distance (in bytes) from the starting byte of the codepoint (character) that {index}
@@ -148,8 +148,8 @@ function vim.str_utf_pos(str) end
--- ```
---
--- @param str string
---- @param index number
---- @return number
+--- @param index integer
+--- @return integer
function vim.str_utf_start(str, index) end
--- Gets the distance (in bytes) from the last byte of the codepoint (character) that {index} points
@@ -168,8 +168,8 @@ function vim.str_utf_start(str, index) end
--- ```
---
--- @param str string
---- @param index number
---- @return number
+--- @param index integer
+--- @return integer
function vim.str_utf_end(str, index) end
--- Convert byte index to UTF-32 and UTF-16 indices. If {index} is not
@@ -180,7 +180,7 @@ function vim.str_utf_end(str, index) end
--- {index} in the middle of a UTF-8 sequence is rounded upwards to the end of
--- that sequence.
--- @param str string
---- @param index? number
+--- @param index? integer
--- @return integer UTF-32 index
--- @return integer UTF-16 index
function vim.str_utfindex(str, index) end
@@ -193,15 +193,14 @@ function vim.str_utfindex(str, index) end
--- can accept, see ":Man 3 iconv".
---
--- @param str string Text to convert
---- @param from number Encoding of {str}
---- @param to number Target encoding
---- @param opts? table<string,any>
---- @return string|nil Converted string if conversion succeeds, `nil` otherwise.
+--- @param from string Encoding of {str}
+--- @param to string Target encoding
+--- @return string? : Converted string if conversion succeeds, `nil` otherwise.
function vim.iconv(str, from, to, opts) end
--- Schedules {fn} to be invoked soon by the main event-loop. Useful
--- to avoid |textlock| or other temporary restrictions.
---- @param fn function
+--- @param fn fun()
function vim.schedule(fn) end
--- Wait for {time} in milliseconds until {callback} returns `true`.
@@ -215,7 +214,6 @@ function vim.schedule(fn) end
--- Examples:
---
--- ```lua
----
--- ---
--- -- Wait for 100 ms, allowing other events to process
--- vim.wait(100, function() end)
diff --git a/runtime/lua/vim/_meta/builtin_types.lua b/runtime/lua/vim/_meta/builtin_types.lua
index 0bbc3e9bc8..9f0d2e7038 100644
--- a/runtime/lua/vim/_meta/builtin_types.lua
+++ b/runtime/lua/vim/_meta/builtin_types.lua
@@ -127,3 +127,11 @@
--- @field skipcol integer
--- @field topfill integer
--- @field topline integer
+
+--- @class vim.fn.getscriptinfo.ret
+--- @field autoload false
+--- @field functions? string[]
+--- @field name string
+--- @field sid string
+--- @field variables? table<string, any>
+--- @field version 1
diff --git a/runtime/lua/vim/_meta/diff.lua b/runtime/lua/vim/_meta/diff.lua
index f265139448..617bc87f59 100644
--- a/runtime/lua/vim/_meta/diff.lua
+++ b/runtime/lua/vim/_meta/diff.lua
@@ -1,5 +1,46 @@
---@meta
+--- Optional parameters:
+--- @class vim.diff.Opts
+--- @inlinedoc
+---
+--- Invoked for each hunk in the diff. Return a negative number
+--- to cancel the callback for any remaining hunks.
+--- Arguments:
+--- - `start_a` (`integer`): Start line of hunk in {a}.
+--- - `count_a` (`integer`): Hunk size in {a}.
+--- - `start_b` (`integer`): Start line of hunk in {b}.
+--- - `count_b` (`integer`): Hunk size in {b}.
+--- @field on_hunk fun(start_a: integer, count_a: integer, start_b: integer, count_b: integer): integer
+---
+--- Form of the returned diff:
+--- - `unified`: String in unified format.
+--- - `indices`: Array of hunk locations.
+--- Note: This option is ignored if `on_hunk` is used.
+--- (default: `'unified'`)
+--- @field result_type 'unified'|'indices'
+---
+--- Run linematch on the resulting hunks from xdiff. When integer, only hunks
+--- upto this size in lines are run through linematch.
+--- Requires `result_type = indices`, ignored otherwise.
+--- @field linematch boolean|integer
+---
+--- Diff algorithm to use. Values:
+--- - `myers`: the default algorithm
+--- - `minimal`: spend extra time to generate the smallest possible diff
+--- - `patience`: patience diff algorithm
+--- - `histogram`: histogram diff algorithm
+--- (default: `'myers'`)
+--- @field algorithm 'myers'|'minimal'|'patience'|'histogram'
+--- @field ctxlen integer Context length
+--- @field interhunkctxlen integer Inter hunk context length
+--- @field ignore_whitespace boolean Ignore whitespace
+--- @field ignore_whitespace_change boolean Ignore whitespace change
+--- @field ignore_whitespace_change_at_eol boolean Ignore whitespace change at end-of-line.
+--- @field ignore_cr_at_eol boolean Ignore carriage return at end-of-line
+--- @field ignore_blank_lines boolean Ignore blank lines
+--- @field indent_heuristic boolean Use the indent heuristic for the internal diff library.
+
-- luacheck: no unused args
--- Run diff on strings {a} and {b}. Any indices returned by this function,
@@ -24,47 +65,7 @@
---
---@param a string First string to compare
---@param b string Second string to compare
----@param opts table<string,any> Optional parameters:
---- - `on_hunk` (callback):
---- Invoked for each hunk in the diff. Return a negative number
---- to cancel the callback for any remaining hunks.
---- Args:
---- - `start_a` (integer): Start line of hunk in {a}.
---- - `count_a` (integer): Hunk size in {a}.
---- - `start_b` (integer): Start line of hunk in {b}.
---- - `count_b` (integer): Hunk size in {b}.
---- - `result_type` (string): Form of the returned diff:
---- - "unified": (default) String in unified format.
---- - "indices": Array of hunk locations.
---- Note: This option is ignored if `on_hunk` is used.
---- - `linematch` (boolean|integer): Run linematch on the resulting hunks
---- from xdiff. When integer, only hunks upto this size in
---- lines are run through linematch. Requires `result_type = indices`,
---- ignored otherwise.
---- - `algorithm` (string):
---- Diff algorithm to use. Values:
---- - "myers" the default algorithm
---- - "minimal" spend extra time to generate the
---- smallest possible diff
---- - "patience" patience diff algorithm
---- - "histogram" histogram diff algorithm
---- - `ctxlen` (integer): Context length
---- - `interhunkctxlen` (integer):
---- Inter hunk context length
---- - `ignore_whitespace` (boolean):
---- Ignore whitespace
---- - `ignore_whitespace_change` (boolean):
---- Ignore whitespace change
---- - `ignore_whitespace_change_at_eol` (boolean)
---- Ignore whitespace change at end-of-line.
---- - `ignore_cr_at_eol` (boolean)
---- Ignore carriage return at end-of-line
---- - `ignore_blank_lines` (boolean)
---- Ignore blank lines
---- - `indent_heuristic` (boolean):
---- Use the indent heuristic for the internal
---- diff library.
----
----@return string|table|nil
+---@param opts vim.diff.Opts
+---@return string|integer[][]?
--- See {opts.result_type}. `nil` if {opts.on_hunk} is given.
function vim.diff(a, b, opts) end
diff --git a/runtime/lua/vim/_meta/lpeg.lua b/runtime/lua/vim/_meta/lpeg.lua
index 1ce40f3340..73b3375c82 100644
--- a/runtime/lua/vim/_meta/lpeg.lua
+++ b/runtime/lua/vim/_meta/lpeg.lua
@@ -6,11 +6,10 @@ error('Cannot require a meta file')
-- with types being renamed to include the vim namespace and with some descriptions made less verbose.
--- @brief <pre>help
---- LPeg is a pattern-matching library for Lua, based on
---- Parsing Expression Grammars (https://bford.info/packrat/) (PEGs).
+--- LPeg is a pattern-matching library for Lua, based on Parsing Expression
+--- Grammars (PEGs). https://bford.info/packrat/
---
---- *lua-lpeg*
---- *vim.lpeg.Pattern*
+--- *lua-lpeg* *vim.lpeg.Pattern*
--- The LPeg library for parsing expression grammars is included as `vim.lpeg`
--- (https://www.inf.puc-rio.br/~roberto/lpeg/).
---
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
index 757720d8fb..428b7c4d4f 100644
--- a/runtime/lua/vim/_meta/options.lua
+++ b/runtime/lua/vim/_meta/options.lua
@@ -11,10 +11,9 @@ vim.bo = vim.bo
---@field [integer] vim.wo
vim.wo = vim.wo
---- Allow CTRL-_ in Insert and Command-line mode. This is default off, to
---- avoid that users that accidentally type CTRL-_ instead of SHIFT-_ get
---- into reverse Insert mode, and don't know how to get out. See
---- 'revins'.
+--- Allow CTRL-_ in Insert mode. This is default off, to avoid that users
+--- that accidentally type CTRL-_ instead of SHIFT-_ get into reverse
+--- Insert mode, and don't know how to get out. See 'revins'.
---
--- @type boolean
vim.o.allowrevins = false
@@ -975,7 +974,7 @@ vim.bo.comments = vim.o.comments
vim.bo.com = vim.bo.comments
--- A template for a comment. The "%s" in the value is replaced with the
---- comment text. For example, C uses "/*%s*/". Currently only used to
+--- comment text. For example, C uses "/*%s*/". Used for `commenting` and to
--- add markers for folding, see `fold-marker`.
---
--- @type string
@@ -2626,6 +2625,8 @@ vim.go.gd = vim.go.gdefault
--- This is a scanf-like string that uses the same format as the
--- 'errorformat' option: see `errorformat`.
---
+--- If ripgrep ('grepprg') is available, this option defaults to `%f:%l:%c:%m`.
+---
--- @type string
vim.o.grepformat = "%f:%l:%m,%f:%l%m,%f %l%m"
vim.o.gfm = vim.o.grepformat
@@ -2637,12 +2638,6 @@ vim.go.gfm = vim.go.grepformat
--- line. The placeholder "$*" is allowed to specify where the arguments
--- will be included. Environment variables are expanded `:set_env`. See
--- `option-backslash` about including spaces and backslashes.
---- When your "grep" accepts the "-H" argument, use this to make ":grep"
---- also work well with a single file:
----
---- ```vim
---- set grepprg=grep\ -nH
---- ```
--- Special value: When 'grepprg' is set to "internal" the `:grep` command
--- works like `:vimgrep`, `:lgrep` like `:lvimgrep`, `:grepadd` like
--- `:vimgrepadd` and `:lgrepadd` like `:lvimgrepadd`.
@@ -2650,9 +2645,19 @@ vim.go.gfm = vim.go.grepformat
--- apply equally to 'grepprg'.
--- This option cannot be set from a `modeline` or in the `sandbox`, for
--- security reasons.
----
---- @type string
-vim.o.grepprg = "grep -n $* /dev/null"
+--- This option defaults to:
+--- - `rg --vimgrep -uu ` if ripgrep is available (`:checkhealth`),
+--- - `grep -HIn $* /dev/null` on Unix,
+--- - `findstr /n $* nul` on Windows.
+--- Ripgrep can perform additional filtering such as using .gitignore rules
+--- and skipping hidden files. This is disabled by default (see the -u option)
+--- to more closely match the behaviour of standard grep.
+--- You can make ripgrep match Vim's case handling using the
+--- -i/--ignore-case and -S/--smart-case options.
+--- An `OptionSet` autocmd can be used to set it up to match automatically.
+---
+--- @type string
+vim.o.grepprg = "grep -HIn $* /dev/null"
vim.o.gp = vim.o.grepprg
vim.bo.grepprg = vim.o.grepprg
vim.bo.gp = vim.bo.grepprg
@@ -5195,9 +5200,6 @@ vim.wo.scr = vim.wo.scroll
--- Minimum is 1, maximum is 100000.
--- Only in `terminal` buffers.
---
---- Note: Lines that are not visible and kept in scrollback are not
---- reflown when the terminal buffer is resized horizontally.
----
--- @type integer
vim.o.scrollback = -1
vim.o.scbk = vim.o.scrollback
@@ -6079,8 +6081,7 @@ vim.go.sta = vim.go.smarttab
--- highlighted with `hl-NonText`.
--- You may also want to add "lastline" to the 'display' option to show as
--- much of the last line as possible.
---- NOTE: only partly implemented, currently works with CTRL-E, CTRL-Y
---- and scrolling with the mouse.
+--- NOTE: partly implemented, doesn't work yet for `gj` and `gk`.
---
--- @type boolean
vim.o.smoothscroll = false
@@ -6746,6 +6747,8 @@ vim.bo.swf = vim.bo.swapfile
--- "split" when both are present.
--- uselast If included, jump to the previously used window when
--- jumping to errors with `quickfix` commands.
+--- If a window has 'winfixbuf' enabled, 'switchbuf' is currently not
+--- applied to the split window.
---
--- @type string
vim.o.switchbuf = "uselast"
@@ -6847,7 +6850,7 @@ vim.go.tpm = vim.go.tabpagemax
--- appear wrong in many places.
--- The value must be more than 0 and less than 10000.
---
---- There are four main ways to use tabs in Vim:
+--- There are five main ways to use tabs in Vim:
--- 1. Always keep 'tabstop' at 8, set 'softtabstop' and 'shiftwidth' to 4
--- (or 3 or whatever you prefer) and use 'noexpandtab'. Then Vim
--- will use a mix of tabs and spaces, but typing <Tab> and <BS> will
@@ -7443,6 +7446,7 @@ vim.bo.vts = vim.bo.vartabstop
---
--- Level Messages ~
--- ----------------------------------------------------------------------
+--- 1 Enables Lua tracing (see above). Does not produce messages.
--- 2 When a file is ":source"'ed, or `shada` file is read or written.
--- 3 UI info, terminal capabilities.
--- 4 Shell commands.
@@ -7863,8 +7867,8 @@ vim.wo.winbl = vim.wo.winblend
--- will scroll 'window' minus two lines, with a minimum of one.
--- When 'window' is equal to 'lines' minus one CTRL-F and CTRL-B scroll
--- in a much smarter way, taking care of wrapping lines.
---- When resizing the Vim window, the value is smaller than 1 or more than
---- or equal to 'lines' it will be set to 'lines' minus 1.
+--- When resizing the Vim window, and the value is smaller than 1 or more
+--- than or equal to 'lines' it will be set to 'lines' minus 1.
--- Note: Do not confuse this with the height of the Vim window, use
--- 'lines' for that.
---
@@ -7874,6 +7878,18 @@ vim.o.wi = vim.o.window
vim.go.window = vim.o.window
vim.go.wi = vim.go.window
+--- If enabled, the window and the buffer it is displaying are paired.
+--- For example, attempting to change the buffer with `:edit` will fail.
+--- Other commands which change a window's buffer such as `:cnext` will
+--- also skip any window with 'winfixbuf' enabled. However if an Ex
+--- command has a "!" modifier, it can force switching buffers.
+---
+--- @type boolean
+vim.o.winfixbuf = false
+vim.o.wfb = vim.o.winfixbuf
+vim.wo.winfixbuf = vim.o.winfixbuf
+vim.wo.wfb = vim.wo.winfixbuf
+
--- Keep the window height when windows are opened or closed and
--- 'equalalways' is set. Also for `CTRL-W_=`. Set by default for the
--- `preview-window` and `quickfix-window`.
diff --git a/runtime/lua/vim/_meta/re.lua b/runtime/lua/vim/_meta/re.lua
index 14c94c7824..d16751fbbf 100644
--- a/runtime/lua/vim/_meta/re.lua
+++ b/runtime/lua/vim/_meta/re.lua
@@ -8,11 +8,11 @@ error('Cannot require a meta file')
-- See 'lpeg.html' for license
--- @brief
---- The `vim.re` module provides a conventional regex-like syntax for pattern usage
---- within LPeg |vim.lpeg|.
+--- The `vim.re` module provides a conventional regex-like syntax for pattern usage within LPeg
+--- |vim.lpeg|. (Unrelated to |vim.regex| which provides Vim |regexp| from Lua.)
---
---- See https://www.inf.puc-rio.br/~roberto/lpeg/re.html for the original
---- documentation including regex syntax and more concrete examples.
+--- See https://www.inf.puc-rio.br/~roberto/lpeg/re.html for the original documentation including
+--- regex syntax and examples.
--- Compiles the given {string} and returns an equivalent LPeg pattern. The given string may define
--- either an expression or a grammar. The optional {defs} table provides extra Lua values to be used
@@ -30,8 +30,8 @@ function vim.re.compile(string, defs) end
--- @param subject string
--- @param pattern vim.lpeg.Pattern|string
--- @param init? integer
---- @return integer|nil the index where the occurrence starts, nil if no match
---- @return integer|nil the index where the occurrence ends, nil if no match
+--- @return integer|nil : the index where the occurrence starts, nil if no match
+--- @return integer|nil : the index where the occurrence ends, nil if no match
function vim.re.find(subject, pattern, init) end
--- Does a global substitution, replacing all occurrences of {pattern} in the given {subject} by
diff --git a/runtime/lua/vim/_meta/spell.lua b/runtime/lua/vim/_meta/spell.lua
index 57f2180895..c636db3b53 100644
--- a/runtime/lua/vim/_meta/spell.lua
+++ b/runtime/lua/vim/_meta/spell.lua
@@ -3,11 +3,11 @@
-- luacheck: no unused args
--- Check {str} for spelling errors. Similar to the Vimscript function
---- |spellbadword()|.
+--- [spellbadword()].
---
--- Note: The behaviour of this function is dependent on: 'spelllang',
--- 'spellfile', 'spellcapcheck' and 'spelloptions' which can all be local to
---- the buffer. Consider calling this with |nvim_buf_call()|.
+--- the buffer. Consider calling this with [nvim_buf_call()].
---
--- Example:
---
@@ -20,7 +20,7 @@
--- ```
---
--- @param str string
---- @return {[1]: string, [2]: string, [3]: string}[]
+--- @return {[1]: string, [2]: 'bad'|'rare'|'local'|'caps', [3]: integer}[]
--- List of tuples with three items:
--- - The badly spelled word.
--- - The type of the spelling error:
diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua
index ac25547212..f4daacfb7d 100644
--- a/runtime/lua/vim/_meta/vimfn.lua
+++ b/runtime/lua/vim/_meta/vimfn.lua
@@ -1598,11 +1598,10 @@ function vim.fn.eventhandler() end
--- The result is a Number:
--- 1 exists
--- 0 does not exist
---- -1 not implemented on this system
--- |exepath()| can be used to get the full path of an executable.
---
--- @param expr any
---- @return 0|1|-1
+--- @return 0|1
function vim.fn.executable(expr) end
--- Execute {command} and capture its output.
@@ -1959,6 +1958,7 @@ function vim.fn.extendnew(expr1, expr2, expr3) end
--- 't' Handle keys as if typed; otherwise they are handled as
--- if coming from a mapping. This matters for undo,
--- opening folds, etc.
+--- 'L' Lowlevel input. Other flags are not used.
--- 'i' Insert the string instead of appending (see above).
--- 'x' Execute commands until typeahead is empty. This is
--- similar to using ":normal!". You can call feedkeys()
@@ -2703,14 +2703,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 0, only get a character when one is available.
+--- If {expr} is omitted, 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
+--- 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()|.
---
---- Without [expr] and when [expr] is 0 a whole character or
+--- Without {expr} and when {expr} is 0 a whole character or
--- special key is returned. If it is a single character, the
--- result is a Number. Use |nr2char()| to convert it to a String.
--- Otherwise a String is returned with the encoded character.
@@ -2720,11 +2720,11 @@ function vim.fn.getchangelist(buf) end
--- also a String when a modifier (shift, control, alt) was used
--- that is not included in the character.
---
---- When [expr] is 0 and Esc is typed, there will be a short delay
+--- 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
--- sequence.
---
---- When [expr] is 1 only the first byte is returned. For a
+--- When {expr} is 1 only the first byte is returned. For a
--- one-byte character it is the character itself as a number.
--- Use nr2char() to convert it to a String.
---
@@ -2828,10 +2828,10 @@ 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
+--- 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
+--- 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
@@ -3581,6 +3581,43 @@ function vim.fn.getreginfo(regname) end
--- @return string[]
function vim.fn.getregion(pos1, pos2, opts) end
+--- Same as |getregion()|, but returns a list of positions
+--- describing the buffer text segments bound by {pos1} and
+--- {pos2}.
+--- The segments are a pair of positions for every line: >
+--- [[{start_pos}, {end_pos}], ...]
+--- <
+--- The position is a |List| with four numbers:
+--- [bufnum, lnum, col, off]
+--- "bufnum" is the buffer number.
+--- "lnum" and "col" are the position in the buffer. The first
+--- column is 1.
+--- If the "off" number of a starting position is non-zero, it is
+--- the offset in screen columns from the start of the character.
+--- E.g., a position within a <Tab> or after the last character.
+--- If the "off" number of an ending position is non-zero, it is
+--- the offset of the character's first cell not included in the
+--- selection, otherwise all its cells are included.
+---
+--- Apart from the options supported by |getregion()|, {opts} also
+--- supports the following:
+---
+--- eol If |TRUE|, indicate positions beyond
+--- the end of a line with "col" values
+--- one more than the length of the line.
+--- If |FALSE|, positions are limited
+--- within their lines, and if a line is
+--- empty or the selection is entirely
+--- beyond the end of a line, a "col"
+--- value of 0 is used for both positions.
+--- (default: |FALSE|)
+---
+--- @param pos1 table
+--- @param pos2 table
+--- @param opts? table
+--- @return integer[][][]
+function vim.fn.getregionpos(pos1, pos2, opts) end
+
--- The result is a String, which is type of register {regname}.
--- The value will be one of:
--- "v" for |charwise| text
@@ -3628,11 +3665,11 @@ function vim.fn.getregtype(regname) end
---
--- Examples: >vim
--- echo getscriptinfo({'name': 'myscript'})
---- echo getscriptinfo({'sid': 15}).variables
+--- echo getscriptinfo({'sid': 15})[0].variables
--- <
---
--- @param opts? table
---- @return any
+--- @return vim.fn.getscriptinfo.ret[]
function vim.fn.getscriptinfo(opts) end
--- If {tabnr} is not specified, then information about all the
@@ -5993,7 +6030,7 @@ function vim.fn.min(expr) end
function vim.fn.mkdir(name, flags, prot) end
--- Return a string that indicates the current mode.
---- If [expr] is supplied and it evaluates to a non-zero Number or
+--- If {expr} is supplied and it evaluates to a non-zero Number or
--- a non-empty String (|non-zero-arg|), then the full mode is
--- returned, otherwise only the first letter is returned.
--- Also see |state()|.
@@ -6520,6 +6557,9 @@ function vim.fn.prevnonblank(lnum) end
--- echo printf("%1$*2$.*3$f", 1.4142135, 6, 2)
--- < 1.41
---
+--- You will get an overflow error |E1510|, when the field-width
+--- or precision will result in a string longer than 6400 chars.
+---
--- *E1500*
--- You cannot mix positional and non-positional arguments: >vim
--- echo printf("%s%1$s", "One", "Two")
@@ -6580,7 +6620,7 @@ function vim.fn.prevnonblank(lnum) end
---
--- @param fmt any
--- @param expr1? any
---- @return any
+--- @return string
function vim.fn.printf(fmt, expr1) end
--- Returns the effective prompt text for buffer {buf}. {buf} can
@@ -7259,6 +7299,7 @@ function vim.fn.screenstring(row, col) end
--- When a match has been found its line number is returned.
--- If there is no match a 0 is returned and the cursor doesn't
--- move. No error message is given.
+--- To get the matched string, use |matchbufline()|.
---
--- {flags} is a String, which can contain these character flags:
--- 'b' search Backward instead of forward
@@ -8289,10 +8330,11 @@ function vim.fn.sha256(string) end
--- Otherwise encloses {string} in single-quotes and replaces all
--- "'" with "'\''".
---
---- If {special} is a |non-zero-arg|:
---- - Special items such as "!", "%", "#" and "<cword>" will be
---- preceded by a backslash. The backslash will be removed again
---- by the |:!| command.
+--- The {special} argument adds additional escaping of keywords
+--- used in Vim commands. If it is a |non-zero-arg|:
+--- - Special items such as "!", "%", "#" and "<cword>" (as listed
+--- in |expand()|) will be preceded by a backslash.
+--- The backslash will be removed again by the |:!| command.
--- - The <NL> character is escaped.
---
--- If 'shell' contains "csh" in the tail:
@@ -8796,7 +8838,8 @@ function vim.fn.sinh(expr) end
--- Similar to using a |slice| "expr[start : end]", but "end" is
--- used exclusive. And for a string the indexes are used as
--- character indexes instead of byte indexes.
---- Also, composing characters are not counted.
+--- Also, composing characters are treated as a part of the
+--- preceding base character.
--- When {end} is omitted the slice continues to the last item.
--- When {end} is -1 the last item is omitted.
--- Returns an empty value if {start} or {end} are invalid.
@@ -9204,8 +9247,8 @@ function vim.fn.strcharlen(string) end
--- of byte index and length.
--- When {skipcc} is omitted or zero, composing characters are
--- counted separately.
---- When {skipcc} set to 1, Composing characters are ignored,
---- similar to |slice()|.
+--- When {skipcc} set to 1, composing characters are treated as a
+--- part of the preceding base character, similar to |slice()|.
--- When a character index is used where a character does not
--- exist it is omitted and counted as one character. For
--- example: >vim
@@ -9225,7 +9268,7 @@ function vim.fn.strcharpart(src, start, len, skipcc) end
--- in String {string}.
--- When {skipcc} is omitted or zero, composing characters are
--- counted separately.
---- When {skipcc} set to 1, Composing characters are ignored.
+--- When {skipcc} set to 1, composing characters are ignored.
--- |strcharlen()| always does this.
---
--- Returns zero on error.
@@ -9348,10 +9391,10 @@ function vim.fn.stridx(haystack, needle, start) end
--- for infinite and NaN floating-point values representations
--- which use |str2float()|. Strings are also dumped literally,
--- only single quote is escaped, which does not allow using YAML
---- for parsing back binary strings. |eval()| should always work for
---- strings and floats though and this is the only official
---- method, use |msgpackdump()| or |json_encode()| if you need to
---- share data with other application.
+--- for parsing back binary strings. |eval()| should always work
+--- for strings and floats though, and this is the only official
+--- method. Use |msgpackdump()| or |json_encode()| if you need to
+--- share data with other applications.
---
--- @param expr any
--- @return string
@@ -9747,6 +9790,10 @@ function vim.fn.synIDtrans(synID) end
--- synconcealed(lnum, 5) [1, 'X', 2]
--- synconcealed(lnum, 6) [0, '', 0]
---
+--- Note: Doesn't consider |matchadd()| highlighting items,
+--- since syntax and matching highlighting are two different
+--- mechanisms |syntax-vs-match|.
+---
--- @param lnum integer
--- @param col integer
--- @return {[1]: integer, [2]: string, [3]: integer}
@@ -10591,17 +10638,16 @@ function vim.fn.win_move_statusline(nr, offset) end
--- [1, 1], unless there is a tabline, then it is [2, 1].
--- {nr} can be the window number or the |window-ID|. Use zero
--- for the current window.
---- Returns [0, 0] if the window cannot be found in the current
---- tabpage.
+--- Returns [0, 0] if the window cannot be found.
---
--- @param nr integer
--- @return any
function vim.fn.win_screenpos(nr) end
---- Move the window {nr} to a new split of the window {target}.
---- This is similar to moving to {target}, creating a new window
---- using |:split| but having the same contents as window {nr}, and
---- then closing {nr}.
+--- Temporarily switch to window {target}, then move window {nr}
+--- to a new split adjacent to {target}.
+--- Unlike commands such as |:split|, no new windows are created
+--- (the |window-ID| of window {nr} is unchanged after the move).
---
--- Both {nr} and {target} can be window numbers or |window-ID|s.
--- Both must be in the current tab page.
@@ -10724,7 +10770,9 @@ function vim.fn.winline() end
--- # the number of the last accessed window (where
--- |CTRL-W_p| goes to). If there is no previous
--- window or it is in another tab page 0 is
---- returned.
+--- returned. May refer to the current window in
+--- some cases (e.g. when evaluating 'statusline'
+--- expressions).
--- {N}j the number of the Nth window below the
--- current window (where |CTRL-W_j| goes to).
--- {N}k the number of the Nth window above the current
diff --git a/runtime/lua/vim/_meta/vvars.lua b/runtime/lua/vim/_meta/vvars.lua
index ee6d8ddf35..e00402ab3f 100644
--- a/runtime/lua/vim/_meta/vvars.lua
+++ b/runtime/lua/vim/_meta/vvars.lua
@@ -54,9 +54,10 @@ vim.v.cmdbang = ...
--- @type string
vim.v.collate = ...
---- Dictionary containing the most recent `complete-items` after
---- `CompleteDone`. Empty if the completion failed, or after
---- leaving and re-entering insert mode.
+--- Dictionary containing the `complete-items` for the most
+--- recently completed word after `CompleteDone`. Empty if the
+--- completion failed, or after leaving and re-entering insert
+--- mode.
--- Note: Plugins can modify the value to emulate the builtin
--- `CompleteDone` event behavior.
--- @type any
@@ -194,6 +195,7 @@ vim.v.errors = ...
--- changed_window Is `v:true` if the event fired while
--- 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
vim.v.event = ...
diff --git a/runtime/lua/vim/_options.lua b/runtime/lua/vim/_options.lua
index 13ad6cc58f..b41e298dd7 100644
--- a/runtime/lua/vim/_options.lua
+++ b/runtime/lua/vim/_options.lua
@@ -642,7 +642,7 @@ end
--- @param t table<any,any>
--- @param val any
local function remove_one_item(t, val)
- if vim.tbl_islist(t) then
+ if vim.islist(t) then
local remove_index = nil
for i, v in ipairs(t) do
if v == val then
diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua
index e97a5fc6c3..d603971495 100644
--- a/runtime/lua/vim/_system.lua
+++ b/runtime/lua/vim/_system.lua
@@ -18,6 +18,7 @@ local uv = vim.uv
--- @field stderr? string
--- @class vim.SystemState
+--- @field cmd string[]
--- @field handle? uv.uv_process_t
--- @field timer? uv.uv_timer_t
--- @field pid? integer
@@ -56,6 +57,7 @@ local function close_handles(state)
end
--- @class vim.SystemObj
+--- @field cmd string[]
--- @field pid integer
--- @field private _state vim.SystemState
--- @field wait fun(self: vim.SystemObj, timeout?: integer): vim.SystemCompleted
@@ -68,6 +70,7 @@ local SystemObj = {}
--- @return vim.SystemObj
local function new_systemobj(state)
return setmetatable({
+ cmd = state.cmd,
pid = state.pid,
_state = state,
}, { __index = SystemObj })
diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua
index 97c5481ad1..02b3f536c2 100644
--- a/runtime/lua/vim/_watch.lua
+++ b/runtime/lua/vim/_watch.lua
@@ -200,11 +200,13 @@ function M.watchdirs(path, opts, callback)
local max_depth = 100
for name, type in vim.fs.dir(path, { depth = max_depth }) do
- local filepath = vim.fs.joinpath(path, name)
- if type == 'directory' and not skip(filepath, opts) then
- local handle = assert(uv.new_fs_event())
- handles[filepath] = handle
- handle:start(filepath, {}, create_on_change(filepath))
+ if type == 'directory' then
+ local filepath = vim.fs.joinpath(path, name)
+ if not skip(filepath, opts) then
+ local handle = assert(uv.new_fs_event())
+ handles[filepath] = handle
+ handle:start(filepath, {}, create_on_change(filepath))
+ end
end
end
@@ -290,6 +292,10 @@ function M.fswatch(path, opts, callback)
if data and #vim.trim(data) > 0 then
vim.schedule(function()
+ if vim.fn.has('linux') == 1 and vim.startswith(data, 'Event queue overflow') then
+ data = 'inotify(7) limit reached, see :h fswatch-limitations for more info.'
+ end
+
vim.notify('fswatch: ' .. data, vim.log.levels.ERROR)
end)
end
@@ -303,6 +309,8 @@ function M.fswatch(path, opts, callback)
fswatch_output_handler(line, opts, callback)
end
end,
+ -- --latency is locale dependent but tostring() isn't and will always have '.' as decimal point.
+ env = { LC_NUMERIC = 'C' },
})
return function()
diff --git a/runtime/lua/vim/deprecated/health.lua b/runtime/lua/vim/deprecated/health.lua
new file mode 100644
index 0000000000..0f6b1f578c
--- /dev/null
+++ b/runtime/lua/vim/deprecated/health.lua
@@ -0,0 +1,42 @@
+local M = {}
+local health = vim.health
+
+local deprecated = {}
+
+function M.check()
+ if next(deprecated) == nil then
+ health.ok('No deprecated functions detected')
+ return
+ end
+
+ for name, v in vim.spairs(deprecated) do
+ health.start('')
+
+ local version, backtraces, alternative = v[1], v[2], v[3]
+ local major, minor = version:match('(%d+)%.(%d+)')
+ major, minor = tonumber(major), tonumber(minor)
+ local removal_version = string.format('nvim-%d.%d', major, minor)
+ local will_be_removed = vim.fn.has(removal_version) == 1 and 'was removed' or 'will be removed'
+
+ local msg = ('%s is deprecated. Feature %s in Nvim %s'):format(name, will_be_removed, version)
+ local msg_alternative = alternative and ('use %s instead.'):format(alternative)
+ local advice = { msg_alternative }
+ table.insert(advice, backtraces)
+ advice = vim.iter(advice):flatten():totable()
+ health.warn(msg, advice)
+ end
+end
+
+function M.add(name, version, backtrace, alternative)
+ if deprecated[name] == nil then
+ deprecated[name] = { version, { backtrace }, alternative }
+ return
+ end
+
+ local it = vim.iter(deprecated[name][2])
+ if it:find(backtrace) == nil then
+ table.insert(deprecated[name][2], backtrace)
+ end
+end
+
+return M
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
index d5075d7d3d..348204abb7 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -76,7 +76,7 @@ local M = {}
--- before lower severities (e.g. ERROR is displayed before WARN).
--- Options:
--- - {reverse}? (boolean) Reverse sort order
---- (default: `false)
+--- (default: `false`)
--- @field severity_sort? boolean|{reverse?:boolean}
--- @class (private) vim.diagnostic.OptsResolved
@@ -152,6 +152,8 @@ local M = {}
--- @field suffix? string|table|(fun(diagnostic:vim.Diagnostic,i:integer,total:integer): string, string)
---
--- @field focus_id? string
+---
+--- @field border? string see |nvim_open_win()|.
--- @class vim.diagnostic.Opts.Underline
---
@@ -239,6 +241,17 @@ local M = {}
--- whole line the sign is placed in.
--- @field linehl? table<vim.diagnostic.Severity,string>
+-- TODO: inherit from `vim.diagnostic.Opts`, implement its fields.
+--- Optional filters |kwargs|, or `nil` for all.
+--- @class vim.diagnostic.Filter
+--- @inlinedoc
+---
+--- Diagnostic namespace, or `nil` for all.
+--- @field ns_id? integer
+---
+--- Buffer number, or 0 for current buffer, or `nil` for all buffers.
+--- @field bufnr? integer
+
--- @nodoc
--- @enum vim.diagnostic.Severity
M.severity = {
@@ -361,43 +374,46 @@ local function to_severity(severity)
end
--- @param severity vim.diagnostic.SeverityFilter
---- @param diagnostics vim.Diagnostic[]
---- @return vim.Diagnostic[]
-local function filter_by_severity(severity, diagnostics)
- if not severity then
- return diagnostics
- end
-
+--- @return fun(vim.Diagnostic):boolean
+local function severity_predicate(severity)
if type(severity) ~= 'table' then
severity = assert(to_severity(severity))
- --- @param t vim.Diagnostic
- return vim.tbl_filter(function(t)
- return t.severity == severity
- end, diagnostics)
+ ---@param d vim.Diagnostic
+ return function(d)
+ return d.severity == severity
+ end
end
-
if severity.min or severity.max then
--- @cast severity {min:vim.diagnostic.Severity,max:vim.diagnostic.Severity}
local min_severity = to_severity(severity.min) or M.severity.HINT
local max_severity = to_severity(severity.max) or M.severity.ERROR
- --- @param t vim.Diagnostic
- return vim.tbl_filter(function(t)
- return t.severity <= min_severity and t.severity >= max_severity
- end, diagnostics)
+ --- @param d vim.Diagnostic
+ return function(d)
+ return d.severity <= min_severity and d.severity >= max_severity
+ end
end
--- @cast severity vim.diagnostic.Severity[]
-
local severities = {} --- @type table<vim.diagnostic.Severity,true>
for _, s in ipairs(severity) do
severities[assert(to_severity(s))] = true
end
- --- @param t vim.Diagnostic
- return vim.tbl_filter(function(t)
- return severities[t.severity]
- end, diagnostics)
+ --- @param d vim.Diagnostic
+ return function(d)
+ return severities[d.severity]
+ end
+end
+
+--- @param severity vim.diagnostic.SeverityFilter
+--- @param diagnostics vim.Diagnostic[]
+--- @return vim.Diagnostic[]
+local function filter_by_severity(severity, diagnostics)
+ if not severity then
+ return diagnostics
+ end
+ return vim.tbl_filter(severity_predicate(severity), diagnostics)
end
--- @param bufnr integer
@@ -682,6 +698,13 @@ local function get_diagnostics(bufnr, opts, clamp)
opts = opts or {}
local namespace = opts.namespace
+
+ if type(namespace) == 'number' then
+ namespace = { namespace }
+ end
+
+ ---@cast namespace integer[]
+
local diagnostics = {}
-- Memoized results of buf_line_count per bufnr
@@ -696,10 +719,18 @@ local function get_diagnostics(bufnr, opts, clamp)
end,
})
+ local match_severity = opts.severity and severity_predicate(opts.severity)
+ or function(_)
+ return true
+ end
+
---@param b integer
---@param d vim.Diagnostic
local function add(b, d)
- if not opts.lnum or d.lnum == opts.lnum then
+ if
+ match_severity(d)
+ and (not opts.lnum or (opts.lnum >= d.lnum and opts.lnum <= (d.end_lnum or d.lnum)))
+ then
if clamp and api.nvim_buf_is_loaded(b) then
local line_count = buf_line_count[b] - 1
if
@@ -742,15 +773,15 @@ local function get_diagnostics(bufnr, opts, clamp)
end
elseif bufnr == nil then
for b, t in pairs(diagnostic_cache) do
- add_all_diags(b, t[namespace] or {})
+ for _, iter_namespace in ipairs(namespace) do
+ add_all_diags(b, t[iter_namespace] or {})
+ end
end
else
bufnr = get_bufnr(bufnr)
- add_all_diags(bufnr, diagnostic_cache[bufnr][namespace] or {})
- end
-
- if opts.severity then
- diagnostics = filter_by_severity(opts.severity, diagnostics)
+ for _, iter_namespace in ipairs(namespace) do
+ add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace] or {})
+ end
end
return diagnostics
@@ -781,21 +812,52 @@ local function set_list(loclist, opts)
end
end
+--- Jump to the diagnostic with the highest severity. First sort the
+--- diagnostics by severity. The first diagnostic then contains the highest severity, and we can
+--- discard all diagnostics with a lower severity.
+--- @param diagnostics vim.Diagnostic[]
+local function filter_highest(diagnostics)
+ table.sort(diagnostics, function(a, b)
+ return a.severity < b.severity
+ end)
+
+ -- Find the first diagnostic where the severity does not match the highest severity, and remove
+ -- that element and all subsequent elements from the array
+ local worst = (diagnostics[1] or {}).severity
+ local len = #diagnostics
+ for i = 2, len do
+ if diagnostics[i].severity ~= worst then
+ for j = i, len do
+ diagnostics[j] = nil
+ end
+ break
+ end
+ end
+end
+
--- @param position {[1]: integer, [2]: integer}
--- @param search_forward boolean
--- @param bufnr integer
--- @param opts vim.diagnostic.GotoOpts
---- @param namespace integer
+--- @param namespace integer[]|integer
--- @return vim.Diagnostic?
local function next_diagnostic(position, search_forward, bufnr, opts, namespace)
position[1] = position[1] - 1
bufnr = get_bufnr(bufnr)
local wrap = if_nil(opts.wrap, true)
- local line_count = api.nvim_buf_line_count(bufnr)
- local diagnostics =
- get_diagnostics(bufnr, vim.tbl_extend('keep', opts, { namespace = namespace }), true)
+
+ local get_opts = vim.deepcopy(opts)
+ get_opts.namespace = get_opts.namespace or namespace
+
+ local diagnostics = get_diagnostics(bufnr, get_opts, true)
+
+ if opts._highest then
+ filter_highest(diagnostics)
+ end
+
local line_diagnostics = diagnostic_lines(diagnostics)
+ local line_count = api.nvim_buf_line_count(bufnr)
for i = 0, line_count do
local offset = i * (search_forward and 1 or -1)
local lnum = position[1] + offset
@@ -814,14 +876,14 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace)
return a.col < b.col
end
is_next = function(d)
- return math.min(d.col, line_length - 1) > position[2]
+ return math.min(d.col, math.max(line_length - 1, 0)) > position[2]
end
else
sort_diagnostics = function(a, b)
return a.col > b.col
end
is_next = function(d)
- return math.min(d.col, line_length - 1) < position[2]
+ return math.min(d.col, math.max(line_length - 1, 0)) < position[2]
end
end
table.sort(line_diagnostics[lnum], sort_diagnostics)
@@ -952,7 +1014,7 @@ function M.set(namespace, bufnr, diagnostics, opts)
bufnr = { bufnr, 'n' },
diagnostics = {
diagnostics,
- vim.tbl_islist,
+ vim.islist,
'a list of diagnostics',
},
opts = { opts, 't', true },
@@ -1115,10 +1177,10 @@ end
--- A table with the following keys:
--- @class vim.diagnostic.GetOpts
---
---- Limit diagnostics to the given namespace.
---- @field namespace? integer
+--- Limit diagnostics to one or more namespaces.
+--- @field namespace? integer[]|integer
---
---- Limit diagnostics to the given line number.
+--- Limit diagnostics to those spanning the specified line number.
--- @field lnum? integer
---
--- See |diagnostic-severity|.
@@ -1137,7 +1199,11 @@ end
--- @field wrap? boolean
---
--- See |diagnostic-severity|.
---- @field severity vim.diagnostic.Severity
+--- @field severity? vim.diagnostic.SeverityFilter
+---
+--- Go to the diagnostic with the highest severity.
+--- (default: `false`)
+--- @field package _highest? boolean
---
--- If `true`, call |vim.diagnostic.open_float()| after moving.
--- If a table, pass the table as the {opts} parameter to |vim.diagnostic.open_float()|.
@@ -1164,7 +1230,7 @@ M.handlers.signs = {
bufnr = { bufnr, 'n' },
diagnostics = {
diagnostics,
- vim.tbl_islist,
+ vim.islist,
'a list of diagnostics',
},
opts = { opts, 't', true },
@@ -1213,9 +1279,7 @@ M.handlers.signs = {
vim.deprecate(
'Defining diagnostic signs with :sign-define or sign_define()',
'vim.diagnostic.config()',
- '0.12',
- nil,
- false
+ '0.12'
)
if not opts.signs.text then
@@ -1287,7 +1351,7 @@ M.handlers.underline = {
bufnr = { bufnr, 'n' },
diagnostics = {
diagnostics,
- vim.tbl_islist,
+ vim.islist,
'a list of diagnostics',
},
opts = { opts, 't', true },
@@ -1360,7 +1424,7 @@ M.handlers.virtual_text = {
bufnr = { bufnr, 'n' },
diagnostics = {
diagnostics,
- vim.tbl_islist,
+ vim.islist,
'a list of diagnostics',
},
opts = { opts, 't', true },
@@ -1481,7 +1545,7 @@ end
--- diagnostics, use |vim.diagnostic.reset()|.
---
--- To hide diagnostics and prevent them from re-displaying, use
---- |vim.diagnostic.disable()|.
+--- |vim.diagnostic.enable()|.
---
---@param namespace integer? Diagnostic namespace. When omitted, hide
--- diagnostics from all namespaces.
@@ -1506,25 +1570,32 @@ function M.hide(namespace, bufnr)
end
end
---- Check whether diagnostics are disabled in a given buffer.
+--- Check whether diagnostics are enabled.
---
----@param bufnr integer? Buffer number, or 0 for current buffer.
----@param namespace integer? Diagnostic namespace. When omitted, checks if
---- all diagnostics are disabled in {bufnr}.
---- Otherwise, only checks if diagnostics from
---- {namespace} are disabled.
----@return boolean
-function M.is_disabled(bufnr, namespace)
- bufnr = get_bufnr(bufnr)
- if namespace and M.get_namespace(namespace).disabled then
- return true
+--- @param filter vim.diagnostic.Filter?
+--- @return boolean
+--- @since 12
+function M.is_enabled(filter)
+ filter = filter or {}
+ if filter.ns_id and M.get_namespace(filter.ns_id).disabled then
+ return false
+ elseif filter.bufnr == nil then
+ -- See enable() logic.
+ return vim.tbl_isempty(diagnostic_disabled) and not diagnostic_disabled[1]
end
+ local bufnr = get_bufnr(filter.bufnr)
if type(diagnostic_disabled[bufnr]) == 'table' then
- return diagnostic_disabled[bufnr][namespace]
+ return not diagnostic_disabled[bufnr][filter.ns_id]
end
- return diagnostic_disabled[bufnr] ~= nil
+ return diagnostic_disabled[bufnr] == nil
+end
+
+--- @deprecated use `vim.diagnostic.is_enabled()`
+function M.is_disabled(bufnr, namespace)
+ vim.deprecate('vim.diagnostic.is_disabled()', 'vim.diagnostic.is_enabled()', '0.12')
+ return not M.is_enabled { bufnr = bufnr or 0, ns_id = namespace }
end
--- Display diagnostics for the given namespace and buffer.
@@ -1547,7 +1618,7 @@ function M.show(namespace, bufnr, diagnostics, opts)
diagnostics = {
diagnostics,
function(v)
- return v == nil or vim.tbl_islist(v)
+ return v == nil or vim.islist(v)
end,
'a list of diagnostics',
},
@@ -1570,7 +1641,7 @@ function M.show(namespace, bufnr, diagnostics, opts)
return
end
- if M.is_disabled(bufnr, namespace) then
+ if not M.is_enabled { bufnr = bufnr or 0, ns_id = namespace } then
return
end
@@ -1668,7 +1739,7 @@ function M.open_float(opts, ...)
if scope == 'line' then
--- @param d vim.Diagnostic
diagnostics = vim.tbl_filter(function(d)
- return d.lnum == lnum
+ return lnum >= d.lnum and lnum <= d.end_lnum
end, diagnostics)
elseif scope == 'cursor' then
-- LSP servers can send diagnostics with `end_col` past the length of the line
@@ -1912,71 +1983,95 @@ function M.setloclist(opts)
set_list(true, opts)
end
---- Disable diagnostics in the given buffer.
----
----@param bufnr integer? Buffer number, or 0 for current buffer. When
---- omitted, disable diagnostics in all buffers.
----@param namespace integer? Only disable diagnostics for the given namespace.
+--- @deprecated use `vim.diagnostic.enable(false, …)`
function M.disable(bufnr, namespace)
- vim.validate({ bufnr = { bufnr, 'n', true }, namespace = { namespace, 'n', true } })
- if bufnr == nil then
- if namespace == nil then
- -- Disable everything (including as yet non-existing buffers and
- -- namespaces) by setting diagnostic_disabled to an empty table and set
- -- its metatable to always return true. This metatable is removed
- -- in enable()
- diagnostic_disabled = setmetatable({}, {
- __index = function()
- return true
- end,
- })
- else
- local ns = M.get_namespace(namespace)
- ns.disabled = true
- end
+ vim.deprecate('vim.diagnostic.disable()', 'vim.diagnostic.enable(false, …)', '0.12')
+ M.enable(false, { bufnr = bufnr, ns_id = namespace })
+end
+
+--- Enables or disables diagnostics.
+---
+--- To "toggle", pass the inverse of `is_enabled()`:
+---
+--- ```lua
+--- vim.diagnostic.enable(not vim.diagnostic.is_enabled())
+--- ```
+---
+--- @param enable (boolean|nil) true/nil to enable, false to disable
+--- @param filter vim.diagnostic.Filter?
+function M.enable(enable, filter)
+ -- Deprecated signature. Drop this in 0.12
+ local legacy = (enable or filter)
+ and vim.tbl_contains({ 'number', 'nil' }, type(enable))
+ and vim.tbl_contains({ 'number', 'nil' }, type(filter))
+
+ if legacy then
+ vim.deprecate(
+ 'vim.diagnostic.enable(buf:number, namespace:number)',
+ 'vim.diagnostic.enable(enable:boolean, filter:table)',
+ '0.12'
+ )
+
+ vim.validate({
+ enable = { enable, 'n', true }, -- Legacy `bufnr` arg.
+ filter = { filter, 'n', true }, -- Legacy `namespace` arg.
+ })
+
+ local ns_id = type(filter) == 'number' and filter or nil
+ filter = {}
+ filter.ns_id = ns_id
+ filter.bufnr = type(enable) == 'number' and enable or nil
+ enable = true
else
- bufnr = get_bufnr(bufnr)
- if namespace == nil then
- diagnostic_disabled[bufnr] = true
- else
- if type(diagnostic_disabled[bufnr]) ~= 'table' then
- diagnostic_disabled[bufnr] = {}
- end
- diagnostic_disabled[bufnr][namespace] = true
- end
+ filter = filter or {}
+ vim.validate({
+ enable = { enable, 'b', true },
+ filter = { filter, 't', true },
+ })
end
- M.hide(namespace, bufnr)
-end
+ enable = enable == nil and true or enable
+ local bufnr = filter.bufnr
---- Enable diagnostics in the given buffer.
----
----@param bufnr integer? Buffer number, or 0 for current buffer. When
---- omitted, enable diagnostics in all buffers.
----@param namespace integer? Only enable diagnostics for the given namespace.
-function M.enable(bufnr, namespace)
- vim.validate({ bufnr = { bufnr, 'n', true }, namespace = { namespace, 'n', true } })
if bufnr == nil then
- if namespace == nil then
- -- Enable everything by setting diagnostic_disabled to an empty table
- diagnostic_disabled = {}
+ if filter.ns_id == nil then
+ diagnostic_disabled = (
+ enable
+ -- Enable everything by setting diagnostic_disabled to an empty table.
+ and {}
+ -- Disable everything (including as yet non-existing buffers and namespaces) by setting
+ -- diagnostic_disabled to an empty table and set its metatable to always return true.
+ or setmetatable({}, {
+ __index = function()
+ return true
+ end,
+ })
+ )
else
- local ns = M.get_namespace(namespace)
- ns.disabled = false
+ local ns = M.get_namespace(filter.ns_id)
+ ns.disabled = not enable
end
else
bufnr = get_bufnr(bufnr)
- if namespace == nil then
- diagnostic_disabled[bufnr] = nil
+ if filter.ns_id == nil then
+ diagnostic_disabled[bufnr] = (not enable) and true or nil
else
if type(diagnostic_disabled[bufnr]) ~= 'table' then
- return
+ if enable then
+ return
+ else
+ diagnostic_disabled[bufnr] = {}
+ end
end
- diagnostic_disabled[bufnr][namespace] = nil
+ diagnostic_disabled[bufnr][filter.ns_id] = (not enable) and true or nil
end
end
- M.show(namespace, bufnr)
+ if enable then
+ M.show(filter.ns_id, bufnr)
+ else
+ M.hide(filter.ns_id, bufnr)
+ end
end
--- Parse a diagnostic from a string.
@@ -2059,7 +2154,7 @@ function M.toqflist(diagnostics)
vim.validate({
diagnostics = {
diagnostics,
- vim.tbl_islist,
+ vim.islist,
'a list of diagnostics',
},
})
@@ -2099,7 +2194,7 @@ function M.fromqflist(list)
vim.validate({
list = {
list,
- vim.tbl_islist,
+ vim.islist,
'a list of quickfix items',
},
})
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
index fba76f93b2..d1fdd0aa16 100644
--- a/runtime/lua/vim/filetype.lua
+++ b/runtime/lua/vim/filetype.lua
@@ -14,7 +14,8 @@ local M = {}
local function starsetf(ft, opts)
return {
function(path, bufnr)
- local f = type(ft) == 'function' and ft(path, bufnr) or ft
+ -- Note: when `ft` is a function its return value may be nil.
+ local f = type(ft) ~= 'function' and ft or ft(path, bufnr)
if not vim.g.ft_ignore_pat then
return f
end
@@ -236,6 +237,7 @@ local extension = {
bbclass = 'bitbake',
bl = 'blank',
blp = 'blueprint',
+ bp = 'bp',
bsd = 'bsdl',
bsdl = 'bsdl',
bst = 'bst',
@@ -246,13 +248,15 @@ local extension = {
bzl = 'bzl',
bazel = 'bzl',
BUILD = 'bzl',
+ mdh = 'c',
+ epro = 'c',
qc = 'c',
cabal = 'cabal',
cairo = 'cairo',
capnp = 'capnp',
cdc = 'cdc',
cdl = 'cdl',
- toc = 'cdrtoc',
+ toc = detect_line1('\\contentsline', 'tex', 'cdrtoc'),
cfc = 'cf',
cfm = 'cf',
cfi = 'cf',
@@ -282,6 +286,7 @@ local extension = {
cbl = 'cobol',
atg = 'coco',
recipe = 'conaryrecipe',
+ ctags = 'conf',
hook = function(path, bufnr)
return M._getline(bufnr, 1) == '[Trigger]' and 'confini' or nil
end,
@@ -335,6 +340,7 @@ local extension = {
si = 'cuplsim',
cyn = 'cynpp',
cypher = 'cypher',
+ dfy = 'dafny',
dart = 'dart',
drt = 'dart',
ds = 'datascript',
@@ -366,6 +372,7 @@ local extension = {
dtsi = 'dts',
dtso = 'dts',
its = 'dts',
+ keymap = 'dts',
dylan = 'dylan',
intr = 'dylanintr',
lid = 'dylanlid',
@@ -465,6 +472,7 @@ local extension = {
glsl = 'glsl',
gn = 'gn',
gni = 'gn',
+ gnuplot = 'gnuplot',
gpi = 'gnuplot',
go = 'go',
gp = 'gp',
@@ -502,7 +510,16 @@ local extension = {
vc = 'hercules',
heex = 'heex',
hex = 'hex',
+ ['a43'] = 'hex',
+ ['a90'] = 'hex',
['h32'] = 'hex',
+ ['h80'] = 'hex',
+ ['h86'] = 'hex',
+ ihex = 'hex',
+ ihe = 'hex',
+ ihx = 'hex',
+ int = 'hex',
+ mcs = 'hex',
hjson = 'hjson',
m3u = 'hlsplaylist',
m3u8 = 'hlsplaylist',
@@ -530,6 +547,7 @@ local extension = {
inf = 'inform',
INF = 'inform',
ii = 'initng',
+ inko = 'inko',
inp = detect.inp,
ms = detect_seq(detect.nroff, 'xmath'),
iss = 'iss',
@@ -554,6 +572,7 @@ local extension = {
jsx = 'javascriptreact',
clp = 'jess',
jgr = 'jgraph',
+ jjdescription = 'jj',
j73 = 'jovial',
jov = 'jovial',
jovial = 'jovial',
@@ -565,7 +584,14 @@ local extension = {
geojson = 'json',
webmanifest = 'json',
ipynb = 'json',
+ ['jupyterlab-settings'] = 'json',
+ ['sublime-project'] = 'json',
+ ['sublime-settings'] = 'json',
+ ['sublime-workspace'] = 'json',
['json-patch'] = 'json',
+ bd = 'json',
+ bda = 'json',
+ xci = 'json',
json5 = 'json5',
jsonc = 'jsonc',
jsonl = 'jsonl',
@@ -609,12 +635,13 @@ local extension = {
el = 'lisp',
lsp = 'lisp',
asd = 'lisp',
+ stsg = 'lisp',
lt = 'lite',
lite = 'lite',
livemd = 'livebook',
lgt = 'logtalk',
lotos = 'lotos',
- lot = 'lotos',
+ lot = detect_line1('\\contentsline', 'tex', 'lotos'),
lout = 'lout',
lou = 'lout',
ulpc = 'lpc',
@@ -625,6 +652,7 @@ local extension = {
nse = 'lua',
rockspec = 'lua',
lua = 'lua',
+ tlu = 'lua',
luau = 'luau',
lrc = 'lyrics',
m = detect.m,
@@ -644,12 +672,12 @@ local extension = {
mws = 'maple',
mpl = 'maple',
mv = 'maple',
- mkdn = 'markdown',
- md = 'markdown',
- mdwn = 'markdown',
- mkd = 'markdown',
- markdown = 'markdown',
- mdown = 'markdown',
+ mkdn = detect.markdown,
+ md = detect.markdown,
+ mdwn = detect.markdown,
+ mkd = detect.markdown,
+ markdown = detect.markdown,
+ mdown = detect.markdown,
mhtml = 'mason',
comp = 'mason',
mason = 'mason',
@@ -744,12 +772,19 @@ local extension = {
ora = 'ora',
org = 'org',
org_archive = 'org',
+ pandoc = 'pandoc',
+ pdk = 'pandoc',
+ pd = 'pandoc',
+ pdc = 'pandoc',
pxsl = 'papp',
papp = 'papp',
pxml = 'papp',
pas = 'pascal',
- lpr = 'pascal',
+ lpr = detect_line1('<%?xml', 'xml', 'pascal'),
dpr = 'pascal',
+ txtpb = 'pbtxt',
+ textproto = 'pbtxt',
+ textpb = 'pbtxt',
pbtxt = 'pbtxt',
g = 'pccts',
pcmk = 'pcmk',
@@ -810,6 +845,7 @@ local extension = {
psf = 'psf',
psl = 'psl',
pug = 'pug',
+ purs = 'purescript',
arr = 'pyret',
pxd = 'pyrex',
pyx = 'pyrex',
@@ -869,6 +905,7 @@ local extension = {
Snw = 'rnoweb',
robot = 'robot',
resource = 'robot',
+ roc = 'roc',
ron = 'ron',
rsc = 'routeros',
x = 'rpcgen',
@@ -911,11 +948,13 @@ local extension = {
sexp = 'sexplib',
bash = detect.bash,
bats = detect.bash,
+ cygport = detect.bash,
ebuild = detect.bash,
eclass = detect.bash,
env = detect.sh,
ksh = detect.ksh,
sh = detect.sh,
+ mdd = 'sh',
sieve = 'sieve',
siv = 'sieve',
sig = detect.sig,
@@ -933,6 +972,7 @@ local extension = {
cdf = 'skill',
sl = 'slang',
ice = 'slice',
+ slint = 'slint',
score = 'slrnsc',
sol = 'solidity',
smali = 'smali',
@@ -983,6 +1023,8 @@ local extension = {
mata = 'stata',
ado = 'stata',
stp = 'stp',
+ styl = 'stylus',
+ stylus = 'stylus',
quark = 'supercollider',
sface = 'surface',
svelte = 'svelte',
@@ -1005,6 +1047,7 @@ local extension = {
tk = 'tcl',
jacl = 'tcl',
tl = 'teal',
+ templ = 'templ',
tmpl = 'template',
ti = 'terminfo',
dtx = 'tex',
@@ -1012,6 +1055,26 @@ local extension = {
bbl = 'tex',
latex = 'tex',
sty = 'tex',
+ pgf = 'tex',
+ nlo = 'tex',
+ nls = 'tex',
+ thm = 'tex',
+ eps_tex = 'tex',
+ pygtex = 'tex',
+ pygstyle = 'tex',
+ clo = 'tex',
+ aux = 'tex',
+ brf = 'tex',
+ ind = 'tex',
+ lof = 'tex',
+ loe = 'tex',
+ nav = 'tex',
+ vrb = 'tex',
+ ins = 'tex',
+ tikz = 'tex',
+ bbx = 'tex',
+ cbx = 'tex',
+ beamer = 'tex',
cls = detect.cls,
texi = 'texinfo',
txi = 'texinfo',
@@ -1035,6 +1098,7 @@ local extension = {
mts = 'typescript',
cts = 'typescript',
tsx = 'typescriptreact',
+ tsp = 'typespec',
uc = 'uc',
uit = 'uil',
uil = 'uil',
@@ -1062,6 +1126,7 @@ local extension = {
vdmrt = 'vdmrt',
vdmsl = 'vdmsl',
vdm = 'vdmsl',
+ vto = 'vento',
vr = 'vera',
vri = 'vera',
vrh = 'vera',
@@ -1110,6 +1175,14 @@ local extension = {
csproj = 'xml',
wpl = 'xml',
xmi = 'xml',
+ xpr = 'xml',
+ xpfm = 'xml',
+ spfm = 'xml',
+ bxml = 'xml',
+ xcu = 'xml',
+ xlb = 'xml',
+ xlc = 'xml',
+ xba = 'xml',
xpm = detect_line1('XPM2', 'xpm2', 'xpm'),
xpm2 = 'xpm2',
xqy = 'xquery',
@@ -1127,6 +1200,7 @@ local extension = {
yml = 'yaml',
yaml = 'yaml',
eyaml = 'yaml',
+ mplstyle = 'yaml',
yang = 'yang',
yuck = 'yuck',
z8a = 'z8a',
@@ -1136,6 +1210,8 @@ local extension = {
zut = 'zimbutempl',
zs = 'zserio',
zsh = 'zsh',
+ zunit = 'zsh',
+ ['zsh-theme'] = 'zsh',
vala = 'vala',
web = detect.web,
pl = detect.pl,
@@ -1229,7 +1305,12 @@ local filename = {
['/etc/default/cdrdao'] = 'cdrdaoconf',
['/etc/defaults/cdrdao'] = 'cdrdaoconf',
['cfengine.conf'] = 'cfengine',
+ cgdbrc = 'cgdbrc',
+ ['init.trans'] = 'clojure',
+ ['.trans'] = 'clojure',
['CMakeLists.txt'] = 'cmake',
+ ['CMakeCache.txt'] = 'cmakecache',
+ ['.cling_history'] = 'cpp',
['.alias'] = detect.csh,
['.cshrc'] = detect.csh,
['.login'] = detect.csh,
@@ -1237,6 +1318,12 @@ local filename = {
['csh.login'] = detect.csh,
['csh.logout'] = detect.csh,
['auto.master'] = 'conf',
+ ['texdoc.cnf'] = 'conf',
+ ['.x11vncrc'] = 'conf',
+ ['.chktexrc'] = 'conf',
+ ['.ripgreprc'] = 'conf',
+ ripgreprc = 'conf',
+ ['.mbsyncrc'] = 'conf',
['configure.in'] = 'config',
['configure.ac'] = 'config',
crontab = 'crontab',
@@ -1262,12 +1349,31 @@ local filename = {
npmrc = 'dosini',
['/etc/yum.conf'] = 'dosini',
['.npmrc'] = 'dosini',
- ['/etc/pacman.conf'] = 'confini',
+ ['pip.conf'] = 'dosini',
+ ['setup.cfg'] = 'dosini',
+ ['pudb.cfg'] = 'dosini',
+ ['.coveragerc'] = 'dosini',
+ ['.pypirc'] = 'dosini',
+ ['.pylintrc'] = 'dosini',
+ ['pylintrc'] = 'dosini',
+ ['.replyrc'] = 'dosini',
+ ['.gitlint'] = 'dosini',
+ ['.oelint.cfg'] = 'dosini',
+ ['psprint.conf'] = 'dosini',
+ sofficerc = 'dosini',
+ ['mimeapps.list'] = 'dosini',
+ ['.wakatime.cfg'] = 'dosini',
+ ['nfs.conf'] = 'dosini',
+ ['nfsmount.conf'] = 'dosini',
+ ['.notmuch-config'] = 'dosini',
+ ['pacman.conf'] = 'confini',
+ ['paru.conf'] = 'confini',
['mpv.conf'] = 'confini',
dune = 'dune',
jbuild = 'dune',
['dune-workspace'] = 'dune',
['dune-project'] = 'dune',
+ Earthfile = 'earthfile',
['.editorconfig'] = 'editorconfig',
['elinks.conf'] = 'elinks',
['mix.lock'] = 'elixir',
@@ -1302,7 +1408,7 @@ local filename = {
['.gnashpluginrc'] = 'gnash',
gnashpluginrc = 'gnash',
gnashrc = 'gnash',
- ['.gnuplot'] = 'gnuplot',
+ ['.gnuplot_history'] = 'gnuplot',
['go.sum'] = 'gosum',
['go.work.sum'] = 'gosum',
['go.work'] = 'gowork',
@@ -1328,6 +1434,10 @@ local filename = {
['/etc/host.conf'] = 'hostconf',
['/etc/hosts.allow'] = 'hostsaccess',
['/etc/hosts.deny'] = 'hostsaccess',
+ ['hyprland.conf'] = 'hyprlang',
+ ['hyprpaper.conf'] = 'hyprlang',
+ ['hypridle.conf'] = 'hyprlang',
+ ['hyprlock.conf'] = 'hyprlang',
['/.icewm/menu'] = 'icemenu',
['.indent.pro'] = 'indent',
indentrc = 'indent',
@@ -1335,17 +1445,21 @@ local filename = {
['ipf.conf'] = 'ipfilter',
['ipf6.conf'] = 'ipfilter',
['ipf.rules'] = 'ipfilter',
+ ['.node_repl_history'] = 'javascript',
['Pipfile.lock'] = 'json',
['.firebaserc'] = 'json',
['.prettierrc'] = 'json',
['.stylelintrc'] = 'json',
+ ['flake.lock'] = 'json',
['.babelrc'] = 'jsonc',
['.eslintrc'] = 'jsonc',
['.hintrc'] = 'jsonc',
+ ['.jscsrc'] = 'jsonc',
['.jsfmtrc'] = 'jsonc',
['.jshintrc'] = 'jsonc',
['.luaurc'] = 'jsonc',
['.swrc'] = 'jsonc',
+ ['.vsconfig'] = 'jsonc',
['.justfile'] = 'just',
Kconfig = 'kconfig',
['Kconfig.debug'] = 'kconfig',
@@ -1365,10 +1479,14 @@ local filename = {
['.lsl'] = detect.lsl,
['.busted'] = 'lua',
['.luacheckrc'] = 'lua',
+ ['.lua_history'] = 'lua',
+ ['config.ld'] = 'lua',
+ ['rock_manifest'] = 'lua',
['lynx.cfg'] = 'lynx',
['m3overrides'] = 'm3build',
['m3makefile'] = 'm3build',
['cm3.cfg'] = 'm3quake',
+ ['.m4_history'] = 'm4',
['.followup'] = 'mail',
['.article'] = 'mail',
['.letter'] = 'mail',
@@ -1376,6 +1494,7 @@ local filename = {
['/etc/mail/aliases'] = 'mailaliases',
mailcap = 'mailcap',
['.mailcap'] = 'mailcap',
+ Kbuild = 'make',
['/etc/man.conf'] = 'manconf',
['man.config'] = 'manconf',
['maxima-init.mac'] = 'maxima',
@@ -1389,6 +1508,8 @@ local filename = {
['mplayer.conf'] = 'mplayerconf',
mrxvtrc = 'mrxvtrc',
['.mrxvtrc'] = 'mrxvtrc',
+ ['.msmtprc'] = 'msmtp',
+ ['.mysql_history'] = 'mysql',
['/etc/nanorc'] = 'nanorc',
Neomuttrc = 'neomuttrc',
['.netrc'] = 'netrc',
@@ -1397,6 +1518,7 @@ local filename = {
['.octaverc'] = 'octave',
octaverc = 'octave',
['octave.conf'] = 'octave',
+ ['.ondirrc'] = 'ondir',
opam = 'opam',
['pacman.log'] = 'pacmanlog',
['/etc/pam.conf'] = 'pamconf',
@@ -1441,8 +1563,11 @@ local filename = {
['MANIFEST.in'] = 'pymanifest',
['.pythonstartup'] = 'python',
['.pythonrc'] = 'python',
+ ['.python_history'] = 'python',
+ ['.jline-jython.history'] = 'python',
SConstruct = 'python',
qmldir = 'qmldir',
+ ['.Rhistory'] = 'r',
['.Rprofile'] = 'r',
Rprofile = 'r',
['Rprofile.site'] = 'r',
@@ -1452,12 +1577,16 @@ local filename = {
['.inputrc'] = 'readline',
['.reminders'] = 'remind',
['requirements.txt'] = 'requirements',
+ ['constraints.txt'] = 'requirements',
+ ['requirements.in'] = 'requirements',
['resolv.conf'] = 'resolv',
['robots.txt'] = 'robots',
Gemfile = 'ruby',
Puppetfile = 'ruby',
['.irbrc'] = 'ruby',
irbrc = 'ruby',
+ ['.irb_history'] = 'ruby',
+ irb_history = 'ruby',
Vagrantfile = 'ruby',
['smb.conf'] = 'samba',
screenrc = 'screen',
@@ -1467,10 +1596,15 @@ local filename = {
['/etc/services'] = 'services',
['/etc/serial.conf'] = 'setserial',
['/etc/udev/cdsymlinks.conf'] = 'sh',
+ ['.ash_history'] = 'sh',
+ ['makepkg.conf'] = 'sh',
+ ['.makepkg.conf'] = 'sh',
+ ['user-dirs.dirs'] = 'sh',
+ ['user-dirs.defaults'] = 'sh',
+ ['.xprofile'] = 'sh',
['bash.bashrc'] = detect.bash,
bashrc = detect.bash,
['.bashrc'] = detect.bash,
- ['.env'] = detect.sh,
['.kshrc'] = detect.ksh,
['.profile'] = detect.sh,
['/etc/profile'] = detect.sh,
@@ -1484,6 +1618,7 @@ local filename = {
['/etc/slp.spi'] = 'slpspi',
['.slrnrc'] = 'slrnrc',
['sendmail.cf'] = 'sm',
+ ['.sqlite_history'] = 'sql',
['squid.conf'] = 'squid',
['ssh_config'] = 'sshconfig',
['sshd_config'] = 'sshdconfig',
@@ -1496,7 +1631,10 @@ local filename = {
['undo.data'] = 'taskdata',
['.tclshrc'] = 'tcl',
['.wishrc'] = 'tcl',
+ ['.tclsh-history'] = 'tcl',
['tclsh.rc'] = 'tcl',
+ ['.xsctcmdhistory'] = 'tcl',
+ ['.xsdbcmdhistory'] = 'tcl',
['texmf.cnf'] = 'texmf',
COPYING = 'text',
README = 'text',
@@ -1513,13 +1651,17 @@ local filename = {
['Gopkg.lock'] = 'toml',
['/.cargo/credentials'] = 'toml',
['Cargo.lock'] = 'toml',
+ ['.black'] = 'toml',
+ black = detect_line1('tool%.black', 'toml', nil),
['trustees.conf'] = 'trustees',
+ ['.ts_node_repl_history'] = 'typescript',
['/etc/udev/udev.conf'] = 'udevconf',
['/etc/updatedb.conf'] = 'updatedb',
['fdrupstream.log'] = 'upstreamlog',
vgrindefs = 'vgrindefs',
['.exrc'] = 'vim',
['_exrc'] = 'vim',
+ ['.netrwhist'] = 'vim',
['_viminfo'] = 'viminfo',
['.viminfo'] = 'viminfo',
['.wgetrc'] = 'wget',
@@ -1541,15 +1683,20 @@ local filename = {
fglrxrc = 'xml',
['/etc/blkid.tab'] = 'xml',
['/etc/blkid.tab.old'] = 'xml',
+ ['fonts.conf'] = 'xml',
['.clangd'] = 'yaml',
['.clang-format'] = 'yaml',
['.clang-tidy'] = 'yaml',
+ ['yarn.lock'] = 'yaml',
+ matplotlibrc = 'yaml',
+ zathurarc = 'zathurarc',
['/etc/zprofile'] = 'zsh',
['.zlogin'] = 'zsh',
['.zlogout'] = 'zsh',
['.zshrc'] = 'zsh',
['.zprofile'] = 'zsh',
['.zcompdump'] = 'zsh',
+ ['.zsh_history'] = 'zsh',
['.zshenv'] = 'zsh',
['.zfbfmarks'] = 'zsh',
-- END FILENAME
@@ -1599,9 +1746,8 @@ local pattern = {
['.*%.blade%.php'] = 'blade',
['bzr_log%..*'] = 'bzr',
['.*enlightenment/.*%.cfg'] = 'c',
- ['${HOME}/cabal%.config'] = 'cabalconfig',
- ['${HOME}/%.config/cabal/config'] = 'cabalconfig',
- ['${XDG_CONFIG_HOME}/cabal/config'] = 'cabalconfig',
+ ['.*/%.cabal/config'] = 'cabalconfig',
+ ['.*/cabal/config'] = 'cabalconfig',
['cabal%.project%..*'] = starsetf('cabalproject'),
['.*/%.calendar/.*'] = starsetf('calendar'),
['.*/share/calendar/.*/calendar%..*'] = starsetf('calendar'),
@@ -1619,6 +1765,7 @@ local pattern = {
},
['[cC]hange[lL]og.*'] = starsetf(detect.changelog),
['.*%.%.ch'] = 'chill',
+ ['.*/etc/translate%-shell'] = 'clojure',
['.*%.cmake%.in'] = 'cmake',
-- */cmus/rc and */.cmus/rc
['.*/%.?cmus/rc'] = 'cmusrc',
@@ -1653,8 +1800,12 @@ local pattern = {
['php%.ini%-.*'] = 'dosini',
['.*/%.aws/config'] = 'confini',
['.*/%.aws/credentials'] = 'confini',
- ['.*/etc/pacman%.conf'] = 'confini',
['.*/etc/yum%.conf'] = 'dosini',
+ ['.*/lxqt/.*%.conf'] = 'dosini',
+ ['.*/screengrab/.*%.conf'] = 'dosini',
+ ['.*/bpython/config'] = 'dosini',
+ ['.*/mypy/config'] = 'dosini',
+ ['.*/flatpak/repo/config'] = 'dosini',
['.*lvs'] = 'dracula',
['.*lpe'] = 'dracula',
['.*/dtrace/.*%.d'] = 'dtrace',
@@ -1745,12 +1896,14 @@ local pattern = {
['.*%.[Ss][Uu][Bb]'] = 'krl',
['lilo%.conf.*'] = starsetf('lilo'),
['.*/etc/logcheck/.*%.d.*/.*'] = starsetf('logcheck'),
+ ['.*/ldscripts/.*'] = 'ld',
['.*lftp/rc'] = 'lftp',
['.*/%.libao'] = 'libao',
['.*/etc/libao%.conf'] = 'libao',
['.*/etc/.*limits%.conf'] = 'limits',
['.*/etc/limits'] = 'limits',
['.*/etc/.*limits%.d/.*%.conf'] = 'limits',
+ ['.*/supertux2/config'] = 'lisp',
['.*/LiteStep/.*/.*%.rc'] = 'litestep',
['.*/etc/login%.access'] = 'loginaccess',
['.*/etc/login%.defs'] = 'logindefs',
@@ -1889,6 +2042,7 @@ local pattern = {
['.*%.[1-9]'] = detect.nroff,
['.*%.ml%.cppo'] = 'ocaml',
['.*%.mli%.cppo'] = 'ocaml',
+ ['.*/octave/history'] = 'octave',
['.*%.opam%.template'] = 'opam',
['.*/openvpn/.*/.*%.conf'] = 'openvpn',
['.*%.[Oo][Pp][Ll]'] = 'opl',
@@ -1918,6 +2072,9 @@ local pattern = {
['.*/queries/.*%.scm'] = 'query', -- treesitter queries (Neovim only)
['.*,v'] = 'rcs',
['%.reminders.*'] = starsetf('remind'),
+ ['.*%-requirements%.txt'] = 'requirements',
+ ['requirements/.*%.txt'] = 'requirements',
+ ['requires/.*%.txt'] = 'requirements',
['[rR]akefile.*'] = starsetf('ruby'),
['[rR]antfile'] = 'ruby',
['[rR]akefile'] = 'ruby',
@@ -1927,7 +2084,9 @@ local pattern = {
['.*/etc/services'] = 'services',
['.*/etc/serial%.conf'] = 'setserial',
['.*/etc/udev/cdsymlinks%.conf'] = 'sh',
+ ['.*/neofetch/config%.conf'] = 'sh',
['%.bash[_%-]aliases'] = detect.bash,
+ ['%.bash[_%-]history'] = detect.bash,
['%.bash[_%-]logout'] = detect.bash,
['%.bash[_%-]profile'] = detect.bash,
['%.kshrc.*'] = detect.ksh,
@@ -1979,6 +2138,7 @@ local pattern = {
['.*termcap.*'] = starsetf(function(path, bufnr)
return require('vim.filetype.detect').printcap('term')
end),
+ ['.*/tex/latex/.*%.cfg'] = 'tex',
['.*%.t%.html'] = 'tilde',
['%.?tmux.*%.conf'] = 'tmux',
['%.?tmux.*%.conf.*'] = { 'tmux', { priority = -1 } },
@@ -1996,6 +2156,7 @@ local pattern = {
['.*/%.init/.*%.conf'] = 'upstart',
['.*/usr/share/upstart/.*%.override'] = 'upstart',
['.*%.[Ll][Oo][Gg]'] = detect.log,
+ ['.*/etc/config/.*'] = starsetf(detect.uci),
['.*%.vhdl_[0-9].*'] = starsetf('vhdl'),
['.*%.ws[fc]'] = 'wsh',
['.*/Xresources/.*'] = starsetf('xdefaults'),
@@ -2022,6 +2183,7 @@ local pattern = {
-- Increase priority to run before the pattern below
['XF86Config%-4.*'] = starsetf(detect.xfree86_v4, { priority = -math.huge + 1 }),
['XF86Config.*'] = starsetf(detect.xfree86_v3),
+ ['.*/%.bundle/config'] = 'yaml',
['%.zcompdump.*'] = starsetf('zsh'),
-- .zlog* and zlog*
['%.?zlog.*'] = starsetf('zsh'),
@@ -2029,7 +2191,7 @@ local pattern = {
['%.?zsh.*'] = starsetf('zsh'),
-- Ignored extension
['.*~'] = function(path, bufnr)
- local short = path:gsub('~$', '', 1)
+ local short = path:gsub('~+$', '', 1)
if path ~= short and short ~= '' then
return M.match({ buf = bufnr, filename = fn.fnameescape(short) })
end
@@ -2157,7 +2319,6 @@ end
--- vim.filetype.add {
--- pattern = {
--- ['.*'] = {
---- priority = -math.huge,
--- function(path, bufnr)
--- local content = vim.api.nvim_buf_get_lines(bufnr, 0, 1, false)[1] or ''
--- if vim.regex([[^#!.*\\<mine\\>]]):match_str(content) ~= nil then
@@ -2166,6 +2327,7 @@ end
--- return 'drawing'
--- end
--- end,
+--- { priority = -math.huge },
--- },
--- },
--- }
@@ -2385,7 +2547,9 @@ function M.match(args)
end
-- Next, check file extension
- local ext = fn.fnamemodify(name, ':e')
+ -- Don't use fnamemodify() with :e modifier here,
+ -- as that's empty when there is only an extension.
+ local ext = name:match('%.([^.]-)$') or ''
ft, on_detect = dispatch(extension[ext], path, bufnr)
if ft then
return ft, on_detect
diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua
index 3db4f2bcdc..ba86d8de5a 100644
--- a/runtime/lua/vim/filetype/detect.lua
+++ b/runtime/lua/vim/filetype/detect.lua
@@ -458,6 +458,9 @@ end
--- @type vim.filetype.mapfn
function M.def(_, bufnr)
+ if getline(bufnr, 1):find('%%%%') then
+ return 'tex'
+ end
if vim.g.filetype_def == 'modula2' or is_modula2(bufnr) then
return modula2(bufnr)
end
@@ -738,7 +741,9 @@ end
--- @type vim.filetype.mapfn
function M.inp(_, bufnr)
- if getline(bufnr, 1):find('^%*') then
+ if getline(bufnr, 1):find('%%%%') then
+ return 'tex'
+ elseif getline(bufnr, 1):find('^%*') then
return 'abaqus'
else
for _, line in ipairs(getlines(bufnr, 1, 500)) do
@@ -889,6 +894,11 @@ local function m4(contents)
end
end
+--- @type vim.filetype.mapfn
+function M.markdown(_, _)
+ return vim.g.filetype_md or 'markdown'
+end
+
--- Rely on the file to start with a comment.
--- MS message text files use ';', Sendmail files use '#' or 'dnl'
--- @type vim.filetype.mapfn
@@ -1131,12 +1141,14 @@ end
--- Distinguish between "default", Prolog and Cproto prototype file.
--- @type vim.filetype.mapfn
function M.proto(_, bufnr)
- -- Cproto files have a comment in the first line and a function prototype in
- -- the second line, it always ends in ";". Indent files may also have
- -- comments, thus we can't match comments to see the difference.
- -- IDL files can have a single ';' in the second line, require at least one
- -- character before the ';'.
- if getline(bufnr, 2):find('.;$') then
+ if getline(bufnr, 2):find('/%* Generated automatically %*/') then
+ return 'c'
+ elseif getline(bufnr, 2):find('.;$') then
+ -- Cproto files have a comment in the first line and a function prototype in
+ -- the second line, it always ends in ";". Indent files may also have
+ -- comments, thus we can't match comments to see the difference.
+ -- IDL files can have a single ';' in the second line, require at least one
+ -- character before the ';'.
return 'cpp'
end
-- Recognize Prolog by specific text in the first non-empty line;
@@ -1574,6 +1586,26 @@ function M.typ(_, bufnr)
return 'typst'
end
+--- @type vim.filetype.mapfn
+function M.uci(_, bufnr)
+ -- Return "uci" iff the file has a config or package statement near the
+ -- top of the file and all preceding lines were comments or blank.
+ for _, line in ipairs(getlines(bufnr, 1, 3)) do
+ -- Match a config or package statement at the start of the line.
+ if
+ line:find('^%s*[cp]%s+%S')
+ or line:find('^%s*config%s+%S')
+ or line:find('^%s*package%s+%S')
+ then
+ return 'uci'
+ end
+ -- Match a line that is either all blank or blank followed by a comment
+ if not (line:find('^%s*$') or line:find('^%s*#')) then
+ break
+ end
+ end
+end
+
-- Determine if a .v file is Verilog, V, or Coq
--- @type vim.filetype.mapfn
function M.v(_, bufnr)
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
index f9fe122f01..b05220ee2c 100644
--- a/runtime/lua/vim/fs.lua
+++ b/runtime/lua/vim/fs.lua
@@ -1,6 +1,7 @@
local M = {}
local iswin = vim.uv.os_uname().sysname == 'Windows_NT'
+local os_sep = iswin and '\\' or '/'
--- Iterate over all the parents of the given path.
---
@@ -47,19 +48,23 @@ function M.dirname(file)
return nil
end
vim.validate({ file = { file, 's' } })
- if iswin and file:match('^%w:[\\/]?$') then
- return (file:gsub('\\', '/'))
- elseif not file:match('[\\/]') then
+ if iswin then
+ file = file:gsub(os_sep, '/') --[[@as string]]
+ if file:match('^%w:/?$') then
+ return file
+ end
+ end
+ if not file:match('/') then
return '.'
elseif file == '/' or file:match('^/[^/]+$') then
return '/'
end
---@type string
- local dir = file:match('[/\\]$') and file:sub(1, #file - 1) or file:match('^([/\\]?.+)[/\\]')
+ local dir = file:match('/$') and file:sub(1, #file - 1) or file:match('^(/?.+)/')
if iswin and dir:match('^%w:$') then
return dir .. '/'
end
- return (dir:gsub('\\', '/'))
+ return dir
end
--- Return the basename of the given path
@@ -72,10 +77,13 @@ function M.basename(file)
return nil
end
vim.validate({ file = { file, 's' } })
- if iswin and file:match('^%w:[\\/]?$') then
- return ''
+ if iswin then
+ file = file:gsub(os_sep, '/') --[[@as string]]
+ if file:match('^%w:/?$') then
+ return ''
+ end
end
- return file:match('[/\\]$') and '' or (file:match('[^\\/]*$'):gsub('\\', '/'))
+ return file:match('/$') and '' or (file:match('[^/]*$'))
end
--- Concatenate directories and/or file paths into a single path with normalization
@@ -112,8 +120,9 @@ function M.dir(path, opts)
skip = { opts.skip, { 'function' }, true },
})
+ path = M.normalize(path)
if not opts.depth or opts.depth == 1 then
- local fs = vim.uv.fs_scandir(M.normalize(path))
+ local fs = vim.uv.fs_scandir(path)
return function()
if not fs then
return
@@ -129,7 +138,7 @@ function M.dir(path, opts)
--- @type string, integer
local dir0, level = unpack(table.remove(dirs, 1))
local dir = level == 1 and dir0 or M.joinpath(path, dir0)
- local fs = vim.uv.fs_scandir(M.normalize(dir))
+ local fs = vim.uv.fs_scandir(dir)
while fs do
local name, t = vim.uv.fs_scandir_next(fs)
if not name then
@@ -189,13 +198,6 @@ end
--- Examples:
---
--- ```lua
---- -- location of Cargo.toml from the current buffer's path
---- local cargo = vim.fs.find('Cargo.toml', {
---- upward = true,
---- stop = vim.uv.os_homedir(),
---- path = vim.fs.dirname(vim.api.nvim_buf_get_name(0)),
---- })
----
--- -- list all test directories under the runtime directory
--- local test_dirs = vim.fs.find(
--- {'test', 'tst', 'testdir'},
@@ -326,29 +328,204 @@ function M.find(names, opts)
return matches
end
+--- Find the first parent directory containing a specific "marker", relative to a file path or
+--- buffer.
+---
+--- If the buffer is unnamed (has no backing file) or has a non-empty 'buftype' then the search
+--- begins from Nvim's |current-directory|.
+---
+--- Example:
+---
+--- ```lua
+--- -- Find the root of a Python project, starting from file 'main.py'
+--- vim.fs.root(vim.fs.joinpath(vim.env.PWD, 'main.py'), {'pyproject.toml', 'setup.py' })
+---
+--- -- Find the root of a git repository
+--- vim.fs.root(0, '.git')
+---
+--- -- Find the parent directory containing any file with a .csproj extension
+--- vim.fs.root(0, function(name, path)
+--- return name:match('%.csproj$') ~= nil
+--- end)
+--- ```
+---
+--- @param source integer|string Buffer number (0 for current buffer) or file path (absolute or
+--- relative to the |current-directory|) to begin the search from.
+--- @param marker (string|string[]|fun(name: string, path: string): boolean) A marker, or list
+--- of markers, to search for. If a function, the function is called for each
+--- evaluated item and should return true if {name} and {path} are a match.
+--- @return string? # Directory path containing one of the given markers, or nil if no directory was
+--- found.
+function M.root(source, marker)
+ assert(source, 'missing required argument: source')
+ assert(marker, 'missing required argument: marker')
+
+ local path ---@type string
+ if type(source) == 'string' then
+ path = source
+ elseif type(source) == 'number' then
+ if vim.bo[source].buftype ~= '' then
+ path = assert(vim.uv.cwd())
+ else
+ path = vim.api.nvim_buf_get_name(source)
+ end
+ else
+ error('invalid type for argument "source": expected string or buffer number')
+ end
+
+ local paths = M.find(marker, {
+ upward = true,
+ path = vim.fn.fnamemodify(path, ':p:h'),
+ })
+
+ if #paths == 0 then
+ return nil
+ end
+
+ return vim.fs.dirname(paths[1])
+end
+
+--- Split a Windows path into a prefix and a body, such that the body can be processed like a POSIX
+--- path. The path must use forward slashes as path separator.
+---
+--- Does not check if the path is a valid Windows path. Invalid paths will give invalid results.
+---
+--- Examples:
+--- - `//./C:/foo/bar` -> `//./C:`, `/foo/bar`
+--- - `//?/UNC/server/share/foo/bar` -> `//?/UNC/server/share`, `/foo/bar`
+--- - `//./system07/C$/foo/bar` -> `//./system07`, `/C$/foo/bar`
+--- - `C:/foo/bar` -> `C:`, `/foo/bar`
+--- - `C:foo/bar` -> `C:`, `foo/bar`
+---
+--- @param path string Path to split.
+--- @return string, string, boolean : prefix, body, whether path is invalid.
+local function split_windows_path(path)
+ local prefix = ''
+
+ --- Match pattern. If there is a match, move the matched pattern from the path to the prefix.
+ --- Returns the matched pattern.
+ ---
+ --- @param pattern string Pattern to match.
+ --- @return string|nil Matched pattern
+ local function match_to_prefix(pattern)
+ local match = path:match(pattern)
+
+ if match then
+ prefix = prefix .. match --[[ @as string ]]
+ path = path:sub(#match + 1)
+ end
+
+ return match
+ end
+
+ local function process_unc_path()
+ return match_to_prefix('[^/]+/+[^/]+/+')
+ end
+
+ if match_to_prefix('^//[?.]/') then
+ -- Device paths
+ local device = match_to_prefix('[^/]+/+')
+
+ -- Return early if device pattern doesn't match, or if device is UNC and it's not a valid path
+ if not device or (device:match('^UNC/+$') and not process_unc_path()) then
+ return prefix, path, false
+ end
+ elseif match_to_prefix('^//') then
+ -- Process UNC path, return early if it's invalid
+ if not process_unc_path() then
+ return prefix, path, false
+ end
+ elseif path:match('^%w:') then
+ -- Drive paths
+ prefix, path = path:sub(1, 2), path:sub(3)
+ end
+
+ -- If there are slashes at the end of the prefix, move them to the start of the body. This is to
+ -- ensure that the body is treated as an absolute path. For paths like C:foo/bar, there are no
+ -- slashes at the end of the prefix, so it will be treated as a relative path, as it should be.
+ local trailing_slash = prefix:match('/+$')
+
+ if trailing_slash then
+ prefix = prefix:sub(1, -1 - #trailing_slash)
+ path = trailing_slash .. path --[[ @as string ]]
+ end
+
+ return prefix, path, true
+end
+
+--- Resolve `.` and `..` components in a POSIX-style path. This also removes extraneous slashes.
+--- `..` is not resolved if the path is relative and resolving it requires the path to be absolute.
+--- If a relative path resolves to the current directory, an empty string is returned.
+---
+--- @see M.normalize()
+--- @param path string Path to resolve.
+--- @return string Resolved path.
+local function path_resolve_dot(path)
+ local is_path_absolute = vim.startswith(path, '/')
+ local new_path_components = {}
+
+ for component in vim.gsplit(path, '/') do
+ if component == '.' or component == '' then -- luacheck: ignore 542
+ -- Skip `.` components and empty components
+ elseif component == '..' then
+ if #new_path_components > 0 and new_path_components[#new_path_components] ~= '..' then
+ -- For `..`, remove the last component if we're still inside the current directory, except
+ -- when the last component is `..` itself
+ table.remove(new_path_components)
+ elseif is_path_absolute then -- luacheck: ignore 542
+ -- Reached the root directory in absolute path, do nothing
+ else
+ -- Reached current directory in relative path, add `..` to the path
+ table.insert(new_path_components, component)
+ end
+ else
+ table.insert(new_path_components, component)
+ end
+ end
+
+ return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/')
+end
+
--- @class vim.fs.normalize.Opts
--- @inlinedoc
---
--- Expand environment variables.
--- (default: `true`)
---- @field expand_env boolean
-
---- Normalize a path to a standard format. A tilde (~) character at the
---- beginning of the path is expanded to the user's home directory and any
---- backslash (\) characters are converted to forward slashes (/). Environment
---- variables are also expanded.
+--- @field expand_env? boolean
---
---- Examples:
+--- @field package _fast? boolean
---
---- ```lua
---- vim.fs.normalize('C:\\\\Users\\\\jdoe')
---- -- 'C:/Users/jdoe'
+--- Path is a Windows path.
+--- (default: `true` in Windows, `false` otherwise)
+--- @field win? boolean
+
+--- Normalize a path to a standard format. A tilde (~) character at the beginning of the path is
+--- expanded to the user's home directory and environment variables are also expanded. "." and ".."
+--- components are also resolved, except when the path is relative and trying to resolve it would
+--- result in an absolute path.
+--- - "." as the only part in a relative path:
+--- - "." => "."
+--- - "././" => "."
+--- - ".." when it leads outside the current directory
+--- - "foo/../../bar" => "../bar"
+--- - "../../foo" => "../../foo"
+--- - ".." in the root directory returns the root directory.
+--- - "/../../" => "/"
---
---- vim.fs.normalize('~/src/neovim')
---- -- '/home/jdoe/src/neovim'
+--- On Windows, backslash (\) characters are converted to forward slashes (/).
---
---- vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim')
---- -- '/Users/jdoe/.config/nvim/init.vim'
+--- Examples:
+--- ```lua
+--- [[C:\Users\jdoe]] => "C:/Users/jdoe"
+--- "~/src/neovim" => "/home/jdoe/src/neovim"
+--- "$XDG_CONFIG_HOME/nvim/init.vim" => "/Users/jdoe/.config/nvim/init.vim"
+--- "~/src/nvim/api/../tui/./tui.c" => "/home/jdoe/src/nvim/tui/tui.c"
+--- "./foo/bar" => "foo/bar"
+--- "foo/../../../bar" => "../../bar"
+--- "/home/jdoe/../../../bar" => "/bar"
+--- "C:foo/../../baz" => "C:../baz"
+--- "C:/foo/../../baz" => "C:/baz"
+--- [[\\?\UNC\server\share\foo\..\..\..\bar]] => "//?/UNC/server/share/bar"
--- ```
---
---@param path (string) Path to normalize
@@ -357,28 +534,79 @@ end
function M.normalize(path, opts)
opts = opts or {}
- vim.validate({
- path = { path, { 'string' } },
- expand_env = { opts.expand_env, { 'boolean' }, true },
- })
+ if not opts._fast then
+ vim.validate({
+ path = { path, { 'string' } },
+ expand_env = { opts.expand_env, { 'boolean' }, true },
+ win = { opts.win, { 'boolean' }, true },
+ })
+ end
+
+ local win = opts.win == nil and iswin or not not opts.win
+ local os_sep_local = win and '\\' or '/'
+
+ -- Empty path is already normalized
+ if path == '' then
+ return ''
+ end
- if path:sub(1, 1) == '~' then
+ -- Expand ~ to users home directory
+ if vim.startswith(path, '~') then
local home = vim.uv.os_homedir() or '~'
- if home:sub(-1) == '\\' or home:sub(-1) == '/' then
+ if home:sub(-1) == os_sep_local then
home = home:sub(1, -2)
end
path = home .. path:sub(2)
end
+ -- Expand environment variables if `opts.expand_env` isn't `false`
if opts.expand_env == nil or opts.expand_env then
path = path:gsub('%$([%w_]+)', vim.uv.os_getenv)
end
- path = path:gsub('\\', '/'):gsub('/+', '/')
- if iswin and path:match('^%w:/$') then
- return path
+ if win then
+ -- Convert path separator to `/`
+ path = path:gsub(os_sep_local, '/')
+ end
+
+ -- Check for double slashes at the start of the path because they have special meaning
+ local double_slash = false
+ if not opts._fast then
+ double_slash = vim.startswith(path, '//') and not vim.startswith(path, '///')
+ end
+
+ local prefix = ''
+
+ if win then
+ local is_valid --- @type boolean
+ -- Split Windows paths into prefix and body to make processing easier
+ prefix, path, is_valid = split_windows_path(path)
+
+ -- If path is not valid, return it as-is
+ if not is_valid then
+ return prefix .. path
+ end
+
+ -- Remove extraneous slashes from the prefix
+ prefix = prefix:gsub('/+', '/')
+ end
+
+ if not opts._fast then
+ -- Resolve `.` and `..` components and remove extraneous slashes from path, then recombine prefix
+ -- and path.
+ path = path_resolve_dot(path)
end
- return (path:gsub('(.)/$', '%1'))
+
+ -- Preserve leading double slashes as they indicate UNC paths and DOS device paths in
+ -- Windows and have implementation-defined behavior in POSIX.
+ path = (double_slash and '/' or '') .. prefix .. path
+
+ -- Change empty path to `.`
+ if path == '' then
+ path = '.'
+ end
+
+ return path
end
return M
diff --git a/runtime/lua/vim/func.lua b/runtime/lua/vim/func.lua
index 206d1bae95..f71659ffb4 100644
--- a/runtime/lua/vim/func.lua
+++ b/runtime/lua/vim/func.lua
@@ -32,10 +32,11 @@ local M = {}
--- first n arguments passed to {fn}.
---
--- @param fn F Function to memoize.
+--- @param strong? boolean Do not use a weak table
--- @return F # Memoized version of {fn}
--- @nodoc
-function M._memoize(hash, fn)
- return require('vim.func._memoize')(hash, fn)
+function M._memoize(hash, fn, strong)
+ return require('vim.func._memoize')(hash, fn, strong)
end
return M
diff --git a/runtime/lua/vim/func/_memoize.lua b/runtime/lua/vim/func/_memoize.lua
index 835bf64c93..65210351bf 100644
--- a/runtime/lua/vim/func/_memoize.lua
+++ b/runtime/lua/vim/func/_memoize.lua
@@ -36,15 +36,19 @@ end
--- @generic F: function
--- @param hash integer|string|fun(...): any
--- @param fn F
+--- @param strong? boolean
--- @return F
-return function(hash, fn)
+return function(hash, fn, strong)
vim.validate({
hash = { hash, { 'number', 'string', 'function' } },
fn = { fn, 'function' },
})
---@type table<any,table<any,any>>
- local cache = setmetatable({}, { __mode = 'kv' })
+ local cache = {}
+ if not strong then
+ setmetatable(cache, { __mode = 'kv' })
+ end
hash = resolve_hash(hash)
diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua
index f6f7abef8f..f40f04a064 100644
--- a/runtime/lua/vim/health.lua
+++ b/runtime/lua/vim/health.lua
@@ -1,43 +1,96 @@
+--- @brief
+---<pre>help
+--- health.vim is a minimal framework to help users troubleshoot configuration and
+--- any other environment conditions that a plugin might care about. Nvim ships
+--- with healthchecks for configuration, performance, python support, ruby
+--- support, clipboard support, and more.
+---
+--- To run all healthchecks, use: >vim
+---
+--- :checkhealth
+--- <
+--- Plugin authors are encouraged to write new healthchecks. |health-dev|
+---
+--- Commands *health-commands*
+---
+--- *:che* *:checkhealth*
+--- :che[ckhealth] Run all healthchecks.
+--- *E5009*
+--- Nvim depends on |$VIMRUNTIME|, 'runtimepath' and 'packpath' to
+--- find the standard "runtime files" for syntax highlighting,
+--- filetype-specific behavior, and standard plugins (including
+--- :checkhealth). If the runtime files cannot be found then
+--- those features will not work.
+---
+--- :che[ckhealth] {plugins}
+--- Run healthcheck(s) for one or more plugins. E.g. to run only
+--- the standard Nvim healthcheck: >vim
+--- :checkhealth vim.health
+--- <
+--- To run the healthchecks for the "foo" and "bar" plugins
+--- (assuming they are on 'runtimepath' and they have implemented
+--- the Lua `require("foo.health").check()` interface): >vim
+--- :checkhealth foo bar
+--- <
+--- To run healthchecks for Lua submodules, use dot notation or
+--- "*" to refer to all submodules. For example Nvim provides
+--- `vim.lsp` and `vim.treesitter`: >vim
+--- :checkhealth vim.lsp vim.treesitter
+--- :checkhealth vim*
+--- <
+---
+--- Create a healthcheck *health-dev* *vim.health*
+---
+--- Healthchecks are functions that check the user environment, configuration, or
+--- any other prerequisites that a plugin cares about. Nvim ships with
+--- healthchecks in:
+--- - $VIMRUNTIME/autoload/health/
+--- - $VIMRUNTIME/lua/vim/lsp/health.lua
+--- - $VIMRUNTIME/lua/vim/treesitter/health.lua
+--- - and more...
+---
+--- To add a new healthcheck for your own plugin, simply create a "health.lua"
+--- module on 'runtimepath' that returns a table with a "check()" function. Then
+--- |:checkhealth| will automatically find and invoke the function.
+---
+--- For example if your plugin is named "foo", define your healthcheck module at
+--- one of these locations (on 'runtimepath'):
+--- - lua/foo/health/init.lua
+--- - lua/foo/health.lua
+---
+--- If your plugin also provides a submodule named "bar" for which you want
+--- a separate healthcheck, define the healthcheck at one of these locations:
+--- - lua/foo/bar/health/init.lua
+--- - lua/foo/bar/health.lua
+---
+--- All such health modules must return a Lua table containing a `check()`
+--- function.
+---
+--- Copy this sample code into `lua/foo/health.lua`, replacing "foo" in the path
+--- with your plugin name: >lua
+---
+--- local M = {}
+---
+--- M.check = function()
+--- vim.health.start("foo report")
+--- -- make sure setup function parameters are ok
+--- if check_setup() then
+--- vim.health.ok("Setup is correct")
+--- else
+--- vim.health.error("Setup is incorrect")
+--- end
+--- -- do some more checking
+--- -- ...
+--- end
+---
+--- return M
+---</pre>
+
local M = {}
local s_output = {} ---@type string[]
---- Returns the fold text of the current healthcheck section
-function M.foldtext()
- local foldtext = vim.fn.foldtext()
-
- if vim.bo.filetype ~= 'checkhealth' then
- return foldtext
- end
-
- if vim.b.failedchecks == nil then
- vim.b.failedchecks = vim.empty_dict()
- end
-
- if vim.b.failedchecks[foldtext] == nil then
- local warning = '- WARNING '
- local warninglen = string.len(warning)
- local err = '- ERROR '
- local errlen = string.len(err)
- local failedchecks = vim.b.failedchecks
- failedchecks[foldtext] = false
-
- local foldcontent = vim.api.nvim_buf_get_lines(0, vim.v.foldstart - 1, vim.v.foldend, false)
- for _, line in ipairs(foldcontent) do
- if string.sub(line, 1, warninglen) == warning or string.sub(line, 1, errlen) == err then
- failedchecks[foldtext] = true
- break
- end
- end
-
- vim.b.failedchecks = failedchecks
- end
-
- return vim.b.failedchecks[foldtext] and '+WE' .. foldtext:sub(4) or foldtext
-end
-
---- @param path string path to search for the healthcheck
---- @return string[] { name, func, type } representing a healthcheck
+-- From a path return a list [{name}, {func}, {type}] representing a healthcheck
local function filepath_to_healthcheck(path)
path = vim.fs.normalize(path)
local name --- @type string
@@ -178,7 +231,9 @@ local function collect_output(output)
vim.list_extend(s_output, vim.split(output, '\n'))
end
---- Starts a new report.
+--- Starts a new report. Most plugins should call this only once, but if
+--- you want different sections to appear in your report, call this once
+--- per section.
---
--- @param name string
function M.start(name)
@@ -186,7 +241,7 @@ function M.start(name)
collect_output(input)
end
---- Reports a message in the current section.
+--- Reports an informational message.
---
--- @param msg string
function M.info(msg)
@@ -194,7 +249,7 @@ function M.info(msg)
collect_output(input)
end
---- Reports a successful healthcheck.
+--- Reports a "success" message.
---
--- @param msg string
function M.ok(msg)
@@ -202,7 +257,7 @@ function M.ok(msg)
collect_output(input)
end
---- Reports a health warning.
+--- Reports a warning.
---
--- @param msg string
--- @param ... string|string[] Optional advice
@@ -211,7 +266,7 @@ function M.warn(msg, ...)
collect_output(input)
end
---- Reports a failed healthcheck.
+--- Reports an error.
---
--- @param msg string
--- @param ... string|string[] Optional advice
@@ -220,54 +275,7 @@ function M.error(msg, ...)
collect_output(input)
end
---- @param type string
-local function deprecate(type)
- local before = string.format('vim.health.report_%s()', type)
- local after = string.format('vim.health.%s()', type)
- local message = vim.deprecate(before, after, '0.11')
- if message then
- M.warn(message)
- end
- vim.cmd.redraw()
- vim.print('Running healthchecks...')
-end
-
---- @deprecated
---- @param name string
-function M.report_start(name)
- deprecate('start')
- M.start(name)
-end
-
---- @deprecated
---- @param msg string
-function M.report_info(msg)
- deprecate('info')
- M.info(msg)
-end
-
---- @deprecated
---- @param msg string
-function M.report_ok(msg)
- deprecate('ok')
- M.ok(msg)
-end
-
---- @deprecated
---- @param msg string
-function M.report_warn(msg, ...)
- deprecate('warn')
- M.warn(msg, ...)
-end
-
---- @deprecated
---- @param msg string
-function M.report_error(msg, ...)
- deprecate('error')
- M.error(msg, ...)
-end
-
-function M.provider_disabled(provider)
+function M._provider_disabled(provider)
local loaded_var = 'loaded_' .. provider .. '_provider'
local v = vim.g[loaded_var]
if v == 0 then
@@ -307,7 +315,7 @@ local function shellify(cmd)
return table.concat(escaped, ' ')
end
-function M.cmd_ok(cmd)
+function M._cmd_ok(cmd)
local out = vim.fn.system(cmd)
return vim.v.shell_error == 0, out
end
@@ -320,7 +328,7 @@ end
--- - stderr (boolean): Append stderr to stdout
--- - ignore_error (boolean): If true, ignore error output
--- - timeout (number): Number of seconds to wait before timing out (default 30)
-function M.system(cmd, args)
+function M._system(cmd, args)
args = args or {}
local stdin = args.stdin or ''
local stderr = vim.F.if_nil(args.stderr, false)
@@ -341,7 +349,7 @@ function M.system(cmd, args)
if jobid < 1 then
local message =
- string.format('Command error (job=%d): %s (in %s)', jobid, shellify(cmd), vim.loop.cwd())
+ string.format('Command error (job=%d): %s (in %s)', jobid, shellify(cmd), vim.uv.cwd())
error(message)
return opts.output, 1
end
@@ -360,7 +368,7 @@ function M.system(cmd, args)
jobid,
shell_error_code,
shellify(cmd),
- vim.loop.cwd()
+ vim.uv.cwd()
)
if opts.output:find('%S') then
emsg = string.format('%s\noutput: %s', emsg, opts.output)
@@ -386,7 +394,7 @@ local path2name = function(path)
path = path:gsub('^.*/lua/', '')
-- Remove the filename (health.lua)
- path = vim.fn.fnamemodify(path, ':h')
+ path = vim.fs.dirname(path)
-- Change slashes to dots
path = path:gsub('/', '.')
@@ -401,17 +409,20 @@ end
local PATTERNS = { '/autoload/health/*.vim', '/lua/**/**/health.lua', '/lua/**/**/health/init.lua' }
--- :checkhealth completion function used by cmdexpand.c get_healthcheck_names()
M._complete = function()
- local names = vim.tbl_flatten(vim.tbl_map(function(pattern)
- return vim.tbl_map(path2name, vim.api.nvim_get_runtime_file(pattern, true))
- end, PATTERNS))
- -- Remove duplicates
- local unique = {}
- vim.tbl_map(function(f)
- unique[f] = true
- end, names)
+ local unique = vim
+ .iter(vim.tbl_map(function(pattern)
+ return vim.tbl_map(path2name, vim.api.nvim_get_runtime_file(pattern, true))
+ end, PATTERNS))
+ :flatten()
+ :fold({}, function(t, name)
+ t[name] = true -- Remove duplicates
+ return t
+ end)
-- vim.health is this file, which is not a healthcheck
unique['vim'] = nil
- return vim.tbl_keys(unique)
+ local rv = vim.tbl_keys(unique)
+ table.sort(rv)
+ return rv
end
--- Runs the specified healthchecks.
@@ -497,11 +508,4 @@ function M._check(mods, plugin_names)
vim.print('')
end
-local fn_bool = function(key)
- return function(...)
- return vim.fn[key](...) == 1
- end
-end
-M.executable = fn_bool('executable')
-
return M
diff --git a/runtime/lua/vim/health/health.lua b/runtime/lua/vim/health/health.lua
new file mode 100644
index 0000000000..5bc03199ee
--- /dev/null
+++ b/runtime/lua/vim/health/health.lua
@@ -0,0 +1,409 @@
+local M = {}
+local health = require('vim.health')
+
+local shell_error = function()
+ return vim.v.shell_error ~= 0
+end
+
+local suggest_faq = 'https://github.com/neovim/neovim/blob/master/BUILD.md#building'
+
+local function check_runtime()
+ health.start('Runtime')
+ -- Files from an old installation.
+ local bad_files = {
+ ['plugin/health.vim'] = false,
+ ['autoload/health/nvim.vim'] = false,
+ ['autoload/health/provider.vim'] = false,
+ ['autoload/man.vim'] = false,
+ ['plugin/man.vim'] = false,
+ ['queries/help/highlights.scm'] = false,
+ ['queries/help/injections.scm'] = false,
+ ['scripts.vim'] = false,
+ ['syntax/syncolor.vim'] = false,
+ }
+ local bad_files_msg = ''
+ for k, _ in pairs(bad_files) do
+ local path = ('%s/%s'):format(vim.env.VIMRUNTIME, k)
+ if vim.uv.fs_stat(path) then
+ bad_files[k] = true
+ bad_files_msg = ('%s%s\n'):format(bad_files_msg, path)
+ end
+ end
+
+ local ok = (bad_files_msg == '')
+ local info = ok and health.ok or health.info
+ info(string.format('$VIMRUNTIME: %s', vim.env.VIMRUNTIME))
+ if not ok then
+ health.error(
+ string.format(
+ 'Found old files in $VIMRUNTIME (this can cause weird behavior):\n%s',
+ bad_files_msg
+ ),
+ { 'Delete the $VIMRUNTIME directory (or uninstall Nvim), then reinstall Nvim.' }
+ )
+ end
+end
+
+local function check_config()
+ health.start('Configuration')
+ local ok = true
+
+ local init_lua = vim.fn.stdpath('config') .. '/init.lua'
+ local init_vim = vim.fn.stdpath('config') .. '/init.vim'
+ local vimrc = vim.env.MYVIMRC and vim.fn.expand(vim.env.MYVIMRC) or init_lua
+
+ if vim.fn.filereadable(vimrc) == 0 and vim.fn.filereadable(init_vim) == 0 then
+ ok = false
+ local has_vim = vim.fn.filereadable(vim.fn.expand('~/.vimrc')) == 1
+ health.warn(
+ ('%s user config file: %s'):format(
+ -1 == vim.fn.getfsize(vimrc) and 'Missing' or 'Unreadable',
+ vimrc
+ ),
+ { has_vim and ':help nvim-from-vim' or ':help config' }
+ )
+ end
+
+ -- If $VIM is empty we don't care. Else make sure it is valid.
+ if vim.env.VIM and vim.fn.filereadable(vim.env.VIM .. '/runtime/doc/nvim.txt') == 0 then
+ ok = false
+ health.error('$VIM is invalid: ' .. vim.env.VIM)
+ end
+
+ if vim.env.NVIM_TUI_ENABLE_CURSOR_SHAPE then
+ ok = false
+ health.warn('$NVIM_TUI_ENABLE_CURSOR_SHAPE is ignored in Nvim 0.2+', {
+ "Use the 'guicursor' option to configure cursor shape. :help 'guicursor'",
+ 'https://github.com/neovim/neovim/wiki/Following-HEAD#20170402',
+ })
+ end
+
+ if vim.v.ctype == 'C' then
+ ok = false
+ health.error(
+ 'Locale does not support UTF-8. Unicode characters may not display correctly.'
+ .. ('\n$LANG=%s $LC_ALL=%s $LC_CTYPE=%s'):format(
+ vim.env.LANG,
+ vim.env.LC_ALL,
+ vim.env.LC_CTYPE
+ ),
+ {
+ 'If using tmux, try the -u option.',
+ 'Ensure that your terminal/shell/tmux/etc inherits the environment, or set $LANG explicitly.',
+ 'Configure your system locale.',
+ }
+ )
+ end
+
+ if vim.o.paste == 1 then
+ ok = false
+ health.error(
+ "'paste' is enabled. This option is only for pasting text.\nIt should not be set in your config.",
+ {
+ 'Remove `set paste` from your init.vim, if applicable.',
+ 'Check `:verbose set paste?` to see if a plugin or script set the option.',
+ }
+ )
+ end
+
+ local writeable = true
+ local shadaopt = vim.fn.split(vim.o.shada, ',')
+ local shadafile = (
+ vim.o.shada == '' and vim.o.shada
+ or vim.fn.substitute(vim.fn.matchstr(shadaopt[#shadaopt], '^n.\\+'), '^n', '', '')
+ )
+ shadafile = (
+ vim.o.shadafile == ''
+ and (shadafile == '' and vim.fn.stdpath('state') .. '/shada/main.shada' or vim.fn.expand(
+ shadafile
+ ))
+ or (vim.o.shadafile == 'NONE' and '' or vim.o.shadafile)
+ )
+ if shadafile ~= '' and vim.fn.glob(shadafile) == '' then
+ -- Since this may be the first time Nvim has been run, try to create a shada file.
+ if not pcall(vim.cmd.wshada) then
+ writeable = false
+ end
+ end
+ if
+ not writeable
+ or (
+ shadafile ~= ''
+ and (vim.fn.filereadable(shadafile) == 0 or vim.fn.filewritable(shadafile) ~= 1)
+ )
+ then
+ ok = false
+ health.error(
+ 'shada file is not '
+ .. ((not writeable or vim.fn.filereadable(shadafile) == 1) and 'writeable' or 'readable')
+ .. ':\n'
+ .. shadafile
+ )
+ end
+
+ if ok then
+ health.ok('no issues found')
+ end
+end
+
+local function check_performance()
+ health.start('Performance')
+
+ -- Check buildtype
+ local buildtype = vim.fn.matchstr(vim.fn.execute('version'), [[\v\cbuild type:?\s*[^\n\r\t ]+]])
+ if buildtype == '' then
+ health.error('failed to get build type from :version')
+ elseif vim.regex([[\v(MinSizeRel|Release|RelWithDebInfo)]]):match_str(buildtype) then
+ health.ok(buildtype)
+ else
+ health.info(buildtype)
+ health.warn('Non-optimized debug build. Nvim will be slower.', {
+ 'Install a different Nvim package, or rebuild with `CMAKE_BUILD_TYPE=RelWithDebInfo`.',
+ suggest_faq,
+ })
+ end
+
+ -- check for slow shell invocation
+ local slow_cmd_time = 1.5
+ local start_time = vim.fn.reltime()
+ vim.fn.system('echo')
+ local elapsed_time = vim.fn.reltimefloat(vim.fn.reltime(start_time))
+ if elapsed_time > slow_cmd_time then
+ health.warn(
+ 'Slow shell invocation (took ' .. vim.fn.printf('%.2f', elapsed_time) .. ' seconds).'
+ )
+ end
+end
+
+-- Load the remote plugin manifest file and check for unregistered plugins
+local function check_rplugin_manifest()
+ health.start('Remote Plugins')
+
+ local existing_rplugins = {}
+ for _, item in ipairs(vim.fn['remote#host#PluginsForHost']('python3')) do
+ existing_rplugins[item.path] = 'python3'
+ end
+
+ local require_update = false
+ local handle_path = function(path)
+ local python_glob = vim.fn.glob(path .. '/rplugin/python*', true, true)
+ if vim.tbl_isempty(python_glob) then
+ return
+ end
+
+ local python_dir = python_glob[1]
+ local python_version = vim.fs.basename(python_dir)
+
+ local scripts = vim.fn.glob(python_dir .. '/*.py', true, true)
+ vim.list_extend(scripts, vim.fn.glob(python_dir .. '/*/__init__.py', true, true))
+
+ for _, script in ipairs(scripts) do
+ local contents = vim.fn.join(vim.fn.readfile(script))
+ if vim.regex([[\<\%(from\|import\)\s\+neovim\>]]):match_str(contents) then
+ if vim.regex([[[\/]__init__\.py$]]):match_str(script) then
+ script = vim.fn.tr(vim.fn.fnamemodify(script, ':h'), '\\', '/')
+ end
+ if not existing_rplugins[script] then
+ local msg = vim.fn.printf('"%s" is not registered.', vim.fs.basename(path))
+ if python_version == 'pythonx' then
+ if vim.fn.has('python3') == 0 then
+ msg = msg .. ' (python3 not available)'
+ end
+ elseif vim.fn.has(python_version) == 0 then
+ msg = msg .. vim.fn.printf(' (%s not available)', python_version)
+ else
+ require_update = true
+ end
+
+ health.warn(msg)
+ end
+
+ break
+ end
+ end
+ end
+
+ for _, path in ipairs(vim.fn.map(vim.split(vim.o.runtimepath, ','), 'resolve(v:val)')) do
+ handle_path(path)
+ end
+
+ if require_update then
+ health.warn('Out of date', { 'Run `:UpdateRemotePlugins`' })
+ else
+ health.ok('Up to date')
+ end
+end
+
+local function check_tmux()
+ if not vim.env.TMUX or vim.fn.executable('tmux') == 0 then
+ return
+ end
+
+ local get_tmux_option = function(option)
+ local cmd = 'tmux show-option -qvg ' .. option -- try global scope
+ local out = vim.fn.system(vim.fn.split(cmd))
+ local val = vim.fn.substitute(out, [[\v(\s|\r|\n)]], '', 'g')
+ if shell_error() then
+ health.error('command failed: ' .. cmd .. '\n' .. out)
+ return 'error'
+ elseif val == '' then
+ cmd = 'tmux show-option -qvgs ' .. option -- try session scope
+ out = vim.fn.system(vim.fn.split(cmd))
+ val = vim.fn.substitute(out, [[\v(\s|\r|\n)]], '', 'g')
+ if shell_error() then
+ health.error('command failed: ' .. cmd .. '\n' .. out)
+ return 'error'
+ end
+ end
+ return val
+ end
+
+ health.start('tmux')
+
+ -- check escape-time
+ local suggestions =
+ { 'set escape-time in ~/.tmux.conf:\nset-option -sg escape-time 10', suggest_faq }
+ local tmux_esc_time = get_tmux_option('escape-time')
+ if tmux_esc_time ~= 'error' then
+ if tmux_esc_time == '' then
+ health.error('`escape-time` is not set', suggestions)
+ elseif tonumber(tmux_esc_time) > 300 then
+ health.error('`escape-time` (' .. tmux_esc_time .. ') is higher than 300ms', suggestions)
+ else
+ health.ok('escape-time: ' .. tmux_esc_time)
+ end
+ end
+
+ -- check focus-events
+ local tmux_focus_events = get_tmux_option('focus-events')
+ if tmux_focus_events ~= 'error' then
+ if tmux_focus_events == '' or tmux_focus_events ~= 'on' then
+ health.warn(
+ "`focus-events` is not enabled. |'autoread'| may not work.",
+ { '(tmux 1.9+ only) Set `focus-events` in ~/.tmux.conf:\nset-option -g focus-events on' }
+ )
+ else
+ health.ok('focus-events: ' .. tmux_focus_events)
+ end
+ end
+
+ -- check default-terminal and $TERM
+ health.info('$TERM: ' .. vim.env.TERM)
+ local cmd = 'tmux show-option -qvg default-terminal'
+ local out = vim.fn.system(vim.fn.split(cmd))
+ local tmux_default_term = vim.fn.substitute(out, [[\v(\s|\r|\n)]], '', 'g')
+ if tmux_default_term == '' then
+ cmd = 'tmux show-option -qvgs default-terminal'
+ out = vim.fn.system(vim.fn.split(cmd))
+ tmux_default_term = vim.fn.substitute(out, [[\v(\s|\r|\n)]], '', 'g')
+ end
+
+ if shell_error() then
+ health.error('command failed: ' .. cmd .. '\n' .. out)
+ elseif tmux_default_term ~= vim.env.TERM then
+ health.info('default-terminal: ' .. tmux_default_term)
+ health.error(
+ '$TERM differs from the tmux `default-terminal` setting. Colors might look wrong.',
+ { '$TERM may have been set by some rc (.bashrc, .zshrc, ...).' }
+ )
+ elseif not vim.regex([[\v(tmux-256color|screen-256color)]]):match_str(vim.env.TERM) then
+ health.error(
+ '$TERM should be "screen-256color" or "tmux-256color" in tmux. Colors might look wrong.',
+ {
+ 'Set default-terminal in ~/.tmux.conf:\nset-option -g default-terminal "screen-256color"',
+ suggest_faq,
+ }
+ )
+ end
+
+ -- check for RGB capabilities
+ local info = vim.fn.system({ 'tmux', 'show-messages', '-T' })
+ local has_setrgbb = vim.fn.stridx(info, ' setrgbb: (string)') ~= -1
+ local has_setrgbf = vim.fn.stridx(info, ' setrgbf: (string)') ~= -1
+ if not has_setrgbb or not has_setrgbf then
+ health.warn(
+ "True color support could not be detected. |'termguicolors'| won't work properly.",
+ {
+ "Add the following to your tmux configuration file, replacing XXX by the value of $TERM outside of tmux:\nset-option -a terminal-features 'XXX:RGB'",
+ "For older tmux versions use this instead:\nset-option -a terminal-overrides 'XXX:Tc'",
+ }
+ )
+ end
+end
+
+local function check_terminal()
+ if vim.fn.executable('infocmp') == 0 then
+ return
+ end
+
+ health.start('terminal')
+ local cmd = 'infocmp -L'
+ local out = vim.fn.system(vim.fn.split(cmd))
+ local kbs_entry = vim.fn.matchstr(out, 'key_backspace=[^,[:space:]]*')
+ local kdch1_entry = vim.fn.matchstr(out, 'key_dc=[^,[:space:]]*')
+
+ if
+ shell_error()
+ and (
+ vim.fn.has('win32') == 0
+ or vim.fn.matchstr(
+ out,
+ [[infocmp: couldn't open terminfo file .\+\%(conemu\|vtpcon\|win32con\)]]
+ )
+ == ''
+ )
+ then
+ health.error('command failed: ' .. cmd .. '\n' .. out)
+ else
+ health.info(
+ vim.fn.printf(
+ 'key_backspace (kbs) terminfo entry: `%s`',
+ (kbs_entry == '' and '? (not found)' or kbs_entry)
+ )
+ )
+
+ health.info(
+ vim.fn.printf(
+ 'key_dc (kdch1) terminfo entry: `%s`',
+ (kbs_entry == '' and '? (not found)' or kdch1_entry)
+ )
+ )
+ end
+
+ for _, env_var in ipairs({
+ 'XTERM_VERSION',
+ 'VTE_VERSION',
+ 'TERM_PROGRAM',
+ 'COLORTERM',
+ 'SSH_TTY',
+ }) do
+ if vim.env[env_var] then
+ health.info(vim.fn.printf('$%s="%s"', env_var, vim.env[env_var]))
+ end
+ end
+end
+
+local function check_external_tools()
+ health.start('External Tools')
+
+ if vim.fn.executable('rg') == 1 then
+ local rg = vim.fn.exepath('rg')
+ local cmd = 'rg -V'
+ local out = vim.fn.system(vim.fn.split(cmd))
+ health.ok(('%s (%s)'):format(vim.trim(out), rg))
+ else
+ health.warn('ripgrep not available')
+ end
+end
+
+function M.check()
+ check_config()
+ check_runtime()
+ check_performance()
+ check_rplugin_manifest()
+ check_terminal()
+ check_tmux()
+ check_external_tools()
+end
+
+return M
diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua
index effe280dee..f278bd357f 100644
--- a/runtime/lua/vim/highlight.lua
+++ b/runtime/lua/vim/highlight.lua
@@ -1,26 +1,3 @@
----@brief
----
---- Nvim includes a function for highlighting a selection on yank.
----
---- To enable it, add the following to your `init.vim`:
----
---- ```vim
---- au TextYankPost * silent! lua vim.highlight.on_yank()
---- ```
----
---- You can customize the highlight group and the duration of the highlight via:
----
---- ```vim
---- au TextYankPost * silent! lua vim.highlight.on_yank {higroup="IncSearch", timeout=150}
---- ```
----
---- If you want to exclude visual selections from highlighting on yank, use:
----
---- ```vim
---- au TextYankPost * silent! lua vim.highlight.on_yank {on_visual=false}
---- ```
----
-
local api = vim.api
local M = {}
@@ -40,6 +17,23 @@ M.priorities = {
user = 200,
}
+--- @class vim.highlight.range.Opts
+--- @inlinedoc
+---
+--- Type of range. See [setreg()]
+--- (default: `'charwise'`)
+--- @field regtype? string
+---
+--- Indicates whether the range is end-inclusive
+--- (default: `false`)
+--- @field inclusive? boolean
+---
+--- Indicates priority of highlight
+--- (default: `vim.highlight.priorities.user`)
+--- @field priority? integer
+---
+--- @field package _scoped? boolean
+
--- Apply highlight group to range of text.
---
---@param bufnr integer Buffer number to apply highlighting to
@@ -47,10 +41,7 @@ M.priorities = {
---@param higroup string Highlight group to use for highlighting
---@param start integer[]|string Start of region as a (line, column) tuple or string accepted by |getpos()|
---@param finish integer[]|string End of region as a (line, column) tuple or string accepted by |getpos()|
----@param opts table|nil Optional parameters
---- - regtype type of range (see |setreg()|, default charwise)
---- - inclusive boolean indicating whether the range is end-inclusive (default false)
---- - priority number indicating priority of highlight (default priorities.user)
+---@param opts? vim.highlight.range.Opts
function M.range(bufnr, ns, higroup, start, finish, opts)
opts = opts or {}
local regtype = opts.regtype or 'v'
@@ -80,10 +71,16 @@ function M.range(bufnr, ns, higroup, start, finish, opts)
end
local yank_ns = api.nvim_create_namespace('hlyank')
-local yank_timer
-local yank_cancel
+local yank_timer --- @type uv.uv_timer_t?
+local yank_cancel --- @type fun()?
---- Highlight the yanked text
+--- Highlight the yanked text during a |TextYankPost| event.
+---
+--- Add the following to your `init.vim`:
+---
+--- ```vim
+--- autocmd TextYankPost * silent! lua vim.highlight.on_yank {higroup='Visual', timeout=300}
+--- ```
---
--- @param opts table|nil Optional parameters
--- - higroup highlight group for yanked region (default "IncSearch")
@@ -128,22 +125,23 @@ function M.on_yank(opts)
local winid = vim.api.nvim_get_current_win()
if yank_timer then
yank_timer:close()
+ assert(yank_cancel)
yank_cancel()
end
+ vim.api.nvim__win_add_ns(winid, yank_ns)
M.range(bufnr, yank_ns, higroup, "'[", "']", {
regtype = event.regtype,
inclusive = event.inclusive,
priority = opts.priority or M.priorities.user,
_scoped = true,
})
- vim.api.nvim_win_add_ns(winid, yank_ns)
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_win_remove_ns, winid, yank_ns)
+ pcall(vim.api.nvim__win_del_ns, winid, yank_ns)
end
yank_timer = vim.defer_fn(yank_cancel, timeout)
diff --git a/runtime/lua/vim/iter.lua b/runtime/lua/vim/iter.lua
index a37b7f7858..1093759efe 100644
--- a/runtime/lua/vim/iter.lua
+++ b/runtime/lua/vim/iter.lua
@@ -7,6 +7,7 @@
--- `vim.iter()`:
---
--- - List tables (arrays, |lua-list|) yield only the value of each element.
+--- - Holes (nil values) are allowed.
--- - Use |Iter:enumerate()| to also pass the index to the next stage.
--- - Or initialize with ipairs(): `vim.iter(ipairs(…))`.
--- - Non-list tables (|lua-dict|) yield both the key and value of each element.
@@ -60,9 +61,6 @@
--- vim.iter(rb):totable()
--- -- { "a", "b" }
--- ```
----
---- In addition to the |vim.iter()| function, the |vim.iter| module provides
---- convenience functions like |vim.iter.filter()| and |vim.iter.totable()|.
--- LuaLS is bad at generics which this module mostly deals with
--- @diagnostic disable:no-unknown
@@ -83,13 +81,13 @@ end
--- Special case implementations for iterators on list tables.
---@nodoc
----@class ListIter : Iter
+---@class ArrayIter : Iter
---@field _table table Underlying table data
---@field _head number Index to the front of a table iterator
---@field _tail number Index to the end of a table iterator (exclusive)
-local ListIter = {}
-ListIter.__index = setmetatable(ListIter, Iter)
-ListIter.__call = function(self)
+local ArrayIter = {}
+ArrayIter.__index = setmetatable(ArrayIter, Iter)
+ArrayIter.__call = function(self)
return self:next()
end
@@ -113,36 +111,34 @@ end
local function sanitize(t)
if type(t) == 'table' and getmetatable(t) == packedmt then
- -- Remove length tag
+ -- Remove length tag and metatable
t.n = nil
+ setmetatable(t, nil)
end
return t
end
---- Flattens a single list-like table. Errors if it attempts to flatten a
+--- Flattens a single array-like table. Errors if it attempts to flatten a
--- dict-like table
----@param v table table which should be flattened
+---@param t table table which should be flattened
---@param max_depth number depth to which the table should be flattened
---@param depth number current iteration depth
---@param result table output table that contains flattened result
---@return table|nil flattened table if it can be flattened, otherwise nil
-local function flatten(v, max_depth, depth, result)
- if depth < max_depth and type(v) == 'table' then
- local i = 0
- for _ in pairs(v) do
- i = i + 1
-
- if v[i] == nil then
+local function flatten(t, max_depth, depth, result)
+ if depth < max_depth and type(t) == 'table' then
+ for k, v in pairs(t) do
+ if type(k) ~= 'number' or k <= 0 or math.floor(k) ~= k then
-- short-circuit: this is not a list like table
return nil
end
- if flatten(v[i], max_depth, depth + 1, result) == nil then
+ if flatten(v, max_depth, depth + 1, result) == nil then
return nil
end
end
- else
- result[#result + 1] = v
+ elseif t ~= nil then
+ result[#result + 1] = t
end
return result
@@ -201,7 +197,7 @@ function Iter:filter(f)
end
---@private
-function ListIter:filter(f)
+function ArrayIter:filter(f)
local inc = self._head < self._tail and 1 or -1
local n = self._head
for i = self._head, self._tail - inc, inc do
@@ -236,11 +232,11 @@ end
---@return Iter
---@diagnostic disable-next-line:unused-local
function Iter:flatten(depth) -- luacheck: no unused args
- error('flatten() requires a list-like table')
+ error('flatten() requires an array-like table')
end
---@private
-function ListIter:flatten(depth)
+function ArrayIter:flatten(depth)
depth = depth or 1
local inc = self._head < self._tail and 1 or -1
local target = {}
@@ -250,7 +246,7 @@ function ListIter:flatten(depth)
-- exit early if we try to flatten a dict-like table
if flattened == nil then
- error('flatten() requires a list-like table')
+ error('flatten() requires an array-like table')
end
for _, v in pairs(flattened) do
@@ -330,7 +326,7 @@ function Iter:map(f)
end
---@private
-function ListIter:map(f)
+function ArrayIter:map(f)
local inc = self._head < self._tail and 1 or -1
local n = self._head
for i = self._head, self._tail - inc, inc do
@@ -363,7 +359,7 @@ function Iter:each(f)
end
---@private
-function ListIter:each(f)
+function ArrayIter:each(f)
local inc = self._head < self._tail and 1 or -1
for i = self._head, self._tail - inc, inc do
f(unpack(self._table[i]))
@@ -374,7 +370,7 @@ end
--- Collect the iterator into a table.
---
--- The resulting table depends on the initial source in the iterator pipeline.
---- List-like tables and function iterators will be collected into a list-like
+--- Array-like tables and function iterators will be collected into an array-like
--- table. If multiple values are returned from the final stage in the iterator
--- pipeline, each value will be included in a table.
---
@@ -391,7 +387,7 @@ end
--- -- { { 'a', 1 }, { 'c', 3 } }
--- ```
---
---- The generated table is a list-like table with consecutive, numeric indices.
+--- The generated table is an array-like table with consecutive, numeric indices.
--- To create a map-like table with arbitrary keys, use |Iter:fold()|.
---
---
@@ -411,12 +407,12 @@ function Iter:totable()
end
---@private
-function ListIter:totable()
- if self.next ~= ListIter.next or self._head >= self._tail then
+function ArrayIter:totable()
+ if self.next ~= ArrayIter.next or self._head >= self._tail then
return Iter.totable(self)
end
- local needs_sanitize = getmetatable(self._table[1]) == packedmt
+ local needs_sanitize = getmetatable(self._table[self._head]) == packedmt
-- Reindex and sanitize.
local len = self._tail - self._head
@@ -453,20 +449,25 @@ function Iter:join(delim)
return table.concat(self:totable(), delim)
end
---- Folds ("reduces") an iterator into a single value.
+--- Folds ("reduces") an iterator into a single value. [Iter:reduce()]()
---
--- Examples:
---
--- ```lua
--- -- Create a new table with only even values
---- local t = { a = 1, b = 2, c = 3, d = 4 }
---- local it = vim.iter(t)
---- it:filter(function(k, v) return v % 2 == 0 end)
---- it:fold({}, function(t, k, v)
---- t[k] = v
---- return t
---- end)
---- -- { b = 2, d = 4 }
+--- vim.iter({ a = 1, b = 2, c = 3, d = 4 })
+--- :filter(function(k, v) return v % 2 == 0 end)
+--- :fold({}, function(acc, k, v)
+--- acc[k] = v
+--- return acc
+--- end) --> { b = 2, d = 4 }
+---
+--- -- Get the "maximum" item of an iterable.
+--- vim.iter({ -99, -4, 3, 42, 0, 0, 7 })
+--- :fold({}, function(acc, v)
+--- acc.max = math.max(v, acc.max or v)
+--- return acc
+--- end) --> { max = 42 }
--- ```
---
---@generic A
@@ -491,7 +492,7 @@ function Iter:fold(init, f)
end
---@private
-function ListIter:fold(init, f)
+function ArrayIter:fold(init, f)
local acc = init
local inc = self._head < self._tail and 1 or -1
for i = self._head, self._tail - inc, inc do
@@ -523,7 +524,7 @@ function Iter:next()
end
---@private
-function ListIter:next()
+function ArrayIter:next()
if self._head ~= self._tail then
local v = self._table[self._head]
local inc = self._head < self._tail and 1 or -1
@@ -546,11 +547,11 @@ end
---
---@return Iter
function Iter:rev()
- error('rev() requires a list-like table')
+ error('rev() requires an array-like table')
end
---@private
-function ListIter:rev()
+function ArrayIter:rev()
local inc = self._head < self._tail and 1 or -1
self._head, self._tail = self._tail - inc, self._head - inc
return self
@@ -574,11 +575,11 @@ end
---
---@return any
function Iter:peek()
- error('peek() requires a list-like table')
+ error('peek() requires an array-like table')
end
---@private
-function ListIter:peek()
+function ArrayIter:peek()
if self._head ~= self._tail then
return self._table[self._head]
end
@@ -633,7 +634,7 @@ function Iter:find(f)
return unpack(result)
end
---- Gets the first value in a |list-iterator| that satisfies a predicate, starting from the end.
+--- Gets the first value satisfying a predicate, from the end of a |list-iterator|.
---
--- Advances the iterator. Returns nil and drains the iterator if no value is found.
---
@@ -655,11 +656,11 @@ end
---@return any
---@diagnostic disable-next-line: unused-local
function Iter:rfind(f) -- luacheck: no unused args
- error('rfind() requires a list-like table')
+ error('rfind() requires an array-like table')
end
---@private
-function ListIter:rfind(f)
+function ArrayIter:rfind(f)
if type(f) ~= 'function' then
local val = f
f = function(v)
@@ -707,9 +708,10 @@ function Iter:take(n)
end
---@private
-function ListIter:take(n)
- local inc = self._head < self._tail and 1 or -1
- self._tail = math.min(self._tail, self._head + n * inc)
+function ArrayIter:take(n)
+ local inc = self._head < self._tail and n or -n
+ local cmp = self._head < self._tail and math.min or math.max
+ self._tail = cmp(self._tail, self._head + inc)
return self
end
@@ -719,19 +721,19 @@ end
---
--- ```lua
--- local it = vim.iter({1, 2, 3, 4})
---- it:nextback()
+--- it:pop()
--- -- 4
---- it:nextback()
+--- it:pop()
--- -- 3
--- ```
---
---@return any
-function Iter:nextback()
- error('nextback() requires a list-like table')
+function Iter:pop()
+ error('pop() requires an array-like table')
end
--- @nodoc
-function ListIter:nextback()
+function ArrayIter:pop()
if self._head ~= self._tail then
local inc = self._head < self._tail and 1 or -1
self._tail = self._tail - inc
@@ -741,27 +743,27 @@ end
--- Gets the last value of a |list-iterator| without consuming it.
---
---- See also |Iter:last()|.
----
--- Example:
---
--- ```lua
--- local it = vim.iter({1, 2, 3, 4})
---- it:peekback()
+--- it:rpeek()
--- -- 4
---- it:peekback()
+--- it:rpeek()
--- -- 4
---- it:nextback()
+--- it:pop()
--- -- 4
--- ```
---
+---@see Iter.last
+---
---@return any
-function Iter:peekback()
- error('peekback() requires a list-like table')
+function Iter:rpeek()
+ error('rpeek() requires an array-like table')
end
---@nodoc
-function ListIter:peekback()
+function ArrayIter:rpeek()
if self._head ~= self._tail then
local inc = self._head < self._tail and 1 or -1
return self._table[self._tail - inc]
@@ -790,7 +792,7 @@ function Iter:skip(n)
end
---@private
-function ListIter:skip(n)
+function ArrayIter:skip(n)
local inc = self._head < self._tail and n or -n
self._head = self._head + inc
if (inc > 0 and self._head > self._tail) or (inc < 0 and self._head < self._tail) then
@@ -799,27 +801,27 @@ function ListIter:skip(n)
return self
end
---- Skips `n` values backwards from the end of a |list-iterator| pipeline.
+--- Discards `n` values from the end of a |list-iterator| pipeline.
---
--- Example:
---
--- ```lua
---- local it = vim.iter({ 1, 2, 3, 4, 5 }):skipback(2)
+--- local it = vim.iter({ 1, 2, 3, 4, 5 }):rskip(2)
--- it:next()
--- -- 1
---- it:nextback()
+--- it:pop()
--- -- 3
--- ```
---
---@param n number Number of values to skip.
---@return Iter
---@diagnostic disable-next-line: unused-local
-function Iter:skipback(n) -- luacheck: no unused args
- error('skipback() requires a list-like table')
+function Iter:rskip(n) -- luacheck: no unused args
+ error('rskip() requires an array-like table')
end
---@private
-function ListIter:skipback(n)
+function ArrayIter:rskip(n)
local inc = self._head < self._tail and n or -n
self._tail = self._tail - inc
if (inc > 0 and self._head > self._tail) or (inc < 0 and self._head < self._tail) then
@@ -830,63 +832,49 @@ end
--- Gets the nth value of an iterator (and advances to it).
---
+--- If `n` is negative, offsets from the end of a |list-iterator|.
+---
--- Example:
---
--- ```lua
----
--- local it = vim.iter({ 3, 6, 9, 12 })
--- it:nth(2)
--- -- 6
--- it:nth(2)
--- -- 12
---
---- ```
----
----@param n number The index of the value to return.
----@return any
-function Iter:nth(n)
- if n > 0 then
- return self:skip(n - 1):next()
- end
-end
-
---- Gets the nth value from the end of a |list-iterator| (and advances to it).
----
---- Example:
----
---- ```lua
----
---- local it = vim.iter({ 3, 6, 9, 12 })
---- it:nthback(2)
+--- local it2 = vim.iter({ 3, 6, 9, 12 })
+--- it2:nth(-2)
--- -- 9
---- it:nthback(2)
+--- it2:nth(-2)
--- -- 3
----
--- ```
---
----@param n number The index of the value to return.
+---@param n number Index of the value to return. May be negative if the source is a |list-iterator|.
---@return any
-function Iter:nthback(n)
+function Iter:nth(n)
if n > 0 then
- return self:skipback(n - 1):nextback()
+ return self:skip(n - 1):next()
+ elseif n < 0 then
+ return self:rskip(math.abs(n) - 1):pop()
end
end
--- Sets the start and end of a |list-iterator| pipeline.
---
---- Equivalent to `:skip(first - 1):skipback(len - last + 1)`.
+--- Equivalent to `:skip(first - 1):rskip(len - last + 1)`.
---
---@param first number
---@param last number
---@return Iter
---@diagnostic disable-next-line: unused-local
function Iter:slice(first, last) -- luacheck: no unused args
- error('slice() requires a list-like table')
+ error('slice() requires an array-like table')
end
---@private
-function ListIter:slice(first, last)
- return self:skip(math.max(0, first - 1)):skipback(math.max(0, self._tail - last - 1))
+function ArrayIter:slice(first, last)
+ return self:skip(math.max(0, first - 1)):rskip(math.max(0, self._tail - last - 1))
end
--- Returns true if any of the items in the iterator match the given predicate.
@@ -952,6 +940,8 @@ end
---
--- ```
---
+---@see Iter.rpeek
+---
---@return any
function Iter:last()
local last = self:next()
@@ -964,7 +954,7 @@ function Iter:last()
end
---@private
-function ListIter:last()
+function ArrayIter:last()
local inc = self._head < self._tail and 1 or -1
local v = self._table[self._tail - inc]
self._head = self._tail
@@ -1009,7 +999,7 @@ function Iter:enumerate()
end
---@private
-function ListIter:enumerate()
+function ArrayIter:enumerate()
local inc = self._head < self._tail and 1 or -1
for i = self._head, self._tail - inc, inc do
local v = self._table[i]
@@ -1039,17 +1029,14 @@ function Iter.new(src, ...)
local t = {}
- -- O(n): scan the source table to decide if it is a list (consecutive integer indices 1…n).
- local count = 0
- for _ in pairs(src) do
- count = count + 1
- local v = src[count]
- if v == nil then
+ -- O(n): scan the source table to decide if it is an array (only positive integer indices).
+ for k, v in pairs(src) do
+ if type(k) ~= 'number' or k <= 0 or math.floor(k) ~= k then
return Iter.new(pairs(src))
end
- t[count] = v
+ t[#t + 1] = v
end
- return ListIter.new(t)
+ return ArrayIter.new(t)
end
if type(src) == 'function' then
@@ -1077,69 +1064,21 @@ function Iter.new(src, ...)
return it
end
---- Create a new ListIter
+--- Create a new ArrayIter
---
----@param t table List-like table. Caller guarantees that this table is a valid list.
+---@param t table Array-like table. Caller guarantees that this table is a valid array. Can have
+--- holes (nil values).
---@return Iter
---@private
-function ListIter.new(t)
+function ArrayIter.new(t)
local it = {}
it._table = t
it._head = 1
it._tail = #t + 1
- setmetatable(it, ListIter)
+ setmetatable(it, ArrayIter)
return it
end
---- Collects an |iterable| into a table.
----
---- ```lua
---- -- Equivalent to:
---- vim.iter(f):totable()
---- ```
----
----@param f function Iterator function
----@return table
-function M.totable(f, ...)
- return Iter.new(f, ...):totable()
-end
-
---- Filters a table or other |iterable|.
----
---- ```lua
---- -- Equivalent to:
---- vim.iter(src):filter(f):totable()
---- ```
----
----@see |Iter:filter()|
----
----@param f fun(...):boolean Filter function. Accepts the current iterator or table values as
---- arguments and returns true if those values should be kept in the
---- final table
----@param src table|function Table or iterator function to filter
----@return table
-function M.filter(f, src, ...)
- return Iter.new(src, ...):filter(f):totable()
-end
-
---- Maps a table or other |iterable|.
----
---- ```lua
---- -- Equivalent to:
---- vim.iter(src):map(f):totable()
---- ```
----
----@see |Iter:map()|
----
----@param f fun(...): any? Map function. Accepts the current iterator or table values as
---- arguments and returns one or more new values. Nil values are removed
---- from the final table.
----@param src table|function Table or iterator function to filter
----@return table
-function M.map(f, src, ...)
- return Iter.new(src, ...):map(f):totable()
-end
-
return setmetatable(M, {
__call = function(_, ...)
return Iter.new(...)
diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua
index 84e9b4197d..ec00c56c7a 100644
--- a/runtime/lua/vim/keymap.lua
+++ b/runtime/lua/vim/keymap.lua
@@ -1,5 +1,20 @@
local keymap = {}
+--- Table of |:map-arguments|.
+--- Same as |nvim_set_keymap()| {opts}, except:
+--- - {replace_keycodes} defaults to `true` if "expr" is `true`.
+---
+--- Also accepts:
+--- @class vim.keymap.set.Opts : vim.api.keyset.keymap
+--- @inlinedoc
+---
+--- Creates buffer-local mapping, `0` or `true` for current buffer.
+--- @field buffer? integer|boolean
+---
+--- Make the mapping recursive. Inverse of {noremap}.
+--- (Default: `false`)
+--- @field remap? boolean
+
--- Adds a new |mapping|.
--- Examples:
---
@@ -18,20 +33,12 @@ local keymap = {}
--- vim.keymap.set('n', '[%%', '<Plug>(MatchitNormalMultiBackward)')
--- ```
---
----@param mode string|table Mode short-name, see |nvim_set_keymap()|.
+---@param mode string|string[] Mode short-name, see |nvim_set_keymap()|.
--- Can also be list of modes to create mapping on multiple modes.
---@param lhs string Left-hand side |{lhs}| of the mapping.
---@param rhs string|function Right-hand side |{rhs}| of the mapping, can be a Lua function.
---
----@param opts table|nil Table of |:map-arguments|.
---- - Same as |nvim_set_keymap()| {opts}, except:
---- - "replace_keycodes" defaults to `true` if "expr" is `true`.
---- - "noremap": inverse of "remap" (see below).
---- - Also accepts:
---- - "buffer": (integer|boolean) Creates buffer-local mapping, `0` or `true`
---- for current buffer.
---- - "remap": (boolean) Make the mapping recursive. Inverse of "noremap".
---- Defaults to `false`.
+---@param opts? vim.keymap.set.Opts
---@see |nvim_set_keymap()|
---@see |maparg()|
---@see |mapcheck()|
@@ -81,6 +88,13 @@ function keymap.set(mode, lhs, rhs, opts)
end
end
+--- @class vim.keymap.del.Opts
+--- @inlinedoc
+---
+--- Remove a mapping from the given buffer.
+--- When `0` or `true`, use the current buffer.
+--- @field buffer? integer|boolean
+
--- Remove an existing mapping.
--- Examples:
---
@@ -92,11 +106,8 @@ end
---
---@param modes string|string[]
---@param lhs string
----@param opts table|nil A table of optional arguments:
---- - "buffer": (integer|boolean) Remove a mapping from the given buffer.
---- When `0` or `true`, use the current buffer.
+---@param opts? vim.keymap.del.Opts
---@see |vim.keymap.set()|
----
function keymap.del(modes, lhs, opts)
vim.validate({
mode = { modes, { 's', 't' } },
@@ -106,6 +117,7 @@ function keymap.del(modes, lhs, opts)
opts = opts or {}
modes = type(modes) == 'string' and { modes } or modes
+ --- @cast modes string[]
local buffer = false ---@type false|integer
if opts.buffer ~= nil then
diff --git a/runtime/lua/vim/loader.lua b/runtime/lua/vim/loader.lua
index d3d8948654..ea77a22416 100644
--- a/runtime/lua/vim/loader.lua
+++ b/runtime/lua/vim/loader.lua
@@ -85,7 +85,7 @@ function Loader.get_hash(path)
end
local function normalize(path)
- return fs.normalize(path, { expand_env = false })
+ return fs.normalize(path, { expand_env = false, _fast = true })
end
--- Gets the rtp excluding after directories.
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index d5c376ba44..1592fd3151 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -1,7 +1,5 @@
local api = vim.api
-local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend
local validate = vim.validate
-local if_nil = vim.F.if_nil
local lsp = vim._defer_require('vim.lsp', {
_changetracking = ..., --- @module 'vim.lsp._changetracking'
@@ -45,6 +43,9 @@ lsp._request_name_to_capability = {
[ms.textDocument_prepareCallHierarchy] = { 'callHierarchyProvider' },
[ms.callHierarchy_incomingCalls] = { 'callHierarchyProvider' },
[ms.callHierarchy_outgoingCalls] = { 'callHierarchyProvider' },
+ [ms.textDocument_prepareTypeHierarchy] = { 'typeHierarchyProvider' },
+ [ms.typeHierarchy_subtypes] = { 'typeHierarchyProvider' },
+ [ms.typeHierarchy_supertypes] = { 'typeHierarchyProvider' },
[ms.textDocument_rename] = { 'renameProvider' },
[ms.textDocument_prepareRename] = { 'renameProvider', 'prepareProvider' },
[ms.textDocument_codeAction] = { 'codeActionProvider' },
@@ -63,6 +64,12 @@ lsp._request_name_to_capability = {
[ms.textDocument_inlayHint] = { 'inlayHintProvider' },
[ms.textDocument_diagnostic] = { 'diagnosticProvider' },
[ms.inlayHint_resolve] = { 'inlayHintProvider', 'resolveProvider' },
+ [ms.textDocument_documentLink] = { 'documentLinkProvider' },
+ [ms.documentLink_resolve] = { 'documentLinkProvider', 'resolveProvider' },
+ [ms.textDocument_didClose] = { 'textDocumentSync', 'openClose' },
+ [ms.textDocument_didOpen] = { 'textDocumentSync', 'openClose' },
+ [ms.textDocument_willSave] = { 'textDocumentSync', 'willSave' },
+ [ms.textDocument_willSaveWaitUntil] = { 'textDocumentSync', 'willSaveWaitUntil' },
}
-- TODO improve handling of scratch buffers with LSP attached.
@@ -108,40 +115,7 @@ function lsp._buf_get_line_ending(bufnr)
end
-- Tracks all clients created via lsp.start_client
-local active_clients = {} --- @type table<integer,vim.lsp.Client>
-local all_buffer_active_clients = {} --- @type table<integer,table<integer,true>>
-local uninitialized_clients = {} --- @type table<integer,vim.lsp.Client>
-
----@param bufnr? integer
----@param fn fun(client: vim.lsp.Client, client_id: integer, bufnr: integer)
-local function for_each_buffer_client(bufnr, fn, restrict_client_ids)
- validate({
- fn = { fn, 'f' },
- restrict_client_ids = { restrict_client_ids, 't', true },
- })
- bufnr = resolve_bufnr(bufnr)
- local client_ids = all_buffer_active_clients[bufnr]
- if not client_ids or tbl_isempty(client_ids) then
- return
- end
-
- if restrict_client_ids and #restrict_client_ids > 0 then
- local filtered_client_ids = {} --- @type table<integer,true>
- for client_id in pairs(client_ids) do
- if vim.list_contains(restrict_client_ids, client_id) then
- filtered_client_ids[client_id] = true
- end
- end
- client_ids = filtered_client_ids
- end
-
- for client_id in pairs(client_ids) do
- local client = active_clients[client_id]
- if client then
- fn(client, client_id, bufnr)
- end
- end
-end
+local all_clients = {} --- @type table<integer,vim.lsp.Client>
local client_errors_base = table.maxn(lsp.rpc.client_errors)
local client_errors_offset = 0
@@ -156,7 +130,7 @@ end
--- Can be used to look up the string from a the number or the number
--- from the string.
--- @nodoc
-lsp.client_errors = tbl_extend(
+lsp.client_errors = vim.tbl_extend(
'error',
lsp.rpc.client_errors,
client_error('BEFORE_INIT_CALLBACK_ERROR'),
@@ -199,16 +173,41 @@ local function once(fn)
end
end
+--- @param client vim.lsp.Client
+--- @param config vim.lsp.ClientConfig
+--- @return boolean
+local function reuse_client_default(client, config)
+ if client.name ~= config.name then
+ return false
+ end
+
+ if config.root_dir then
+ 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 config.root_dir == dir.name then
+ return true
+ end
+ end
+ end
+
+ -- TODO(lewis6991): also check config.workspace_folders
+
+ return false
+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 name and
--- root_dir matches.
---- @field reuse_client fun(client: vim.lsp.Client, config: table): boolean
+--- @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
+---
+--- Suppress error reporting if the LSP server fails to start (default false).
+--- @field silent? boolean
--- 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`.
@@ -220,7 +219,7 @@ end
--- vim.lsp.start({
--- name = 'my-server-name',
--- cmd = {'name-of-language-server-executable'},
---- root_dir = vim.fs.dirname(vim.fs.find({'pyproject.toml', 'setup.py'}, { upward = true })[1]),
+--- root_dir = vim.fs.root(0, {'pyproject.toml', 'setup.py'}),
--- })
--- ```
---
@@ -229,9 +228,9 @@ end
--- - `name` arbitrary name for the LSP client. Should be unique per language server.
--- - `cmd` command string[] or function, described at |vim.lsp.start_client()|.
--- - `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.find()| and |vim.fs.dirname()| to detect the
---- root by traversing the file system upwards starting from the current directory until either
---- a `pyproject.toml` or `setup.py` file is found.
+--- should be re-used. The example above uses |vim.fs.root()| and |vim.fs.dirname()| to detect
+--- the root by traversing the file system upwards starting from the current directory until
+--- either a `pyproject.toml` or `setup.py` file is found.
--- - `workspace_folders` list of `{ uri:string, name: string }` tables specifying the project root
--- folders used by the language server. If `nil` the property is derived from `root_dir` for
--- convenience.
@@ -251,30 +250,32 @@ end
--- @return integer? client_id
function lsp.start(config, opts)
opts = opts or {}
- local reuse_client = opts.reuse_client
- or function(client, conf)
- return client.root_dir == conf.root_dir and client.name == conf.name
- end
-
+ local reuse_client = opts.reuse_client or reuse_client_default
local bufnr = resolve_bufnr(opts.bufnr)
- for _, clients in ipairs({ uninitialized_clients, lsp.get_clients() }) do
- for _, client in pairs(clients) do
- if reuse_client(client, config) then
- lsp.buf_attach_client(bufnr, client.id)
+ for _, client in pairs(all_clients) do
+ if reuse_client(client, config) then
+ if lsp.buf_attach_client(bufnr, client.id) then
return client.id
+ else
+ return nil
end
end
end
- local client_id = lsp.start_client(config)
+ local client_id, err = lsp.start_client(config)
+ if err then
+ if not opts.silent then
+ vim.notify(err, vim.log.levels.WARN)
+ end
+ return nil
+ end
- if not client_id then
- return -- lsp.start_client will have printed an error
+ if client_id and lsp.buf_attach_client(bufnr, client_id) then
+ return client_id
end
- lsp.buf_attach_client(bufnr, client_id)
- return client_id
+ return nil
end
--- Consumes the latest progress messages from all clients and formats them as a string.
@@ -317,6 +318,7 @@ local function is_empty_or_default(bufnr, option)
end
local info = api.nvim_get_option_info2(option, { buf = bufnr })
+ ---@param e vim.fn.getscriptinfo.ret
local scriptinfo = vim.tbl_filter(function(e)
return e.sid == info.last_set_sid
end, vim.fn.getscriptinfo())
@@ -355,7 +357,7 @@ function lsp._set_defaults(client, bufnr)
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 })
+ vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = bufnr, desc = 'vim.lsp.buf.hover()' })
end
end)
if client.supports_method(ms.textDocument_diagnostic) then
@@ -383,48 +385,30 @@ local function reset_defaults(bufnr)
end)
end
---- @param client vim.lsp.Client
-local function on_client_init(client)
- local id = client.id
- uninitialized_clients[id] = nil
- -- Only assign after initialized.
- active_clients[id] = client
- -- If we had been registered before we start, then send didOpen This can
- -- happen if we attach to buffers before initialize finishes or if
- -- someone restarts a client.
- for bufnr, client_ids in pairs(all_buffer_active_clients) do
- if client_ids[id] then
- client.on_attach(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 = active_clients[client_id] or uninitialized_clients[client_id]
-
- for bufnr, client_ids in pairs(all_buffer_active_clients) do
- if client_ids[client_id] then
- vim.schedule(function()
- if client and client.attached_buffers[bufnr] then
- api.nvim_exec_autocmds('LspDetach', {
- buffer = bufnr,
- modeline = false,
- data = { client_id = client_id },
- })
- end
+ local client = all_clients[client_id]
+
+ for bufnr in pairs(client.attached_buffers) do
+ vim.schedule(function()
+ if client and client.attached_buffers[bufnr] then
+ api.nvim_exec_autocmds('LspDetach', {
+ buffer = bufnr,
+ modeline = false,
+ data = { client_id = client_id },
+ })
+ end
- local namespace = vim.lsp.diagnostic.get_namespace(client_id)
- vim.diagnostic.reset(namespace, bufnr)
+ local namespace = vim.lsp.diagnostic.get_namespace(client_id)
+ vim.diagnostic.reset(namespace, bufnr)
+ client.attached_buffers[bufnr] = nil
- client_ids[client_id] = nil
- if vim.tbl_isempty(client_ids) then
- reset_defaults(bufnr)
- end
- end)
- end
+ if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then
+ reset_defaults(bufnr)
+ end
+ end)
end
local name = client.name or 'unknown'
@@ -432,8 +416,7 @@ local function on_client_exit(code, signal, client_id)
-- Schedule the deletion of the client object so that it exists in the execution of LspDetach
-- autocommands
vim.schedule(function()
- active_clients[client_id] = nil
- uninitialized_clients[client_id] = nil
+ all_clients[client_id] = nil
-- Client can be absent if executable starts, but initialize fails
-- init/attach won't have happened
@@ -455,51 +438,26 @@ end
--- Starts and initializes a client with the given configuration.
--- @param config vim.lsp.ClientConfig Configuration for the server.
---- @return integer|nil 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 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
function lsp.start_client(config)
- local client = require('vim.lsp.client').create(config)
-
- if not client then
- return
+ local ok, res = pcall(require('vim.lsp.client').create, config)
+ if not ok then
+ return nil, res --[[@as string]]
end
- --- @diagnostic disable-next-line: invisible
- table.insert(client._on_init_cbs, on_client_init)
+ local client = assert(res)
+
--- @diagnostic disable-next-line: invisible
table.insert(client._on_exit_cbs, on_client_exit)
- -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes.
- uninitialized_clients[client.id] = client
+ all_clients[client.id] = client
client:initialize()
- return client.id
-end
-
---- Notify all attached clients that a buffer has changed.
----@param _ integer
----@param bufnr integer
----@param changedtick integer
----@param firstline integer
----@param lastline integer
----@param new_lastline integer
----@return true?
-local function text_document_did_change_handler(
- _,
- bufnr,
- changedtick,
- firstline,
- lastline,
- new_lastline
-)
- -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached
- if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then
- return true
- end
- util.buf_versions[bufnr] = changedtick
- changetracking.send_changes(bufnr, firstline, lastline, new_lastline)
+ return client.id, nil
end
---Buffer lifecycle handler for textDocument/didSave
@@ -543,6 +501,117 @@ local function text_document_did_save_handler(bufnr)
end
end
+---@param bufnr integer resolved buffer
+---@param client vim.lsp.Client
+local function buf_detach_client(bufnr, client)
+ api.nvim_exec_autocmds('LspDetach', {
+ buffer = bufnr,
+ modeline = false,
+ data = { client_id = client.id },
+ })
+
+ changetracking.reset_buf(client, bufnr)
+
+ 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)
+ end
+
+ client.attached_buffers[bufnr] = nil
+ util.buf_versions[bufnr] = nil
+
+ local namespace = lsp.diagnostic.get_namespace(client.id)
+ vim.diagnostic.reset(namespace, bufnr)
+end
+
+--- @type table<integer,true>
+local attached_buffers = {}
+
+--- @param bufnr integer
+local function buf_attach(bufnr)
+ if attached_buffers[bufnr] then
+ return
+ end
+ attached_buffers[bufnr] = true
+
+ local uri = vim.uri_from_bufnr(bufnr)
+ local augroup = ('lsp_b_%d_save'):format(bufnr)
+ local group = api.nvim_create_augroup(augroup, { clear = true })
+ api.nvim_create_autocmd('BufWritePre', {
+ group = group,
+ buffer = bufnr,
+ desc = 'vim.lsp: textDocument/willSave',
+ callback = function(ctx)
+ for _, client in ipairs(lsp.get_clients({ bufnr = ctx.buf })) do
+ local params = {
+ textDocument = {
+ uri = uri,
+ },
+ reason = protocol.TextDocumentSaveReason.Manual, ---@type integer
+ }
+ if client.supports_method(ms.textDocument_willSave) then
+ client.notify(ms.textDocument_willSave, params)
+ end
+ if client.supports_method(ms.textDocument_willSaveWaitUntil) then
+ local result, err =
+ 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
+ log.error(vim.inspect(err))
+ end
+ end
+ end
+ end,
+ })
+ api.nvim_create_autocmd('BufWritePost', {
+ group = group,
+ buffer = bufnr,
+ desc = 'vim.lsp: textDocument/didSave handler',
+ callback = function(ctx)
+ text_document_did_save_handler(ctx.buf)
+ end,
+ })
+ -- First time, so attach and set up stuff.
+ api.nvim_buf_attach(bufnr, false, {
+ on_lines = function(_, _, changedtick, firstline, lastline, new_lastline)
+ if #lsp.get_clients({ bufnr = bufnr }) == 0 then
+ return true -- detach
+ end
+ util.buf_versions[bufnr] = changedtick
+ changetracking.send_changes(bufnr, firstline, lastline, new_lastline)
+ end,
+
+ on_reload = function()
+ local clients = lsp.get_clients({ bufnr = 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)
+ end
+ end
+ for _, client in ipairs(clients) do
+ client:_text_document_did_open_handler(bufnr)
+ end
+ end,
+
+ on_detach = function()
+ local clients = lsp.get_clients({ bufnr = bufnr, _uninitialized = true })
+ for _, client in ipairs(clients) do
+ buf_detach_client(bufnr, client)
+ end
+ attached_buffers[bufnr] = nil
+ end,
+
+ -- TODO if we know all of the potential clients ahead of time, then we
+ -- could conditionally set this.
+ -- utf_sizes = size_index > 1;
+ utf_sizes = true,
+ })
+end
+
--- Implements the `textDocument/did…` notifications required to track a buffer
--- for any language server.
---
@@ -561,92 +630,24 @@ function lsp.buf_attach_client(bufnr, client_id)
log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr))
return false
end
- local buffer_client_ids = all_buffer_active_clients[bufnr]
- -- This is our first time attaching to this buffer.
- if not buffer_client_ids then
- buffer_client_ids = {}
- all_buffer_active_clients[bufnr] = buffer_client_ids
- local uri = vim.uri_from_bufnr(bufnr)
- local augroup = ('lsp_c_%d_b_%d_save'):format(client_id, bufnr)
- local group = api.nvim_create_augroup(augroup, { clear = true })
- api.nvim_create_autocmd('BufWritePre', {
- group = group,
- buffer = bufnr,
- desc = 'vim.lsp: textDocument/willSave',
- callback = function(ctx)
- for _, client in ipairs(lsp.get_clients({ bufnr = ctx.buf })) do
- local params = {
- textDocument = {
- uri = uri,
- },
- reason = protocol.TextDocumentSaveReason.Manual,
- }
- if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then
- client.notify(ms.textDocument_willSave, params)
- end
- if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSaveWaitUntil') then
- local result, err =
- 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
- log.error(vim.inspect(err))
- end
- end
- end
- end,
- })
- api.nvim_create_autocmd('BufWritePost', {
- group = group,
- buffer = bufnr,
- desc = 'vim.lsp: textDocument/didSave handler',
- callback = function(ctx)
- text_document_did_save_handler(ctx.buf)
- end,
- })
- -- First time, so attach and set up stuff.
- api.nvim_buf_attach(bufnr, false, {
- on_lines = text_document_did_change_handler,
- on_reload = function()
- local params = { textDocument = { uri = uri } }
- for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do
- changetracking.reset_buf(client, bufnr)
- if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then
- client.notify(ms.textDocument_didClose, params)
- end
- client:_text_document_did_open_handler(bufnr)
- end
- end,
- on_detach = function()
- local params = { textDocument = { uri = uri } }
- for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do
- changetracking.reset_buf(client, bufnr)
- if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then
- client.notify(ms.textDocument_didClose, params)
- end
- client.attached_buffers[bufnr] = nil
- end
- util.buf_versions[bufnr] = nil
- all_buffer_active_clients[bufnr] = nil
- end,
- -- TODO if we know all of the potential clients ahead of time, then we
- -- could conditionally set this.
- -- utf_sizes = size_index > 1;
- utf_sizes = true,
- })
+ local client = lsp.get_client_by_id(client_id)
+ if not client then
+ return false
end
- if buffer_client_ids[client_id] then
+ buf_attach(bufnr)
+
+ if client.attached_buffers[bufnr] then
return true
end
- -- This is our first time attaching this client to this buffer.
- buffer_client_ids[client_id] = true
- local client = active_clients[client_id]
+ client.attached_buffers[bufnr] = true
+
+ -- This is our first time attaching this client to this buffer.
-- Send didOpen for the client if it is initialized. If it isn't initialized
-- then it will send didOpen on initialize.
- if client then
+ if client.initialized then
client:_on_attach(bufnr)
end
return true
@@ -665,7 +666,7 @@ function lsp.buf_detach_client(bufnr, client_id)
})
bufnr = resolve_bufnr(bufnr)
- local client = lsp.get_client_by_id(client_id)
+ local client = all_clients[client_id]
if not client or not client.attached_buffers[bufnr] then
vim.notify(
string.format(
@@ -675,32 +676,9 @@ function lsp.buf_detach_client(bufnr, client_id)
)
)
return
+ else
+ buf_detach_client(bufnr, client)
end
-
- api.nvim_exec_autocmds('LspDetach', {
- buffer = bufnr,
- modeline = false,
- data = { client_id = client_id },
- })
-
- changetracking.reset_buf(client, bufnr)
-
- if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then
- local uri = vim.uri_from_bufnr(bufnr)
- local params = { textDocument = { uri = uri } }
- client.notify(ms.textDocument_didClose, params)
- end
-
- client.attached_buffers[bufnr] = nil
- util.buf_versions[bufnr] = nil
-
- all_buffer_active_clients[bufnr][client_id] = nil
- if #vim.tbl_keys(all_buffer_active_clients[bufnr]) == 0 then
- all_buffer_active_clients[bufnr] = nil
- end
-
- local namespace = lsp.diagnostic.get_namespace(client_id)
- vim.diagnostic.reset(namespace, bufnr)
end
--- Checks if a buffer is attached for a particular client.
@@ -708,7 +686,7 @@ end
---@param bufnr (integer) Buffer handle, or 0 for current
---@param client_id (integer) the client id
function lsp.buf_is_attached(bufnr, client_id)
- return (all_buffer_active_clients[resolve_bufnr(bufnr)] or {})[client_id] == true
+ return lsp.get_clients({ bufnr = bufnr, id = client_id, _uninitialized = true })[1] ~= nil
end
--- Gets a client by id, or nil if the id is invalid.
@@ -718,7 +696,7 @@ end
---
---@return (nil|vim.lsp.Client) client rpc object
function lsp.get_client_by_id(client_id)
- return active_clients[client_id] or uninitialized_clients[client_id]
+ return all_clients[client_id]
end
--- Returns list of buffers attached to client_id.
@@ -726,7 +704,7 @@ end
---@param client_id integer client id
---@return integer[] buffers list of buffer ids
function lsp.get_buffers_by_client_id(client_id)
- local client = lsp.get_client_by_id(client_id)
+ local client = all_clients[client_id]
return client and vim.tbl_keys(client.attached_buffers) or {}
end
@@ -742,17 +720,22 @@ end
--- By default asks the server to shutdown, unless stop was requested
--- already for this client, then force-shutdown is attempted.
---
----@param client_id integer|vim.lsp.Client id or |vim.lsp.Client| object, or list thereof
----@param force boolean|nil shutdown forcefully
+---@param client_id integer|integer[]|vim.lsp.Client[] id, list of id's, or list of |vim.lsp.Client| objects
+---@param force? boolean shutdown forcefully
function lsp.stop_client(client_id, force)
+ --- @type integer[]|vim.lsp.Client[]
local ids = type(client_id) == 'table' and client_id or { client_id }
for _, id in ipairs(ids) do
- if type(id) == 'table' and id.stop ~= nil then
- id.stop(force)
- elseif active_clients[id] then
- active_clients[id].stop(force)
- elseif uninitialized_clients[id] then
- uninitialized_clients[id].stop(true)
+ if type(id) == 'table' then
+ if id.stop then
+ id.stop(force)
+ end
+ else
+ --- @cast id -vim.lsp.Client
+ local client = all_clients[id]
+ if client then
+ client.stop(force)
+ end
end
end
end
@@ -772,6 +755,9 @@ end
---
--- Only return clients supporting the given method
--- @field method? string
+---
+--- Also return uninitialized clients.
+--- @field package _uninitialized? boolean
--- Get active clients.
---
@@ -784,15 +770,16 @@ function lsp.get_clients(filter)
local clients = {} --- @type vim.lsp.Client[]
- local t = filter.bufnr and (all_buffer_active_clients[resolve_bufnr(filter.bufnr)] or {})
- or active_clients
- for client_id in pairs(t) do
- local client = active_clients[client_id]
+ local bufnr = filter.bufnr and resolve_bufnr(filter.bufnr)
+
+ for _, client in pairs(all_clients) do
if
client
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._uninitialized or client.initialized)
then
clients[#clients + 1] = client
end
@@ -810,15 +797,9 @@ end
api.nvim_create_autocmd('VimLeavePre', {
desc = 'vim.lsp: exit handler',
callback = function()
+ local active_clients = lsp.get_clients()
log.info('exit_handler', active_clients)
- for _, client in pairs(uninitialized_clients) do
- client.stop(true)
- end
- -- TODO handle v:dying differently?
- if tbl_isempty(active_clients) then
- return
- end
- for _, client in pairs(active_clients) do
+ for _, client in pairs(all_clients) do
client.stop()
end
@@ -827,7 +808,7 @@ api.nvim_create_autocmd('VimLeavePre', {
local send_kill = false
for client_id, client in pairs(active_clients) do
- local timeout = if_nil(client.flags.exit_timeout, false)
+ local timeout = client.flags.exit_timeout
if timeout then
send_kill = true
timeouts[client_id] = timeout
@@ -910,7 +891,7 @@ function lsp.buf_request(bufnr, method, params, handler)
local function _cancel_all_requests()
for client_id, request_id in pairs(client_request_ids) do
- local client = active_clients[client_id]
+ local client = all_clients[client_id]
client.cancel_request(request_id)
end
end
@@ -924,12 +905,12 @@ end
---@param bufnr (integer) Buffer handle, or 0 for current.
---@param method (string) LSP method name
---@param params (table|nil) Parameters to send to the server
----@param handler fun(results: table<integer, {error: lsp.ResponseError, result: any}>) (function)
+---@param handler fun(results: table<integer, {error: lsp.ResponseError?, result: any}>) (function)
--- Handler called after all requests are completed. Server results are passed as
--- a `client_id:result` map.
---@return function cancel Function that cancels all requests.
function lsp.buf_request_all(bufnr, method, params, handler)
- local results = {} --- @type table<integer,{error:string, result:any}>
+ local results = {} --- @type table<integer,{error: lsp.ResponseError?, result: any}>
local result_count = 0
local expected_result_count = 0
@@ -967,10 +948,10 @@ end
---@param params table? Parameters to send to the server
---@param timeout_ms integer? Maximum time in milliseconds to wait for a result.
--- (default: `1000`)
----@return table<integer, {err: lsp.ResponseError, result: any}>? result Map of client_id:request_result.
+---@return table<integer, {error: lsp.ResponseError?, result: any}>? result Map of client_id:request_result.
---@return string? err On timeout, cancel, or error, `err` is a string describing the failure reason, and `result` is nil.
function lsp.buf_request_sync(bufnr, method, params, timeout_ms)
- local request_results
+ local request_results ---@type table
local cancel = lsp.buf_request_all(bufnr, method, params, function(it)
request_results = it
@@ -1106,7 +1087,7 @@ end
---@return boolean stopped true if client is stopped, false otherwise.
function lsp.client_is_stopped(client_id)
assert(client_id, 'missing client_id param')
- return active_clients[client_id] == nil and not uninitialized_clients[client_id]
+ return not all_clients[client_id]
end
--- Gets a map of client_id:client pairs for the given buffer, where each value
@@ -1172,7 +1153,11 @@ function lsp.for_each_buffer_client(bufnr, fn)
'lsp.get_clients({ bufnr = bufnr }) with regular loop',
'0.12'
)
- return for_each_buffer_client(bufnr, fn)
+ bufnr = resolve_bufnr(bufnr)
+
+ for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do
+ fn(client, client.id, bufnr)
+ end
end
--- Function to manage overriding defaults for LSP handlers.
diff --git a/runtime/lua/vim/lsp/_meta/protocol.lua b/runtime/lua/vim/lsp/_meta/protocol.lua
index a5da5ac6b7..9a11972007 100644
--- a/runtime/lua/vim/lsp/_meta/protocol.lua
+++ b/runtime/lua/vim/lsp/_meta/protocol.lua
@@ -2534,8 +2534,14 @@ error('Cannot require a meta file')
---@proposed
---@field inlineCompletionProvider? boolean|lsp.InlineCompletionOptions
---
+---Text document specific server capabilities.
+---
+---@since 3.18.0
+---@proposed
+---@field textDocument? lsp._anonym12.textDocument
+---
---Workspace specific server capabilities.
----@field workspace? lsp._anonym12.workspace
+---@field workspace? lsp._anonym14.workspace
---
---Experimental server capabilities.
---@field experimental? lsp.LSPAny
@@ -2598,8 +2604,10 @@ error('Cannot require a meta file')
---appears in the user interface.
---@field source? string
---
----The diagnostic's message. It usually appears in the user interface
----@field message string
+---The diagnostic's message. It usually appears in the user interface.
+---
+---@since 3.18.0 - support for `MarkupContent`. This is guarded by the client capability `textDocument.diagnostic.markupMessageSupport`.
+---@field message string|lsp.MarkupContent
---
---Additional metadata about the diagnostic.
---
@@ -2684,7 +2692,7 @@ error('Cannot require a meta file')
---capabilities.
---
---@since 3.17.0
----@field completionItem? lsp._anonym13.completionItem
+---@field completionItem? lsp._anonym15.completionItem
---Hover options.
---@class lsp.HoverOptions: lsp.WorkDoneProgressOptions
@@ -2811,6 +2819,8 @@ error('Cannot require a meta file')
---errors are currently presented to the user for the given range. There is no guarantee
---that these accurately reflect the error state of the resource. The primary parameter
---to compute code actions is the provided range.
+---
+---Note that the client should check the `textDocument.diagnostic.markupMessageSupport` server capability before sending diagnostics with markup messages to a server.
---@field diagnostics lsp.Diagnostic[]
---
---Requested kind of actions to return.
@@ -3146,7 +3156,7 @@ error('Cannot require a meta file')
---@class lsp.NotebookDocumentSyncOptions
---
---The notebooks to be synced
----@field notebookSelector (lsp._anonym14.notebookSelector|lsp._anonym16.notebookSelector)[]
+---@field notebookSelector (lsp._anonym16.notebookSelector|lsp._anonym18.notebookSelector)[]
---
---Whether save notification should be forwarded to
---the server. Will only be honored if mode === `notebook`.
@@ -3514,7 +3524,7 @@ error('Cannot require a meta file')
---anymore since the information is outdated).
---
---@since 3.17.0
----@field staleRequestSupport? lsp._anonym18.staleRequestSupport
+---@field staleRequestSupport? lsp._anonym20.staleRequestSupport
---
---Client capabilities specific to regular expressions.
---
@@ -3590,7 +3600,7 @@ error('Cannot require a meta file')
---create file, rename file and delete file changes.
---
---@since 3.16.0
----@field changeAnnotationSupport? lsp._anonym19.changeAnnotationSupport
+---@field changeAnnotationSupport? lsp._anonym21.changeAnnotationSupport
---@class lsp.DidChangeConfigurationClientCapabilities
---
@@ -3617,20 +3627,20 @@ error('Cannot require a meta file')
---@field dynamicRegistration? boolean
---
---Specific capabilities for the `SymbolKind` in the `workspace/symbol` request.
----@field symbolKind? lsp._anonym20.symbolKind
+---@field symbolKind? lsp._anonym22.symbolKind
---
---The client supports tags on `SymbolInformation`.
---Clients supporting tags have to handle unknown tags gracefully.
---
---@since 3.16.0
----@field tagSupport? lsp._anonym21.tagSupport
+---@field tagSupport? lsp._anonym23.tagSupport
---
---The client support partial workspace symbols. The client will send the
---request `workspaceSymbol/resolve` to the server to resolve additional
---properties.
---
---@since 3.17.0
----@field resolveSupport? lsp._anonym22.resolveSupport
+---@field resolveSupport? lsp._anonym24.resolveSupport
---The client capabilities of a {@link ExecuteCommandRequest}.
---@class lsp.ExecuteCommandClientCapabilities
@@ -3775,9 +3785,9 @@ error('Cannot require a meta file')
---
---The client supports the following `CompletionItem` specific
---capabilities.
----@field completionItem? lsp._anonym23.completionItem
+---@field completionItem? lsp._anonym25.completionItem
---
----@field completionItemKind? lsp._anonym27.completionItemKind
+---@field completionItemKind? lsp._anonym29.completionItemKind
---
---Defines how the client handles whitespace and indentation
---when accepting a completion item that uses multi line
@@ -3794,7 +3804,7 @@ error('Cannot require a meta file')
---capabilities.
---
---@since 3.17.0
----@field completionList? lsp._anonym28.completionList
+---@field completionList? lsp._anonym30.completionList
---@class lsp.HoverClientCapabilities
---
@@ -3813,7 +3823,7 @@ error('Cannot require a meta file')
---
---The client supports the following `SignatureInformation`
---specific properties.
----@field signatureInformation? lsp._anonym29.signatureInformation
+---@field signatureInformation? lsp._anonym31.signatureInformation
---
---The client supports to send additional context information for a
---`textDocument/signatureHelp` request. A client that opts into
@@ -3891,7 +3901,7 @@ error('Cannot require a meta file')
---
---Specific capabilities for the `SymbolKind` in the
---`textDocument/documentSymbol` request.
----@field symbolKind? lsp._anonym31.symbolKind
+---@field symbolKind? lsp._anonym33.symbolKind
---
---The client supports hierarchical document symbols.
---@field hierarchicalDocumentSymbolSupport? boolean
@@ -3901,7 +3911,7 @@ error('Cannot require a meta file')
---Clients supporting tags have to handle unknown tags gracefully.
---
---@since 3.16.0
----@field tagSupport? lsp._anonym32.tagSupport
+---@field tagSupport? lsp._anonym34.tagSupport
---
---The client supports an additional label presented in the UI when
---registering a document symbol provider.
@@ -3920,7 +3930,7 @@ error('Cannot require a meta file')
---set the request can only return `Command` literals.
---
---@since 3.8.0
----@field codeActionLiteralSupport? lsp._anonym33.codeActionLiteralSupport
+---@field codeActionLiteralSupport? lsp._anonym35.codeActionLiteralSupport
---
---Whether code action supports the `isPreferred` property.
---
@@ -3943,7 +3953,7 @@ error('Cannot require a meta file')
---properties via a separate `codeAction/resolve` request.
---
---@since 3.16.0
----@field resolveSupport? lsp._anonym35.resolveSupport
+---@field resolveSupport? lsp._anonym37.resolveSupport
---
---Whether the client honors the change annotations in
---text edits and resource operations returned via the
@@ -4051,12 +4061,12 @@ error('Cannot require a meta file')
---Specific options for the folding range kind.
---
---@since 3.17.0
----@field foldingRangeKind? lsp._anonym36.foldingRangeKind
+---@field foldingRangeKind? lsp._anonym38.foldingRangeKind
---
---Specific options for the folding range.
---
---@since 3.17.0
----@field foldingRange? lsp._anonym37.foldingRange
+---@field foldingRange? lsp._anonym39.foldingRange
---@class lsp.SelectionRangeClientCapabilities
---
@@ -4075,7 +4085,7 @@ error('Cannot require a meta file')
---Clients supporting tags have to handle unknown tags gracefully.
---
---@since 3.15.0
----@field tagSupport? lsp._anonym38.tagSupport
+---@field tagSupport? lsp._anonym40.tagSupport
---
---Whether the client interprets the version property of the
---`textDocument/publishDiagnostics` notification's parameter.
@@ -4119,7 +4129,7 @@ error('Cannot require a meta file')
---`request.range` are both set to true but the server only provides a
---range provider the client might not render a minimap correctly or might
---even decide to not show any semantic tokens at all.
----@field requests lsp._anonym39.requests
+---@field requests lsp._anonym41.requests
---
---The token types that the client supports.
---@field tokenTypes string[]
@@ -4202,7 +4212,7 @@ error('Cannot require a meta file')
---
---Indicates which properties a client can resolve lazily on an inlay
---hint.
----@field resolveSupport? lsp._anonym42.resolveSupport
+---@field resolveSupport? lsp._anonym44.resolveSupport
---Client capabilities specific to diagnostic pull requests.
---
@@ -4216,6 +4226,9 @@ error('Cannot require a meta file')
---
---Whether the clients supports related documents for document diagnostic pulls.
---@field relatedDocumentSupport? boolean
+---
+---Whether the client supports `MarkupContent` in diagnostic messages.
+---@field markupMessageSupport? boolean
---Client capabilities specific to inline completions.
---
@@ -4244,7 +4257,7 @@ error('Cannot require a meta file')
---@class lsp.ShowMessageRequestClientCapabilities
---
---Capabilities specific to the `MessageActionItem` type.
----@field messageActionItem? lsp._anonym43.messageActionItem
+---@field messageActionItem? lsp._anonym45.messageActionItem
---Client capabilities for the showDocument request.
---
@@ -4671,7 +4684,7 @@ error('Cannot require a meta file')
---@since 3.17.0
---@alias lsp.DocumentDiagnosticReport lsp.RelatedFullDocumentDiagnosticReport|lsp.RelatedUnchangedDocumentDiagnosticReport
----@alias lsp.PrepareRenameResult lsp.Range|lsp._anonym44.PrepareRenameResult|lsp._anonym45.PrepareRenameResult
+---@alias lsp.PrepareRenameResult lsp.Range|lsp._anonym46.PrepareRenameResult|lsp._anonym47.PrepareRenameResult
---A document selector is the combination of one or many document filters.
---
@@ -4692,7 +4705,7 @@ error('Cannot require a meta file')
---An event describing a change to a text document. If only a text is provided
---it is considered to be the full content of the document.
----@alias lsp.TextDocumentContentChangeEvent lsp._anonym46.TextDocumentContentChangeEvent|lsp._anonym47.TextDocumentContentChangeEvent
+---@alias lsp.TextDocumentContentChangeEvent lsp._anonym48.TextDocumentContentChangeEvent|lsp._anonym49.TextDocumentContentChangeEvent
---MarkedString can be used to render human readable text. It is either a markdown string
---or a code-block that provides a language and a code snippet. The language identifier
@@ -4706,7 +4719,7 @@ error('Cannot require a meta file')
---
---Note that markdown strings will be sanitized - that means html will be escaped.
---@deprecated use MarkupContent instead.
----@alias lsp.MarkedString string|lsp._anonym48.MarkedString
+---@alias lsp.MarkedString string|lsp._anonym50.MarkedString
---A document filter describes a top level text document or
---a notebook cell document.
@@ -4739,14 +4752,14 @@ error('Cannot require a meta file')
---\@sample A language filter that applies to all package.json paths: `{ language: 'json', pattern: '**package.json' }`
---
---@since 3.17.0
----@alias lsp.TextDocumentFilter lsp._anonym49.TextDocumentFilter|lsp._anonym50.TextDocumentFilter|lsp._anonym51.TextDocumentFilter
+---@alias lsp.TextDocumentFilter lsp._anonym51.TextDocumentFilter|lsp._anonym52.TextDocumentFilter|lsp._anonym53.TextDocumentFilter
---A notebook document filter denotes a notebook document by
---different properties. The properties will be match
---against the notebook's URI (same as with documents)
---
---@since 3.17.0
----@alias lsp.NotebookDocumentFilter lsp._anonym52.NotebookDocumentFilter|lsp._anonym53.NotebookDocumentFilter|lsp._anonym54.NotebookDocumentFilter
+---@alias lsp.NotebookDocumentFilter lsp._anonym54.NotebookDocumentFilter|lsp._anonym55.NotebookDocumentFilter|lsp._anonym56.NotebookDocumentFilter
---The glob pattern to watch relative to the base path. Glob patterns can have the following syntax:
---- `*` to match one or more characters in a path segment
@@ -4856,7 +4869,19 @@ error('Cannot require a meta file')
---The client's version as defined by the client.
---@field version? string
----@class lsp._anonym12.workspace
+---@class lsp._anonym13.textDocument.diagnostic
+---
+---Whether the server supports `MarkupContent` in diagnostic messages.
+---@field markupMessageSupport? boolean
+
+---@class lsp._anonym12.textDocument
+---
+---Capabilities specific to the diagnostic pull model.
+---
+---@since 3.18.0
+---@field diagnostic? lsp._anonym13.textDocument.diagnostic
+
+---@class lsp._anonym14.workspace
---
---The server supports workspace folder.
---
@@ -4868,7 +4893,7 @@ error('Cannot require a meta file')
---@since 3.16.0
---@field fileOperations? lsp.FileOperationOptions
----@class lsp._anonym13.completionItem
+---@class lsp._anonym15.completionItem
---
---The server has support for completion item label
---details (see also `CompletionItemLabelDetails`) when
@@ -4877,11 +4902,11 @@ error('Cannot require a meta file')
---@since 3.17.0
---@field labelDetailsSupport? boolean
----@class lsp._anonym15.notebookSelector.cells
+---@class lsp._anonym17.notebookSelector.cells
---
---@field language string
----@class lsp._anonym14.notebookSelector
+---@class lsp._anonym16.notebookSelector
---
---The notebook to be synced If a string
---value is provided it matches against the
@@ -4889,13 +4914,13 @@ error('Cannot require a meta file')
---@field notebook string|lsp.NotebookDocumentFilter
---
---The cells of the matching notebook to be synced.
----@field cells? lsp._anonym15.notebookSelector.cells[]
+---@field cells? lsp._anonym17.notebookSelector.cells[]
----@class lsp._anonym17.notebookSelector.cells
+---@class lsp._anonym19.notebookSelector.cells
---
---@field language string
----@class lsp._anonym16.notebookSelector
+---@class lsp._anonym18.notebookSelector
---
---The notebook to be synced If a string
---value is provided it matches against the
@@ -4903,9 +4928,9 @@ error('Cannot require a meta file')
---@field notebook? string|lsp.NotebookDocumentFilter
---
---The cells of the matching notebook to be synced.
----@field cells lsp._anonym17.notebookSelector.cells[]
+---@field cells lsp._anonym19.notebookSelector.cells[]
----@class lsp._anonym18.staleRequestSupport
+---@class lsp._anonym20.staleRequestSupport
---
---The client will actively cancel the request.
---@field cancel boolean
@@ -4915,14 +4940,14 @@ error('Cannot require a meta file')
---response with error code `ContentModified`
---@field retryOnContentModified string[]
----@class lsp._anonym19.changeAnnotationSupport
+---@class lsp._anonym21.changeAnnotationSupport
---
---Whether the client groups edits with equal labels into tree nodes,
---for instance all edits labelled with "Changes in Strings" would
---be a tree node.
---@field groupsOnLabel? boolean
----@class lsp._anonym20.symbolKind
+---@class lsp._anonym22.symbolKind
---
---The symbol kind values the client supports. When this
---property exists the client also guarantees that it will
@@ -4934,32 +4959,32 @@ error('Cannot require a meta file')
---the initial version of the protocol.
---@field valueSet? lsp.SymbolKind[]
----@class lsp._anonym21.tagSupport
+---@class lsp._anonym23.tagSupport
---
---The tags supported by the client.
---@field valueSet lsp.SymbolTag[]
----@class lsp._anonym22.resolveSupport
+---@class lsp._anonym24.resolveSupport
---
---The properties that a client can resolve lazily. Usually
---`location.range`
---@field properties string[]
----@class lsp._anonym24.completionItem.tagSupport
+---@class lsp._anonym26.completionItem.tagSupport
---
---The tags supported by the client.
---@field valueSet lsp.CompletionItemTag[]
----@class lsp._anonym25.completionItem.resolveSupport
+---@class lsp._anonym27.completionItem.resolveSupport
---
---The properties that a client can resolve lazily.
---@field properties string[]
----@class lsp._anonym26.completionItem.insertTextModeSupport
+---@class lsp._anonym28.completionItem.insertTextModeSupport
---
---@field valueSet lsp.InsertTextMode[]
----@class lsp._anonym23.completionItem
+---@class lsp._anonym25.completionItem
---
---Client supports snippets as insert text.
---
@@ -4988,7 +5013,7 @@ error('Cannot require a meta file')
---a resolve call.
---
---@since 3.15.0
----@field tagSupport? lsp._anonym24.completionItem.tagSupport
+---@field tagSupport? lsp._anonym26.completionItem.tagSupport
---
---Client support insert replace edit to control different behavior if a
---completion item is inserted in the text or should replace text.
@@ -5001,14 +5026,14 @@ error('Cannot require a meta file')
---and `details` could be resolved lazily.
---
---@since 3.16.0
----@field resolveSupport? lsp._anonym25.completionItem.resolveSupport
+---@field resolveSupport? lsp._anonym27.completionItem.resolveSupport
---
---The client supports the `insertTextMode` property on
---a completion item to override the whitespace handling mode
---as defined by the client (see `insertTextMode`).
---
---@since 3.16.0
----@field insertTextModeSupport? lsp._anonym26.completionItem.insertTextModeSupport
+---@field insertTextModeSupport? lsp._anonym28.completionItem.insertTextModeSupport
---
---The client has support for completion item label
---details (see also `CompletionItemLabelDetails`).
@@ -5016,7 +5041,7 @@ error('Cannot require a meta file')
---@since 3.17.0
---@field labelDetailsSupport? boolean
----@class lsp._anonym27.completionItemKind
+---@class lsp._anonym29.completionItemKind
---
---The completion item kind values the client supports. When this
---property exists the client also guarantees that it will
@@ -5028,7 +5053,7 @@ error('Cannot require a meta file')
---the initial version of the protocol.
---@field valueSet? lsp.CompletionItemKind[]
----@class lsp._anonym28.completionList
+---@class lsp._anonym30.completionList
---
---The client supports the following itemDefaults on
---a completion list.
@@ -5040,7 +5065,7 @@ error('Cannot require a meta file')
---@since 3.17.0
---@field itemDefaults? string[]
----@class lsp._anonym30.signatureInformation.parameterInformation
+---@class lsp._anonym32.signatureInformation.parameterInformation
---
---The client supports processing label offsets instead of a
---simple label string.
@@ -5048,14 +5073,14 @@ error('Cannot require a meta file')
---@since 3.14.0
---@field labelOffsetSupport? boolean
----@class lsp._anonym29.signatureInformation
+---@class lsp._anonym31.signatureInformation
---
---Client supports the following content formats for the documentation
---property. The order describes the preferred format of the client.
---@field documentationFormat? lsp.MarkupKind[]
---
---Client capabilities specific to parameter information.
----@field parameterInformation? lsp._anonym30.signatureInformation.parameterInformation
+---@field parameterInformation? lsp._anonym32.signatureInformation.parameterInformation
---
---The client supports the `activeParameter` property on `SignatureInformation`
---literal.
@@ -5070,7 +5095,7 @@ error('Cannot require a meta file')
---@since 3.18.0
---@field noActiveParameterSupport? boolean
----@class lsp._anonym31.symbolKind
+---@class lsp._anonym33.symbolKind
---
---The symbol kind values the client supports. When this
---property exists the client also guarantees that it will
@@ -5082,12 +5107,12 @@ error('Cannot require a meta file')
---the initial version of the protocol.
---@field valueSet? lsp.SymbolKind[]
----@class lsp._anonym32.tagSupport
+---@class lsp._anonym34.tagSupport
---
---The tags supported by the client.
---@field valueSet lsp.SymbolTag[]
----@class lsp._anonym34.codeActionLiteralSupport.codeActionKind
+---@class lsp._anonym36.codeActionLiteralSupport.codeActionKind
---
---The code action kind values the client supports. When this
---property exists the client also guarantees that it will
@@ -5095,18 +5120,18 @@ error('Cannot require a meta file')
---to a default value when unknown.
---@field valueSet lsp.CodeActionKind[]
----@class lsp._anonym33.codeActionLiteralSupport
+---@class lsp._anonym35.codeActionLiteralSupport
---
---The code action kind is support with the following value
---set.
----@field codeActionKind lsp._anonym34.codeActionLiteralSupport.codeActionKind
+---@field codeActionKind lsp._anonym36.codeActionLiteralSupport.codeActionKind
----@class lsp._anonym35.resolveSupport
+---@class lsp._anonym37.resolveSupport
---
---The properties that a client can resolve lazily.
---@field properties string[]
----@class lsp._anonym36.foldingRangeKind
+---@class lsp._anonym38.foldingRangeKind
---
---The folding range kind values the client supports. When this
---property exists the client also guarantees that it will
@@ -5114,7 +5139,7 @@ error('Cannot require a meta file')
---to a default value when unknown.
---@field valueSet? lsp.FoldingRangeKind[]
----@class lsp._anonym37.foldingRange
+---@class lsp._anonym39.foldingRange
---
---If set, the client signals that it supports setting collapsedText on
---folding ranges to display custom labels instead of the default text.
@@ -5122,52 +5147,52 @@ error('Cannot require a meta file')
---@since 3.17.0
---@field collapsedText? boolean
----@class lsp._anonym38.tagSupport
+---@class lsp._anonym40.tagSupport
---
---The tags supported by the client.
---@field valueSet lsp.DiagnosticTag[]
----@class lsp._anonym40.requests.range
+---@class lsp._anonym42.requests.range
----@class lsp._anonym41.requests.full
+---@class lsp._anonym43.requests.full
---
---The client will send the `textDocument/semanticTokens/full/delta` request if
---the server provides a corresponding handler.
---@field delta? boolean
----@class lsp._anonym39.requests
+---@class lsp._anonym41.requests
---
---The client will send the `textDocument/semanticTokens/range` request if
---the server provides a corresponding handler.
----@field range? boolean|lsp._anonym40.requests.range
+---@field range? boolean|lsp._anonym42.requests.range
---
---The client will send the `textDocument/semanticTokens/full` request if
---the server provides a corresponding handler.
----@field full? boolean|lsp._anonym41.requests.full
+---@field full? boolean|lsp._anonym43.requests.full
----@class lsp._anonym42.resolveSupport
+---@class lsp._anonym44.resolveSupport
---
---The properties that a client can resolve lazily.
---@field properties string[]
----@class lsp._anonym43.messageActionItem
+---@class lsp._anonym45.messageActionItem
---
---Whether the client supports additional attributes which
---are preserved and send back to the server in the
---request's response.
---@field additionalPropertiesSupport? boolean
----@class lsp._anonym44.PrepareRenameResult
+---@class lsp._anonym46.PrepareRenameResult
---
---@field range lsp.Range
---
---@field placeholder string
----@class lsp._anonym45.PrepareRenameResult
+---@class lsp._anonym47.PrepareRenameResult
---
---@field defaultBehavior boolean
----@class lsp._anonym46.TextDocumentContentChangeEvent
+---@class lsp._anonym48.TextDocumentContentChangeEvent
---
---The range of the document that changed.
---@field range lsp.Range
@@ -5180,18 +5205,18 @@ error('Cannot require a meta file')
---The new text for the provided range.
---@field text string
----@class lsp._anonym47.TextDocumentContentChangeEvent
+---@class lsp._anonym49.TextDocumentContentChangeEvent
---
---The new text of the whole document.
---@field text string
----@class lsp._anonym48.MarkedString
+---@class lsp._anonym50.MarkedString
---
---@field language string
---
---@field value string
----@class lsp._anonym49.TextDocumentFilter
+---@class lsp._anonym51.TextDocumentFilter
---
---A language id, like `typescript`.
---@field language string
@@ -5202,7 +5227,7 @@ error('Cannot require a meta file')
---A glob pattern, like **/*.{ts,js}. See TextDocumentFilter for examples.
---@field pattern? string
----@class lsp._anonym50.TextDocumentFilter
+---@class lsp._anonym52.TextDocumentFilter
---
---A language id, like `typescript`.
---@field language? string
@@ -5213,7 +5238,7 @@ error('Cannot require a meta file')
---A glob pattern, like **/*.{ts,js}. See TextDocumentFilter for examples.
---@field pattern? string
----@class lsp._anonym51.TextDocumentFilter
+---@class lsp._anonym53.TextDocumentFilter
---
---A language id, like `typescript`.
---@field language? string
@@ -5224,7 +5249,7 @@ error('Cannot require a meta file')
---A glob pattern, like **/*.{ts,js}. See TextDocumentFilter for examples.
---@field pattern string
----@class lsp._anonym52.NotebookDocumentFilter
+---@class lsp._anonym54.NotebookDocumentFilter
---
---The type of the enclosing notebook.
---@field notebookType string
@@ -5235,7 +5260,7 @@ error('Cannot require a meta file')
---A glob pattern.
---@field pattern? string
----@class lsp._anonym53.NotebookDocumentFilter
+---@class lsp._anonym55.NotebookDocumentFilter
---
---The type of the enclosing notebook.
---@field notebookType? string
@@ -5246,7 +5271,7 @@ error('Cannot require a meta file')
---A glob pattern.
---@field pattern? string
----@class lsp._anonym54.NotebookDocumentFilter
+---@class lsp._anonym56.NotebookDocumentFilter
---
---The type of the enclosing notebook.
---@field notebookType? string
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 50121f30b2..49833eaeec 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -35,13 +35,13 @@ function M.hover()
request(ms.textDocument_hover, params)
end
-local function request_with_options(name, params, options)
+local function request_with_opts(name, params, opts)
local req_handler --- @type function?
- if options then
+ if opts then
req_handler = function(err, result, ctx, config)
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
local handler = client.handlers[name] or vim.lsp.handlers[name]
- handler(err, result, ctx, vim.tbl_extend('force', config or {}, options))
+ handler(err, result, ctx, vim.tbl_extend('force', config or {}, opts))
end
end
request(name, params, req_handler)
@@ -62,14 +62,13 @@ end
--- vim.lsp.buf.references(nil, { on_list = on_list })
--- ```
---
---- If you prefer loclist do something like this:
+--- If you prefer loclist instead of qflist:
--- ```lua
---- local function on_list(options)
---- vim.fn.setloclist(0, {}, ' ', options)
---- vim.cmd.lopen()
---- end
+--- vim.lsp.buf.definition({ loclist = true })
+--- vim.lsp.buf.references(nil, { loclist = true })
--- ```
--- @field on_list? fun(t: vim.lsp.LocationOpts.OnList)
+--- @field loclist? boolean
--- @class vim.lsp.LocationOpts.OnList
--- @field items table[] Structured like |setqflist-what|
@@ -83,32 +82,32 @@ end
--- Jumps to the declaration of the symbol under the cursor.
--- @note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead.
---- @param options? vim.lsp.LocationOpts
-function M.declaration(options)
+--- @param opts? vim.lsp.LocationOpts
+function M.declaration(opts)
local params = util.make_position_params()
- request_with_options(ms.textDocument_declaration, params, options)
+ request_with_opts(ms.textDocument_declaration, params, opts)
end
--- Jumps to the definition of the symbol under the cursor.
---- @param options? vim.lsp.LocationOpts
-function M.definition(options)
+--- @param opts? vim.lsp.LocationOpts
+function M.definition(opts)
local params = util.make_position_params()
- request_with_options(ms.textDocument_definition, params, options)
+ request_with_opts(ms.textDocument_definition, params, opts)
end
--- Jumps to the definition of the type of the symbol under the cursor.
---- @param options? vim.lsp.LocationOpts
-function M.type_definition(options)
+--- @param opts? vim.lsp.LocationOpts
+function M.type_definition(opts)
local params = util.make_position_params()
- request_with_options(ms.textDocument_typeDefinition, params, options)
+ request_with_opts(ms.textDocument_typeDefinition, params, opts)
end
--- Lists all the implementations for the symbol under the cursor in the
--- quickfix window.
---- @param options? vim.lsp.LocationOpts
-function M.implementation(options)
+--- @param opts? vim.lsp.LocationOpts
+function M.implementation(opts)
local params = util.make_position_params()
- request_with_options(ms.textDocument_implementation, params, options)
+ request_with_opts(ms.textDocument_implementation, params, opts)
end
--- Displays signature information about the symbol under the cursor in a
@@ -213,25 +212,25 @@ end
--- Formats a buffer using the attached (and optionally filtered) language
--- server clients.
---
---- @param options? vim.lsp.buf.format.Opts
-function M.format(options)
- options = options or {}
- local bufnr = options.bufnr or api.nvim_get_current_buf()
+--- @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 mode = api.nvim_get_mode().mode
- local range = options.range
+ local range = opts.range
if not range and mode == 'v' or mode == 'V' then
range = range_from_selection(bufnr, mode)
end
local method = range and ms.textDocument_rangeFormatting or ms.textDocument_formatting
local clients = vim.lsp.get_clients({
- id = options.id,
+ id = opts.id,
bufnr = bufnr,
- name = options.name,
+ name = opts.name,
method = method,
})
- if options.filter then
- clients = vim.tbl_filter(options.filter, clients)
+ if opts.filter then
+ clients = vim.tbl_filter(opts.filter, clients)
end
if #clients == 0 then
@@ -250,12 +249,12 @@ function M.format(options)
return params
end
- if options.async then
+ if opts.async then
local function do_format(idx, client)
if not client then
return
end
- local params = set_range(client, util.make_formatting_params(options.formatting_options))
+ local params = set_range(client, util.make_formatting_params(opts.formatting_options))
client.request(method, params, function(...)
local handler = client.handlers[method] or vim.lsp.handlers[method]
handler(...)
@@ -264,9 +263,9 @@ function M.format(options)
end
do_format(next(clients))
else
- local timeout_ms = options.timeout_ms or 1000
+ local timeout_ms = opts.timeout_ms or 1000
for _, client in pairs(clients) do
- local params = set_range(client, util.make_formatting_params(options.formatting_options))
+ local params = set_range(client, util.make_formatting_params(opts.formatting_options))
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)
@@ -295,18 +294,18 @@ end
---
---@param new_name string|nil If not provided, the user will be prompted for a new
--- name using |vim.ui.input()|.
----@param options? vim.lsp.buf.rename.Opts Additional options:
-function M.rename(new_name, options)
- options = options or {}
- local bufnr = options.bufnr or api.nvim_get_current_buf()
+---@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 clients = vim.lsp.get_clients({
bufnr = bufnr,
- name = options.name,
+ name = opts.name,
-- Clients must at least support rename, prepareRename is optional
method = ms.textDocument_rename,
})
- if options.filter then
- clients = vim.tbl_filter(options.filter, clients)
+ if opts.filter then
+ clients = vim.tbl_filter(opts.filter, clients)
end
if #clients == 0 then
@@ -415,21 +414,21 @@ end
---
---@param context (table|nil) Context for the request
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
----@param options? vim.lsp.ListOpts
-function M.references(context, options)
+---@param opts? vim.lsp.ListOpts
+function M.references(context, opts)
validate({ context = { context, 't', true } })
local params = util.make_position_params()
params.context = context or {
includeDeclaration = true,
}
- request_with_options(ms.textDocument_references, params, options)
+ request_with_opts(ms.textDocument_references, params, opts)
end
--- Lists all symbols in the current buffer in the quickfix window.
---- @param options? vim.lsp.ListOpts
-function M.document_symbol(options)
+--- @param opts? vim.lsp.ListOpts
+function M.document_symbol(opts)
local params = { textDocument = util.make_text_document_params() }
- request_with_options(ms.textDocument_documentSymbol, params, options)
+ request_with_opts(ms.textDocument_documentSymbol, params, opts)
end
--- @param call_hierarchy_items lsp.CallHierarchyItem[]?
@@ -461,7 +460,14 @@ local function call_hierarchy(method)
vim.notify(err.message, vim.log.levels.WARN)
return
end
+ if not result then
+ vim.notify('No item resolved', vim.log.levels.WARN)
+ return
+ end
local call_hierarchy_item = pick_call_hierarchy_item(result)
+ if not call_hierarchy_item then
+ return
+ end
local client = vim.lsp.get_client_by_id(ctx.client_id)
if client then
client.request(method, { item = call_hierarchy_item }, nil, ctx.bufnr)
@@ -488,6 +494,80 @@ function M.outgoing_calls()
call_hierarchy(ms.callHierarchy_outgoingCalls)
end
+--- Lists all the subtypes or supertypes of the symbol under the
+--- cursor in the |quickfix| window. If the symbol can resolve to
+--- multiple items, the user can pick one using |vim.ui.select()|.
+---@param kind "subtypes"|"supertypes"
+function M.typehierarchy(kind)
+ local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes
+
+ --- Merge results from multiple clients into a single table. Client-ID is preserved.
+ ---
+ --- @param results table<integer, {error: lsp.ResponseError?, result: lsp.TypeHierarchyItem[]?}>
+ --- @return [integer, lsp.TypeHierarchyItem][]
+ local function merge_results(results)
+ local merged_results = {}
+ for client_id, client_result in pairs(results) do
+ if client_result.error then
+ vim.notify(client_result.error.message, vim.log.levels.WARN)
+ elseif client_result.result then
+ for _, item in pairs(client_result.result) do
+ table.insert(merged_results, { client_id, item })
+ end
+ end
+ end
+ return merged_results
+ end
+
+ local bufnr = api.nvim_get_current_buf()
+ local params = util.make_position_params()
+ --- @param results table<integer, {error: lsp.ResponseError?, result: lsp.TypeHierarchyItem[]?}>
+ vim.lsp.buf_request_all(bufnr, ms.textDocument_prepareTypeHierarchy, params, function(results)
+ local merged_results = merge_results(results)
+ if #merged_results == 0 then
+ vim.notify('No items resolved', vim.log.levels.INFO)
+ return
+ end
+
+ if #merged_results == 1 then
+ local item = merged_results[1]
+ local client = vim.lsp.get_client_by_id(item[1])
+ if client then
+ client.request(method, { item = item[2] }, nil, bufnr)
+ else
+ vim.notify(
+ string.format('Client with id=%d disappeared during call hierarchy request', item[1]),
+ vim.log.levels.WARN
+ )
+ end
+ else
+ local select_opts = {
+ prompt = 'Select a type hierarchy item:',
+ kind = 'typehierarchy',
+ format_item = function(item)
+ if not item[2].detail or #item[2].detail == 0 then
+ return item[2].name
+ end
+ return string.format('%s %s', item[2].name, item[2].detail)
+ end,
+ }
+
+ vim.ui.select(merged_results, select_opts, function(item)
+ local client = vim.lsp.get_client_by_id(item[1])
+ if client then
+ --- @type lsp.TypeHierarchyItem
+ client.request(method, { item = item[2] }, nil, bufnr)
+ else
+ vim.notify(
+ string.format('Client with id=%d disappeared during call hierarchy request', item[1]),
+ vim.log.levels.WARN
+ )
+ end
+ end)
+ end
+ end)
+end
+
--- List workspace folders.
---
function M.list_workspace_folders()
@@ -514,28 +594,9 @@ function M.add_workspace_folder(workspace_folder)
print(workspace_folder, ' is not a valid directory')
return
end
- local new_workspace = {
- uri = vim.uri_from_fname(workspace_folder),
- name = workspace_folder,
- }
- local params = { event = { added = { new_workspace }, removed = {} } }
- local bufnr = vim.api.nvim_get_current_buf()
+ local bufnr = api.nvim_get_current_buf()
for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do
- local found = false
- for _, folder in pairs(client.workspace_folders or {}) do
- if folder.name == workspace_folder then
- found = true
- print(workspace_folder, 'is already part of this workspace')
- break
- end
- end
- if not found then
- client.notify(ms.workspace_didChangeWorkspaceFolders, params)
- if not client.workspace_folders then
- client.workspace_folders = {}
- end
- table.insert(client.workspace_folders, new_workspace)
- end
+ client:_add_workspace_folder(workspace_folder)
end
end
@@ -547,23 +608,12 @@ function M.remove_workspace_folder(workspace_folder)
workspace_folder = workspace_folder
or npcall(vim.fn.input, 'Workspace Folder: ', vim.fn.expand('%:p:h'))
api.nvim_command('redraw')
- if not (workspace_folder and #workspace_folder > 0) then
+ if not workspace_folder or #workspace_folder == 0 then
return
end
- local workspace = {
- uri = vim.uri_from_fname(workspace_folder),
- name = workspace_folder,
- }
- local params = { event = { added = {}, removed = { workspace } } }
- local bufnr = vim.api.nvim_get_current_buf()
+ local bufnr = api.nvim_get_current_buf()
for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do
- for idx, folder in pairs(client.workspace_folders) do
- if folder.name == workspace_folder then
- client.notify(ms.workspace_didChangeWorkspaceFolders, params)
- client.workspace_folders[idx] = nil
- return
- end
- end
+ client:_remove_workspace_folder(workspace_folder)
end
print(workspace_folder, 'is not currently part of the workspace')
end
@@ -575,14 +625,14 @@ end
--- string means no filtering is done.
---
--- @param query string? optional
---- @param options? vim.lsp.ListOpts
-function M.workspace_symbol(query, options)
+--- @param opts? vim.lsp.ListOpts
+function M.workspace_symbol(query, opts)
query = query or npcall(vim.fn.input, 'Query: ')
if query == nil then
return
end
local params = { query = query }
- request_with_options(ms.workspace_symbol, params, options)
+ request_with_opts(ms.workspace_symbol, params, opts)
end
--- Send request to the server to resolve document highlights for the current
@@ -774,19 +824,19 @@ end
--- Selects a code action available at the current
--- cursor position.
---
----@param options? vim.lsp.buf.code_action.Opts
+---@param opts? vim.lsp.buf.code_action.Opts
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
---@see vim.lsp.protocol.CodeActionTriggerKind
-function M.code_action(options)
- validate({ options = { options, 't', true } })
- options = options or {}
+function M.code_action(opts)
+ validate({ options = { opts, 't', true } })
+ opts = opts or {}
-- Detect old API call code_action(context) which should now be
-- code_action({ context = context} )
--- @diagnostic disable-next-line:undefined-field
- if options.diagnostics or options.only then
- options = { options = options }
+ if opts.diagnostics or opts.only then
+ opts = { options = opts }
end
- local context = options.context or {}
+ local context = opts.context or {}
if not context.triggerKind then
context.triggerKind = vim.lsp.protocol.CodeActionTriggerKind.Invoked
end
@@ -816,17 +866,17 @@ function M.code_action(options)
results[ctx.client_id] = { error = err, result = result, ctx = ctx }
remaining = remaining - 1
if remaining == 0 then
- on_code_action_results(results, options)
+ on_code_action_results(results, opts)
end
end
for _, client in ipairs(clients) do
---@type lsp.CodeActionParams
local params
- if options.range then
- assert(type(options.range) == 'table', 'code_action range must be a table')
- local start = assert(options.range.start, 'range must have a `start` property')
- local end_ = assert(options.range['end'], 'range must have a `end` property')
+ 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')
+ local end_ = assert(opts.range['end'], 'range must have a `end` property')
params = util.make_given_range_params(start, end_, bufnr, client.offset_encoding)
elseif mode == 'v' or mode == 'V' then
local range = range_from_selection(bufnr, mode)
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index ff0db166d5..4beb7fefda 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -15,7 +15,7 @@ local validate = vim.validate
--- @inlinedoc
---
--- Allow using incremental sync for buffer edits
---- (defailt: `true`)
+--- (default: `true`)
--- @field allow_incremental_sync? boolean
---
--- Debounce `didChange` notifications to the server by the given number in milliseconds.
@@ -37,7 +37,7 @@ local validate = vim.validate
--- `is_closing` and `terminate`.
--- See |vim.lsp.rpc.request()|, |vim.lsp.rpc.notify()|.
--- For TCP there is a builtin RPC client factory: |vim.lsp.rpc.connect()|
---- @field cmd string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient?
+--- @field cmd string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient
---
--- Directory to launch the `cmd` process. Not related to `root_dir`.
--- (default: cwd)
@@ -185,6 +185,10 @@ local validate = vim.validate
--- @field root_dir string
---
--- @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
@@ -225,7 +229,7 @@ local validate = vim.validate
--- 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?
+--- @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}
@@ -416,7 +420,7 @@ local function get_workspace_folders(workspace_folders, root_dir)
return {
{
uri = vim.uri_from_fname(root_dir),
- name = string.format('%s', root_dir),
+ name = root_dir,
},
}
end
@@ -502,25 +506,17 @@ function Client.create(config)
}
-- Start the RPC client.
- local rpc --- @type vim.lsp.rpc.PublicClient?
local config_cmd = config.cmd
if type(config_cmd) == 'function' then
- rpc = config_cmd(dispatchers)
+ self.rpc = config_cmd(dispatchers)
else
- rpc = lsp.rpc.start(config_cmd, dispatchers, {
+ self.rpc = lsp.rpc.start(config_cmd, dispatchers, {
cwd = config.cmd_cwd,
env = config.cmd_env,
detached = config.detached,
})
end
- -- Return nil if the rpc client fails to start
- if not rpc then
- return
- end
-
- self.rpc = rpc
-
setmetatable(self, Client)
return self
@@ -575,6 +571,7 @@ function Client:initialize()
initializationOptions = config.init_options,
capabilities = self.capabilities,
trace = self._trace,
+ workDoneToken = '1',
}
self:_run_callbacks(
@@ -608,8 +605,19 @@ function Client:initialize()
self:_notify(ms.workspace_didChangeConfiguration, { settings = self.settings })
end
+ -- If server is being restarted, make sure to re-attach to any previously attached buffers.
+ -- Save which buffers before on_init in case new buffers are attached.
+ local reattach_bufs = vim.deepcopy(self.attached_buffers)
+
self:_run_callbacks(self._on_init_cbs, lsp.client_errors.ON_INIT_CALLBACK_ERROR, self, result)
+ 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)
+ end
+ end
+
log.info(
self._log_prefix,
'server_capabilities',
@@ -647,10 +655,10 @@ end
--- checks for capabilities and handler availability.
---
--- @param method string LSP method name.
---- @param params table|nil LSP request params.
---- @param handler lsp.Handler|nil Response |lsp-handler| for this method.
---- @param bufnr integer Buffer handle (0 for current).
---- @return boolean status, integer|nil request_id {status} is a bool indicating
+--- @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
@@ -693,7 +701,7 @@ function Client:_request(method, params, handler, bufnr)
local request = { type = 'pending', bufnr = bufnr, method = method }
self.requests[request_id] = request
api.nvim_exec_autocmds('LspRequest', {
- buffer = bufnr,
+ buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
modeline = false,
data = { client_id = self.id, request_id = request_id, request = request },
})
@@ -709,7 +717,7 @@ 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.tbl_flatten({ ... }))
+ local message = table.concat(vim.iter({ ... }):flatten():totable())
if vim.in_fast_event() then
vim.schedule(function()
api.nvim_err_writeln(message)
@@ -761,7 +769,7 @@ function Client:_request_sync(method, params, timeout_ms, bufnr)
return request_result
end
---- @private
+--- @package
--- Sends a notification to an LSP server.
---
--- @param method string LSP method name.
@@ -804,7 +812,7 @@ function Client:_cancel_request(id)
if request and request.type == 'pending' then
request.type = 'cancel'
api.nvim_exec_autocmds('LspRequest', {
- buffer = request.bufnr,
+ 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 },
})
@@ -900,7 +908,7 @@ 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 vim.tbl_get(self.server_capabilities, 'textDocumentSync', 'openClose') then
+ if not self.supports_method(ms.textDocument_didOpen) then
return
end
if not api.nvim_buf_is_loaded(bufnr) then
@@ -1053,4 +1061,45 @@ function Client:_on_exit(code, signal)
)
end
+--- @package
+--- Add a directory to the workspace folders.
+--- @param dir string?
+function Client:_add_workspace_folder(dir)
+ for _, folder in pairs(self.workspace_folders or {}) do
+ if folder.name == dir then
+ print(dir, 'is already part of this workspace')
+ return
+ end
+ end
+
+ local wf = assert(get_workspace_folders(nil, dir))
+
+ self:_notify(ms.workspace_didChangeWorkspaceFolders, {
+ event = { added = wf, removed = {} },
+ })
+
+ if not self.workspace_folders then
+ self.workspace_folders = {}
+ end
+ vim.list_extend(self.workspace_folders, wf)
+end
+
+--- @package
+--- Remove a directory to the workspace folders.
+--- @param dir string?
+function Client:_remove_workspace_folder(dir)
+ local wf = assert(get_workspace_folders(nil, dir))
+
+ self:_notify(ms.workspace_didChangeWorkspaceFolders, {
+ event = { added = {}, removed = wf },
+ })
+
+ for idx, folder in pairs(self.workspace_folders) do
+ if folder.name == dir then
+ table.remove(self.workspace_folders, idx)
+ break
+ end
+ end
+end
+
return Client
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index 48c096c0c1..c85bb6aa32 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -79,7 +79,7 @@ function M.run()
local lenses_by_client = lens_cache_by_buf[bufnr] or {}
for client, lenses in pairs(lenses_by_client) do
for _, lens in pairs(lenses) do
- if lens.range.start.line == (line - 1) then
+ if lens.range.start.line == (line - 1) and lens.command and lens.command.command ~= '' then
table.insert(options, { client = client, lens = lens })
end
end
@@ -164,7 +164,7 @@ function M.display(lenses, bufnr, client_id)
return a.range.start.character < b.range.start.character
end)
for j, lens in ipairs(line_lenses) do
- local text = lens.command and lens.command.title or 'Unresolved lens ...'
+ local text = (lens.command and lens.command.title or 'Unresolved lens ...'):gsub('%s+', ' ')
table.insert(chunks, { text, 'LspCodeLens' })
if j < num_line_lenses then
table.insert(chunks, { ' | ', 'LspCodeLensSeparator' })
@@ -231,7 +231,7 @@ local function resolve_lenses(lenses, bufnr, client_id, callback)
countdown()
else
assert(client)
- client.request('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
@@ -299,14 +299,18 @@ function M.refresh(opts)
local bufnr = opts.bufnr and resolve_bufnr(opts.bufnr)
local buffers = bufnr and { bufnr }
or vim.tbl_filter(api.nvim_buf_is_loaded, api.nvim_list_bufs())
- local params = {
- textDocument = util.make_text_document_params(),
- }
for _, buf in ipairs(buffers) do
if not active_refreshes[buf] then
+ local params = {
+ textDocument = util.make_text_document_params(buf),
+ }
active_refreshes[buf] = true
- vim.lsp.buf_request(buf, ms.textDocument_codeLens, params, M.on_codelens)
+
+ local request_ids = vim.lsp.buf_request(buf, ms.textDocument_codeLens, params, M.on_codelens)
+ if vim.tbl_isempty(request_ids) then
+ active_refreshes[buf] = nil
+ end
end
end
end
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index daf4fec8d2..f9d394642c 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -3,6 +3,7 @@ local protocol = require('vim.lsp.protocol')
local ms = protocol.Methods
local util = require('vim.lsp.util')
local api = vim.api
+local completion = require('vim.lsp._completion')
--- @type table<string,lsp.Handler>
local M = {}
@@ -12,7 +13,7 @@ local M = {}
--- Writes to error buffer.
---@param ... string Will be concatenated before being written
local function err_message(...)
- vim.notify(table.concat(vim.tbl_flatten({ ... })), vim.log.levels.ERROR)
+ vim.notify(table.concat(vim.iter({ ... }):flatten():totable()), vim.log.levels.ERROR)
api.nvim_command('redraw')
end
@@ -22,16 +23,16 @@ M[ms.workspace_executeCommand] = function(_, _, _, _)
end
--- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
----@param result lsp.ProgressParams
+---@param params lsp.ProgressParams
---@param ctx lsp.HandlerContext
-M[ms.dollar_progress] = function(_, result, ctx)
+M[ms.dollar_progress] = function(_, params, ctx)
local client = vim.lsp.get_client_by_id(ctx.client_id)
if not client then
err_message('LSP[id=', tostring(ctx.client_id), '] client has shut down during progress update')
return vim.NIL
end
local kind = nil
- local value = result.value
+ local value = params.value
if type(value) == 'table' then
kind = value.kind
@@ -39,21 +40,21 @@ M[ms.dollar_progress] = function(_, result, ctx)
-- So that consumers always have it available, even if they consume a
-- subset of the full sequence
if kind == 'begin' then
- client.progress.pending[result.token] = value.title
+ client.progress.pending[params.token] = value.title
else
- value.title = client.progress.pending[result.token]
+ value.title = client.progress.pending[params.token]
if kind == 'end' then
- client.progress.pending[result.token] = nil
+ client.progress.pending[params.token] = nil
end
end
end
- client.progress:push(result)
+ client.progress:push(params)
api.nvim_exec_autocmds('LspProgress', {
pattern = kind,
modeline = false,
- data = { client_id = ctx.client_id, result = result },
+ data = { client_id = ctx.client_id, params = params },
})
end
@@ -253,26 +254,24 @@ M[ms.textDocument_references] = function(_, result, ctx, config)
local title = 'References'
local items = util.locations_to_items(result, client.offset_encoding)
+ local list = { title = title, items = items, context = ctx }
if config.loclist then
- vim.fn.setloclist(0, {}, ' ', { title = title, items = items, context = ctx })
- api.nvim_command('lopen')
+ vim.fn.setloclist(0, {}, ' ', list)
+ vim.cmd.lopen()
elseif config.on_list then
- assert(type(config.on_list) == 'function', 'on_list is not a function')
- config.on_list({ title = title, items = items, context = ctx })
+ assert(vim.is_callable(config.on_list), 'on_list is not a function')
+ config.on_list(list)
else
- vim.fn.setqflist({}, ' ', { title = title, items = items, context = ctx })
- api.nvim_command('botright copen')
+ vim.fn.setqflist({}, ' ', list)
+ vim.cmd('botright copen')
end
end
--- Return a function that converts LSP responses to list items and opens the list
---
---- The returned function has an optional {config} parameter that accepts a table
---- with the following keys:
----
---- loclist: (boolean) use the location list (default is to use the quickfix list)
+--- The returned function has an optional {config} parameter that accepts |vim.lsp.ListOpts|
---
----@param map_result function `((resp, bufnr) -> list)` to convert the response
+---@param map_result fun(resp, bufnr: integer): table to convert the response
---@param entity string name of the resource used in a `not found` error message
---@param title_fn fun(ctx: lsp.HandlerContext): string Function to call to generate list title
---@return lsp.Handler
@@ -286,15 +285,16 @@ local function response_to_list(map_result, entity, title_fn)
local title = title_fn(ctx)
local items = map_result(result, ctx.bufnr)
+ local list = { title = title, items = items, context = ctx }
if config.loclist then
- vim.fn.setloclist(0, {}, ' ', { title = title, items = items, context = ctx })
- api.nvim_command('lopen')
+ vim.fn.setloclist(0, {}, ' ', list)
+ vim.cmd.lopen()
elseif config.on_list then
- assert(type(config.on_list) == 'function', 'on_list is not a function')
- config.on_list({ title = title, items = items, context = ctx })
+ assert(vim.is_callable(config.on_list), 'on_list is not a function')
+ config.on_list(list)
else
- vim.fn.setqflist({}, ' ', { title = title, items = items, context = ctx })
- api.nvim_command('botright copen')
+ vim.fn.setqflist({}, ' ', list)
+ vim.cmd('botright copen')
end
end
end
@@ -354,7 +354,7 @@ M[ms.textDocument_completion] = function(_, result, _, _)
local textMatch = vim.fn.match(line_to_cursor, '\\k*$')
local prefix = line_to_cursor:sub(textMatch + 1)
- local matches = util.text_document_completion_list_to_complete_items(result, prefix)
+ local matches = completion._lsp_to_complete_items(result, prefix)
vim.fn.complete(textMatch + 1, matches)
end
@@ -428,7 +428,7 @@ local function location_handler(_, result, ctx, config)
-- textDocument/definition can return Location or Location[]
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
- if not vim.tbl_islist(result) then
+ if not vim.islist(result) then
result = { result }
end
@@ -436,7 +436,7 @@ local function location_handler(_, result, ctx, config)
local items = util.locations_to_items(result, client.offset_encoding)
if config.on_list then
- assert(type(config.on_list) == 'function', 'on_list is not a function')
+ assert(vim.is_callable(config.on_list), 'on_list is not a function')
config.on_list({ title = title, items = items })
return
end
@@ -444,8 +444,13 @@ local function location_handler(_, result, ctx, config)
util.jump_to_location(result[1], client.offset_encoding, config.reuse_win)
return
end
- vim.fn.setqflist({}, ' ', { title = title, items = items })
- api.nvim_command('botright copen')
+ if config.loclist then
+ vim.fn.setloclist(0, {}, ' ', { title = title, items = items })
+ vim.cmd.lopen()
+ else
+ vim.fn.setqflist({}, ' ', { title = title, items = items })
+ vim.cmd('botright copen')
+ end
end
--- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration
@@ -555,7 +560,7 @@ local function make_call_hierarchy_handler(direction)
end
end
vim.fn.setqflist({}, ' ', { title = 'LSP call hierarchy', items = items })
- api.nvim_command('botright copen')
+ vim.cmd('botright copen')
end
end
@@ -565,6 +570,45 @@ M[ms.callHierarchy_incomingCalls] = make_call_hierarchy_handler('from')
--- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_outgoingCalls
M[ms.callHierarchy_outgoingCalls] = make_call_hierarchy_handler('to')
+--- Displays type hierarchy in the quickfix window.
+local function make_type_hierarchy_handler()
+ --- @param result lsp.TypeHierarchyItem[]
+ return function(_, result, ctx, _)
+ if not result then
+ return
+ end
+ local function format_item(item)
+ if not item.detail or #item.detail == 0 then
+ return item.name
+ end
+ return string.format('%s %s', item.name, item.detail)
+ end
+ local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
+ local items = {}
+ for _, type_hierarchy_item in pairs(result) do
+ local col = util._get_line_byte_from_position(
+ ctx.bufnr,
+ type_hierarchy_item.range.start,
+ client.offset_encoding
+ )
+ table.insert(items, {
+ filename = assert(vim.uri_to_fname(type_hierarchy_item.uri)),
+ text = format_item(type_hierarchy_item),
+ lnum = type_hierarchy_item.range.start.line + 1,
+ col = col + 1,
+ })
+ end
+ vim.fn.setqflist({}, ' ', { title = 'LSP type hierarchy', items = items })
+ vim.cmd('botright copen')
+ end
+end
+
+--- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#typeHierarchy_incomingCalls
+M[ms.typeHierarchy_subtypes] = make_type_hierarchy_handler()
+
+--- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#typeHierarchy_outgoingCalls
+M[ms.typeHierarchy_supertypes] = make_type_hierarchy_handler()
+
--- @see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage
--- @param result lsp.LogMessageParams
M[ms.window_logMessage] = function(_, result, ctx, _)
@@ -615,7 +659,8 @@ M[ms.window_showDocument] = function(_, result, ctx, _)
if result.external then
-- TODO(lvimuser): ask the user for confirmation
- local ret, err = vim.ui.open(uri)
+ local cmd, err = vim.ui.open(uri)
+ local ret = cmd and cmd:wait(2000) or nil
if ret == nil or ret.code ~= 0 then
return {
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
index 797a1097f9..a79ae76eb9 100644
--- a/runtime/lua/vim/lsp/health.lua
+++ b/runtime/lua/vim/lsp/health.lua
@@ -51,6 +51,29 @@ end
local function check_watcher()
vim.health.start('vim.lsp: File watcher')
+
+ -- Only run the check if file watching has been enabled by a client.
+ local clients = vim.lsp.get_clients()
+ if
+ --- @param client vim.lsp.Client
+ vim.iter(clients):all(function(client)
+ local has_capability = vim.tbl_get(
+ client.capabilities,
+ 'workspace',
+ 'didChangeWatchedFiles',
+ 'dynamicRegistration'
+ )
+ local has_dynamic_capability =
+ client.dynamic_capabilities:get(vim.lsp.protocol.Methods.workspace_didChangeWatchedFiles)
+ return has_capability == nil
+ or has_dynamic_capability == nil
+ or client.workspace_folders == nil
+ end)
+ then
+ report_info('file watching "(workspace/didChangeWatchedFiles)" disabled on all clients')
+ return
+ end
+
local watchfunc = vim.lsp._watchfiles._watchfunc
assert(watchfunc)
local watchfunc_name --- @type string
diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua
index ec676ea97f..f98496456b 100644
--- a/runtime/lua/vim/lsp/inlay_hint.lua
+++ b/runtime/lua/vim/lsp/inlay_hint.lua
@@ -4,13 +4,30 @@ local ms = require('vim.lsp.protocol').Methods
local api = vim.api
local M = {}
----@class (private) vim.lsp.inlay_hint.bufstate
+---@class (private) vim.lsp.inlay_hint.globalstate Global state for inlay hints
+---@field enabled boolean Whether inlay hints are enabled for this scope
+---@type vim.lsp.inlay_hint.globalstate
+local globalstate = {
+ enabled = false,
+}
+
+---@class (private) vim.lsp.inlay_hint.bufstate: vim.lsp.inlay_hint.globalstate Buffer local state for inlay hints
---@field version? integer
---@field client_hints? table<integer, table<integer, lsp.InlayHint[]>> client_id -> (lnum -> hints)
---@field applied table<integer, integer> Last version of hints applied to this line
----@field enabled boolean Whether inlay hints are enabled for this buffer
---@type table<integer, vim.lsp.inlay_hint.bufstate>
-local bufstates = {}
+local bufstates = vim.defaulttable(function(_)
+ return setmetatable({ applied = {} }, {
+ __index = globalstate,
+ __newindex = function(state, key, value)
+ if globalstate[key] == value then
+ rawset(state, key, nil)
+ else
+ rawset(state, key, value)
+ end
+ end,
+ })
+end)
local namespace = api.nvim_create_namespace('vim_lsp_inlayhint')
local augroup = api.nvim_create_augroup('vim_lsp_inlayhint', {})
@@ -34,22 +51,22 @@ function M.on_inlayhint(err, result, ctx, _)
return
end
local bufstate = bufstates[bufnr]
- if not bufstate or not bufstate.enabled then
+ if not bufstate.enabled then
return
end
if not (bufstate.client_hints and bufstate.version) then
bufstate.client_hints = vim.defaulttable()
bufstate.version = ctx.version
end
- local hints_by_client = bufstate.client_hints
+ local client_hints = bufstate.client_hints
local client = assert(vim.lsp.get_client_by_id(client_id))
- local new_hints_by_lnum = vim.defaulttable()
+ local new_lnum_hints = vim.defaulttable()
local num_unprocessed = #result
if num_unprocessed == 0 then
- hints_by_client[client_id] = {}
+ client_hints[client_id] = {}
bufstate.version = ctx.version
- api.nvim__buf_redraw_range(bufnr, 0, -1)
+ api.nvim__redraw({ buf = bufnr, valid = true })
return
end
@@ -73,15 +90,15 @@ function M.on_inlayhint(err, result, ctx, _)
for _, hint in ipairs(result) do
local lnum = hint.position.line
hint.position.character = pos_to_byte(hint.position)
- table.insert(new_hints_by_lnum[lnum], hint)
+ table.insert(new_lnum_hints[lnum], hint)
end
- hints_by_client[client_id] = new_hints_by_lnum
+ client_hints[client_id] = new_lnum_hints
bufstate.version = ctx.version
- api.nvim__buf_redraw_range(bufnr, 0, -1)
+ api.nvim__redraw({ buf = bufnr, valid = true })
end
---- |lsp-handler| for the method `textDocument/inlayHint/refresh`
+--- |lsp-handler| for the method `workspace/inlayHint/refresh`
---@param ctx lsp.HandlerContext
---@private
function M.on_refresh(err, _, ctx, _)
@@ -91,11 +108,7 @@ function M.on_refresh(err, _, ctx, _)
for _, bufnr in ipairs(vim.lsp.get_buffers_by_client_id(ctx.client_id)) do
for _, winid in ipairs(api.nvim_list_wins()) do
if api.nvim_win_get_buf(winid) == bufnr then
- local bufstate = bufstates[bufnr]
- if bufstate then
- util._refresh(ms.textDocument_inlayHint, { bufnr = bufnr })
- break
- end
+ util._refresh(ms.textDocument_inlayHint, { bufnr = bufnr })
end
end
end
@@ -123,7 +136,8 @@ 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)
---- resolved_hint = client.request_sync('inlayHint/resolve', hint.inlay_hint, 100, 0).result
+--- 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
@@ -154,7 +168,7 @@ function M.get(filter)
end
local bufstate = bufstates[bufnr]
- if not (bufstate and bufstate.client_hints) then
+ if not bufstate.client_hints then
return {}
end
@@ -175,19 +189,19 @@ function M.get(filter)
end
--- @type vim.lsp.inlay_hint.get.ret[]
- local hints = {}
+ local result = {}
for _, client in pairs(clients) do
- local hints_by_lnum = bufstate.client_hints[client.id]
- if hints_by_lnum then
+ local lnum_hints = bufstate.client_hints[client.id]
+ if lnum_hints then
for lnum = range.start.line, range['end'].line do
- local line_hints = hints_by_lnum[lnum] or {}
- for _, hint in pairs(line_hints) do
+ local hints = lnum_hints[lnum] or {}
+ for _, hint in pairs(hints) do
local line, char = hint.position.line, hint.position.character
if
(line > range.start.line or char >= range.start.character)
and (line < range['end'].line or char <= range['end'].character)
then
- table.insert(hints, {
+ table.insert(result, {
bufnr = bufnr,
client_id = client.id,
inlay_hint = hint,
@@ -197,18 +211,15 @@ function M.get(filter)
end
end
end
- return hints
+ return result
end
--- Clear inlay hints
---@param bufnr (integer) Buffer handle, or 0 for current
local function clear(bufnr)
- if bufnr == nil or bufnr == 0 then
+ if bufnr == 0 then
bufnr = api.nvim_get_current_buf()
end
- if not bufstates[bufnr] then
- return
- end
local bufstate = bufstates[bufnr]
local client_lens = (bufstate or {}).client_hints or {}
local client_ids = vim.tbl_keys(client_lens) --- @type integer[]
@@ -218,19 +229,18 @@ local function clear(bufnr)
end
end
api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
- api.nvim__buf_redraw_range(bufnr, 0, -1)
+ api.nvim__redraw({ buf = bufnr, valid = true })
end
--- Disable inlay hints for a buffer
----@param bufnr (integer|nil) Buffer handle, or 0 or nil for current
+---@param bufnr (integer) Buffer handle, or 0 for current
local function _disable(bufnr)
- if bufnr == nil or bufnr == 0 then
+ if bufnr == 0 then
bufnr = api.nvim_get_current_buf()
end
clear(bufnr)
- if bufstates[bufnr] then
- bufstates[bufnr] = { enabled = false, applied = {} }
- end
+ bufstates[bufnr] = nil
+ bufstates[bufnr].enabled = false
end
--- Refresh inlay hints, only if we have attached clients that support it
@@ -244,30 +254,38 @@ local function _refresh(bufnr, opts)
end
--- Enable inlay hints for a buffer
----@param bufnr (integer|nil) Buffer handle, or 0 or nil for current
+---@param bufnr (integer) Buffer handle, or 0 for current
local function _enable(bufnr)
- if bufnr == nil or bufnr == 0 then
+ if bufnr == 0 then
bufnr = api.nvim_get_current_buf()
end
- local bufstate = bufstates[bufnr]
- if not bufstate then
- bufstates[bufnr] = { applied = {}, enabled = true }
- api.nvim_create_autocmd('LspNotify', {
- buffer = bufnr,
- callback = function(opts)
- if
- opts.data.method ~= ms.textDocument_didChange
- and opts.data.method ~= ms.textDocument_didOpen
- then
- return
- end
- if bufstates[bufnr] and bufstates[bufnr].enabled then
- _refresh(bufnr, { client_id = opts.data.client_id })
- end
- end,
- group = augroup,
- })
- _refresh(bufnr)
+ bufstates[bufnr] = nil
+ bufstates[bufnr].enabled = true
+ _refresh(bufnr)
+end
+
+api.nvim_create_autocmd('LspNotify', {
+ callback = function(args)
+ ---@type integer
+ local bufnr = args.buf
+
+ if
+ args.data.method ~= ms.textDocument_didChange
+ and args.data.method ~= ms.textDocument_didOpen
+ then
+ return
+ end
+ if bufstates[bufnr].enabled then
+ _refresh(bufnr, { client_id = args.data.client_id })
+ end
+ end,
+ group = augroup,
+})
+api.nvim_create_autocmd('LspAttach', {
+ callback = function(args)
+ ---@type integer
+ local bufnr = args.buf
+
api.nvim_buf_attach(bufnr, false, {
on_reload = function(_, cb_bufnr)
clear(cb_bufnr)
@@ -278,32 +296,30 @@ local function _enable(bufnr)
end,
on_detach = function(_, cb_bufnr)
_disable(cb_bufnr)
+ bufstates[cb_bufnr] = nil
end,
})
- api.nvim_create_autocmd('LspDetach', {
- buffer = bufnr,
- callback = function(args)
- local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_inlayHint })
-
- if
- not vim.iter(clients):any(function(c)
- return c.id ~= args.data.client_id
- end)
- then
- _disable(bufnr)
- end
- end,
- group = augroup,
- })
- else
- bufstate.enabled = true
- _refresh(bufnr)
- end
-end
+ end,
+ group = augroup,
+})
+api.nvim_create_autocmd('LspDetach', {
+ callback = function(args)
+ ---@type integer
+ local bufnr = args.buf
+ local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_inlayHint })
+ if not vim.iter(clients):any(function(c)
+ return c.id ~= args.data.client_id
+ end) then
+ _disable(bufnr)
+ end
+ end,
+ group = augroup,
+})
api.nvim_set_decoration_provider(namespace, {
on_win = function(_, _, bufnr, topline, botline)
- local bufstate = bufstates[bufnr]
+ ---@type vim.lsp.inlay_hint.bufstate
+ local bufstate = rawget(bufstates, bufnr)
if not bufstate then
return
end
@@ -311,14 +327,18 @@ api.nvim_set_decoration_provider(namespace, {
if bufstate.version ~= util.buf_versions[bufnr] then
return
end
- local hints_by_client = assert(bufstate.client_hints)
+
+ if not bufstate.client_hints then
+ return
+ end
+ local client_hints = assert(bufstate.client_hints)
for lnum = topline, botline do
if bufstate.applied[lnum] ~= bufstate.version then
api.nvim_buf_clear_namespace(bufnr, namespace, lnum, lnum + 1)
- for _, hints_by_lnum in pairs(hints_by_client) do
- local line_hints = hints_by_lnum[lnum] or {}
- for _, hint in pairs(line_hints) do
+ for _, lnum_hints in pairs(client_hints) do
+ local hints = lnum_hints[lnum] or {}
+ for _, hint in pairs(hints) do
local text = ''
local label = hint.label
if type(label) == 'string' then
@@ -349,34 +369,65 @@ api.nvim_set_decoration_provider(namespace, {
end,
})
---- @param bufnr (integer|nil) Buffer handle, or 0 or nil for current
+--- Query whether inlay hint is enabled in the {filter}ed scope
+--- @param filter vim.lsp.inlay_hint.enable.Filter
--- @return boolean
--- @since 12
-function M.is_enabled(bufnr)
+function M.is_enabled(filter)
+ vim.validate({ filter = { filter, 'table', true } })
+ filter = filter or {}
+ local bufnr = filter.bufnr
+
vim.validate({ bufnr = { bufnr, 'number', true } })
- if bufnr == nil or bufnr == 0 then
+ if bufnr == nil then
+ return globalstate.enabled
+ elseif bufnr == 0 then
bufnr = api.nvim_get_current_buf()
end
- return bufstates[bufnr] and bufstates[bufnr].enabled or false
+ return bufstates[bufnr].enabled
end
---- Enables or disables inlay hints for a buffer.
+--- Optional filters |kwargs|, or `nil` for all.
+--- @class vim.lsp.inlay_hint.enable.Filter
+--- @inlinedoc
+--- Buffer number, or 0 for current buffer, or nil for all.
+--- @field bufnr integer?
+
+--- Enables or disables inlay hints for the {filter}ed scope.
---
--- To "toggle", pass the inverse of `is_enabled()`:
---
--- ```lua
---- vim.lsp.inlay_hint.enable(0, not vim.lsp.inlay_hint.is_enabled())
+--- vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled())
--- ```
---
---- @param bufnr (integer|nil) Buffer handle, or 0 or nil for current
--- @param enable (boolean|nil) true/nil to enable, false to disable
+--- @param filter vim.lsp.inlay_hint.enable.Filter?
--- @since 12
-function M.enable(bufnr, enable)
- vim.validate({ enable = { enable, 'boolean', true }, bufnr = { bufnr, 'number', true } })
- if enable == false then
- _disable(bufnr)
+function M.enable(enable, filter)
+ vim.validate({ enable = { enable, 'boolean', true }, filter = { filter, 'table', true } })
+ enable = enable == nil or enable
+ filter = filter or {}
+
+ if filter.bufnr == nil then
+ globalstate.enabled = enable
+ for _, bufnr in ipairs(api.nvim_list_bufs()) do
+ if api.nvim_buf_is_loaded(bufnr) then
+ if enable == false then
+ _disable(bufnr)
+ else
+ _enable(bufnr)
+ end
+ else
+ bufstates[bufnr] = nil
+ end
+ end
else
- _enable(bufnr)
+ if enable == false then
+ _disable(filter.bufnr)
+ else
+ _enable(filter.bufnr)
+ end
end
end
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 599f02425e..419c2ff644 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -10,6 +10,8 @@ local function get_value_set(tbl)
return value_set
end
+local sysname = vim.uv.os_uname().sysname
+
-- Protocol for the Microsoft Language Server Protocol (mslsp)
local protocol = {}
@@ -835,7 +837,10 @@ function protocol.make_client_capabilities()
refreshSupport = true,
},
didChangeWatchedFiles = {
- dynamicRegistration = true,
+ -- TODO(lewis6991): do not advertise didChangeWatchedFiles on Linux
+ -- or BSD since all the current backends are too limited.
+ -- Ref: #27807, #28058, #23291, #26520
+ dynamicRegistration = sysname == 'Darwin' or sysname == 'Windows_NT',
relativePatternSupport = true,
},
inlayHint = {
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 984e4f040a..3c63a12da2 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -621,95 +621,67 @@ local function merge_dispatchers(dispatchers)
return merged
end
---- Create a LSP RPC client factory that connects via TCP to the given host and port.
+--- Create a LSP RPC client factory that connects to either:
+---
+--- - a named pipe (windows)
+--- - a domain socket (unix)
+--- - 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()|.
---
----@param host string host to connect to
----@param port integer port to connect to
+---@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, port)
+function M.connect(host_or_path, port)
return function(dispatchers)
dispatchers = merge_dispatchers(dispatchers)
- local tcp = assert(uv.new_tcp())
+ 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)
- tcp:write(msg)
- end,
- is_closing = function()
- return closing
- end,
- terminate = function()
- if not closing then
- closing = true
- tcp:shutdown()
- tcp:close()
- dispatchers.on_exit(0, 0)
+ 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,
- }
- local client = new_client(dispatchers, transport)
- tcp:connect(host, port, function(err)
- if err then
- vim.schedule(function()
- vim.notify(
- string.format('Could not connect to %s:%s, reason: %s', host, port, vim.inspect(err)),
- vim.log.levels.WARN
- )
- end)
- return
- end
- local handle_body = function(body)
- client:handle_body(body)
- end
- tcp:read_start(M.create_read_loop(handle_body, transport.terminate, function(read_err)
- client:on_error(M.client_errors.READ_ERROR, read_err)
- end))
- end)
-
- return public_client(client)
- end
-end
-
---- Create a LSP RPC client factory that connects via named pipes (Windows)
---- or unix domain sockets (Unix) to the given pipe_path (file path on
---- Unix and name on Windows).
----
---- Return a function that can be passed to the `cmd` field for
---- |vim.lsp.start_client()| or |vim.lsp.start()|.
----
----@param pipe_path string file path of the domain socket (Unix) or name of the named pipe (Windows) to connect to
----@return fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient
-function M.domain_socket_connect(pipe_path)
- return function(dispatchers)
- dispatchers = merge_dispatchers(dispatchers)
- local pipe =
- assert(uv.new_pipe(false), string.format('pipe with name %s could not be opened.', pipe_path))
- local closing = false
- local transport = {
- write = vim.schedule_wrap(function(msg)
- pipe:write(msg)
- end),
is_closing = function()
return closing
end,
terminate = function()
if not closing then
closing = true
- pipe:shutdown()
- pipe:close()
+ handle:shutdown()
+ handle:close()
dispatchers.on_exit(0, 0)
end
end,
}
local client = new_client(dispatchers, transport)
- pipe:connect(pipe_path, function(err)
+ 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', pipe_path, vim.inspect(err)),
+ string.format('Could not connect to %s, reason: %s', address, vim.inspect(err)),
vim.log.levels.WARN
)
end)
@@ -718,10 +690,19 @@ function M.domain_socket_connect(pipe_path)
local handle_body = function(body)
client:handle_body(body)
end
- pipe:read_start(M.create_read_loop(handle_body, transport.terminate, function(read_err)
+ 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))
- 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
+ handle:connect(host_or_path, port, on_connect)
+ end
return public_client(client)
end
@@ -741,7 +722,7 @@ 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:
+--- @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.
@@ -816,8 +797,7 @@ function M.start(cmd, dispatchers, extra_spawn_params)
end
local msg =
string.format('Spawning language server with cmd: `%s` failed%s', vim.inspect(cmd), sfx)
- vim.notify(msg, vim.log.levels.WARN)
- return nil
+ error(msg)
end
sysobj = sysobj_or_err --[[@as vim.SystemObj]]
diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua
index 20ac0a125f..ef2502b12e 100644
--- a/runtime/lua/vim/lsp/semantic_tokens.lua
+++ b/runtime/lua/vim/lsp/semantic_tokens.lua
@@ -394,7 +394,7 @@ function STHighlighter:process_response(response, client, version)
current_result.namespace_cleared = false
-- redraw all windows displaying buffer
- api.nvim__buf_redraw_range(self.bufnr, 0, -1)
+ api.nvim__redraw({ buf = self.bufnr, valid = true })
end
--- on_win handler for the decoration provider (see |nvim_set_decoration_provider|)
@@ -570,9 +570,9 @@ local M = {}
--- client.server_capabilities.semanticTokensProvider = nil
--- ```
---
----@param bufnr integer
----@param client_id integer
----@param opts? table Optional keyword arguments
+---@param bufnr (integer) Buffer number, or `0` for current buffer
+---@param client_id (integer) The ID of the |vim.lsp.Client|
+---@param opts? (table) Optional keyword arguments
--- - debounce (integer, default: 200): Debounce token requests
--- to the server by the given number in milliseconds
function M.start(bufnr, client_id, opts)
@@ -581,6 +581,10 @@ function M.start(bufnr, client_id, opts)
client_id = { client_id, 'n', false },
})
+ if bufnr == 0 then
+ bufnr = api.nvim_get_current_buf()
+ end
+
opts = opts or {}
assert(
(not opts.debounce or type(opts.debounce) == 'number'),
@@ -626,14 +630,18 @@ end
--- of `start()`, so you should only need this function to manually disengage the semantic
--- token engine without fully detaching the LSP client from the buffer.
---
----@param bufnr integer
----@param client_id integer
+---@param bufnr (integer) Buffer number, or `0` for current buffer
+---@param client_id (integer) The ID of the |vim.lsp.Client|
function M.stop(bufnr, client_id)
vim.validate({
bufnr = { bufnr, 'n', false },
client_id = { client_id, 'n', false },
})
+ if bufnr == 0 then
+ bufnr = api.nvim_get_current_buf()
+ end
+
local highlighter = STHighlighter.active[bufnr]
if not highlighter then
return
@@ -741,12 +749,15 @@ end
--- mark will be deleted by the semantic token engine when appropriate; for
--- example, when the LSP sends updated tokens. This function is intended for
--- use inside |LspTokenUpdate| callbacks.
----@param token (table) a semantic token, found as `args.data.token` in |LspTokenUpdate|.
----@param bufnr (integer) the buffer to highlight
+---@param token (table) A semantic token, found as `args.data.token` in |LspTokenUpdate|
+---@param bufnr (integer) The buffer to highlight, or `0` for current buffer
---@param client_id (integer) The ID of the |vim.lsp.Client|
---@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
local highlighter = STHighlighter.active[bufnr]
if not highlighter then
return
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index f8e5b6a90d..5a229a1169 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -1,5 +1,4 @@
local protocol = require('vim.lsp.protocol')
-local snippet = require('vim.lsp._snippet_grammar')
local validate = vim.validate
local api = vim.api
local list_extend = vim.list_extend
@@ -343,68 +342,6 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding)
return col
end
---- Process and return progress reports from lsp server
----@private
----@deprecated Use vim.lsp.status() or access client.progress directly
-function M.get_progress_messages()
- vim.deprecate('vim.lsp.util.get_progress_messages()', 'vim.lsp.status()', '0.11')
- local new_messages = {}
- local progress_remove = {}
-
- for _, client in ipairs(vim.lsp.get_clients()) do
- local groups = {}
- for progress in client.progress do
- local value = progress.value
- if type(value) == 'table' and value.kind then
- local group = groups[progress.token]
- if not group then
- group = {
- done = false,
- progress = true,
- title = 'empty title',
- }
- groups[progress.token] = group
- end
- group.title = value.title or group.title
- group.cancellable = value.cancellable or group.cancellable
- if value.kind == 'end' then
- group.done = true
- end
- group.message = value.message or group.message
- group.percentage = value.percentage or group.percentage
- end
- end
-
- for _, group in pairs(groups) do
- table.insert(new_messages, group)
- end
-
- local messages = client.messages
- local data = messages
- for token, ctx in pairs(data.progress) do
- local new_report = {
- name = data.name,
- title = ctx.title or 'empty title',
- message = ctx.message,
- percentage = ctx.percentage,
- done = ctx.done,
- progress = true,
- }
- table.insert(new_messages, new_report)
-
- if ctx.done then
- table.insert(progress_remove, { client = client, token = token })
- end
- end
- end
-
- for _, item in ipairs(progress_remove) do
- item.client.messages.progress[item.token] = nil
- end
-
- return new_messages
-end
-
--- Applies a list of text edits to a buffer.
---@param text_edits table list of `TextEdit` objects
---@param bufnr integer Buffer id
@@ -541,38 +478,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
end
end
--- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
--- local valid_unix_path_characters = "[^/]"
--- https://github.com/davidm/lua-glob-pattern
--- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
--- function M.glob_to_regex(glob)
--- end
-
---- Can be used to extract the completion items from a
---- `textDocument/completion` request, which may return one of
---- `CompletionItem[]`, `CompletionList` or null.
----
---- Note that this method doesn't apply `itemDefaults` to `CompletionList`s, and hence the returned
---- results might be incorrect.
----
----@deprecated
----@param result table The result of a `textDocument/completion` request
----@return lsp.CompletionItem[] List of completion items
----@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
-function M.extract_completion_items(result)
- vim.deprecate('vim.lsp.util.extract_completion_items()', nil, '0.11')
- if type(result) == 'table' and result.items then
- -- result is a `CompletionList`
- return result.items
- elseif result ~= nil then
- -- result is `CompletionItem[]`
- return result
- else
- -- result is `null`
- return {}
- end
-end
-
--- Applies a `TextDocumentEdit`, which is a list of changes to a single
--- document.
---
@@ -615,38 +520,6 @@ function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
M.apply_text_edits(text_document_edit.edits, bufnr, offset_encoding)
end
---- Parses snippets in a completion entry.
----
----@deprecated
----@param input string unparsed snippet
----@return string parsed snippet
-function M.parse_snippet(input)
- vim.deprecate('vim.lsp.util.parse_snippet()', nil, '0.11')
- local ok, parsed = pcall(function()
- return snippet.parse(input)
- end)
- if not ok then
- return input
- end
-
- return tostring(parsed)
-end
-
---- Turns the result of a `textDocument/completion` request into vim-compatible
---- |complete-items|.
----
----@deprecated
----@param result table The result of a `textDocument/completion` call, e.g.
---- from |vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
---- `CompletionList` or `null`
----@param prefix (string) the prefix to filter the completion items
----@return table[] items
----@see complete-items
-function M.text_document_completion_list_to_complete_items(result, prefix)
- vim.deprecate('vim.lsp.util.text_document_completion_list_to_complete_items()', nil, '0.11')
- return vim.lsp._completion._lsp_to_complete_items(result, prefix)
-end
-
local function path_components(path)
return vim.split(path, '/', { plain = true })
end
@@ -690,7 +563,7 @@ end
---
--- It deletes existing buffers that conflict with the renamed file name only when
--- * `opts` requests overwriting; or
---- * the conflicting buffers are not loaded, so that deleting thme does not result in data loss.
+--- * the conflicting buffers are not loaded, so that deleting them does not result in data loss.
---
--- @param old_fname string
--- @param new_fname string
diff --git a/runtime/lua/vim/provider/health.lua b/runtime/lua/vim/provider/health.lua
new file mode 100644
index 0000000000..63e0da448a
--- /dev/null
+++ b/runtime/lua/vim/provider/health.lua
@@ -0,0 +1,792 @@
+local health = vim.health
+local iswin = vim.uv.os_uname().sysname == 'Windows_NT'
+
+local M = {}
+
+local function clipboard()
+ health.start('Clipboard (optional)')
+
+ if
+ os.getenv('TMUX')
+ and vim.fn.executable('tmux') == 1
+ and vim.fn.executable('pbpaste') == 1
+ and not health._cmd_ok('pbpaste')
+ then
+ local tmux_version = string.match(vim.fn.system('tmux -V'), '%d+%.%d+')
+ local advice = {
+ 'Install tmux 2.6+. https://superuser.com/q/231130',
+ 'or use tmux with reattach-to-user-namespace. https://superuser.com/a/413233',
+ }
+ health.error('pbcopy does not work with tmux version: ' .. tmux_version, advice)
+ end
+
+ local clipboard_tool = vim.fn['provider#clipboard#Executable']()
+ if vim.g.clipboard ~= nil and clipboard_tool == '' then
+ local error_message = vim.fn['provider#clipboard#Error']()
+ health.error(
+ error_message,
+ "Use the example in :help g:clipboard as a template, or don't set g:clipboard at all."
+ )
+ elseif clipboard_tool:find('^%s*$') then
+ health.warn(
+ 'No clipboard tool found. Clipboard registers (`"+` and `"*`) will not work.',
+ ':help clipboard'
+ )
+ else
+ health.ok('Clipboard tool found: ' .. clipboard_tool)
+ end
+end
+
+local function node()
+ health.start('Node.js provider (optional)')
+
+ if health._provider_disabled('node') then
+ return
+ end
+
+ if
+ vim.fn.executable('node') == 0
+ or (
+ vim.fn.executable('npm') == 0
+ and vim.fn.executable('yarn') == 0
+ and vim.fn.executable('pnpm') == 0
+ )
+ then
+ health.warn(
+ '`node` and `npm` (or `yarn`, `pnpm`) must be in $PATH.',
+ 'Install Node.js and verify that `node` and `npm` (or `yarn`, `pnpm`) commands work.'
+ )
+ return
+ end
+
+ -- local node_v = vim.fn.split(system({'node', '-v'}), "\n")[1] or ''
+ local ok, node_v = health._cmd_ok({ 'node', '-v' })
+ health.info('Node.js: ' .. node_v)
+ if not ok or vim.version.lt(node_v, '6.0.0') then
+ health.warn('Nvim node.js host does not support Node ' .. node_v)
+ -- Skip further checks, they are nonsense if nodejs is too old.
+ return
+ end
+ if vim.fn['provider#node#can_inspect']() == 0 then
+ health.warn(
+ 'node.js on this system does not support --inspect-brk so $NVIM_NODE_HOST_DEBUG is ignored.'
+ )
+ end
+
+ local node_detect_table = vim.fn['provider#node#Detect']()
+ local host = node_detect_table[1]
+ if host:find('^%s*$') then
+ health.warn('Missing "neovim" npm (or yarn, pnpm) package.', {
+ 'Run in shell: npm install -g neovim',
+ 'Run in shell (if you use yarn): yarn global add neovim',
+ 'Run in shell (if you use pnpm): pnpm install -g neovim',
+ 'You may disable this provider (and warning) by adding `let g:loaded_node_provider = 0` to your init.vim',
+ })
+ return
+ end
+ health.info('Nvim node.js host: ' .. host)
+
+ local manager = 'npm'
+ if vim.fn.executable('yarn') == 1 then
+ manager = 'yarn'
+ elseif vim.fn.executable('pnpm') == 1 then
+ manager = 'pnpm'
+ end
+
+ local latest_npm_cmd = (
+ iswin and 'cmd /c ' .. manager .. ' info neovim --json' or manager .. ' info neovim --json'
+ )
+ local latest_npm
+ ok, latest_npm = health._cmd_ok(vim.split(latest_npm_cmd, ' '))
+ if not ok or latest_npm:find('^%s$') then
+ health.error(
+ 'Failed to run: ' .. latest_npm_cmd,
+ { "Make sure you're connected to the internet.", 'Are you behind a firewall or proxy?' }
+ )
+ return
+ end
+
+ local pcall_ok, pkg_data = pcall(vim.json.decode, latest_npm)
+ if not pcall_ok then
+ return 'error: ' .. latest_npm
+ end
+ local latest_npm_subtable = pkg_data['dist-tags'] or {}
+ latest_npm = latest_npm_subtable['latest'] or 'unable to parse'
+
+ local current_npm_cmd = { 'node', host, '--version' }
+ local current_npm
+ ok, current_npm = health._cmd_ok(current_npm_cmd)
+ if not ok then
+ health.error(
+ 'Failed to run: ' .. table.concat(current_npm_cmd, ' '),
+ { 'Report this issue with the output of: ', table.concat(current_npm_cmd, ' ') }
+ )
+ return
+ end
+
+ if latest_npm ~= 'unable to parse' and vim.version.lt(current_npm, latest_npm) then
+ local message = 'Package "neovim" is out-of-date. Installed: '
+ .. current_npm:gsub('%\n$', '')
+ .. ', latest: '
+ .. latest_npm:gsub('%\n$', '')
+
+ health.warn(message, {
+ 'Run in shell: npm install -g neovim',
+ 'Run in shell (if you use yarn): yarn global add neovim',
+ 'Run in shell (if you use pnpm): pnpm install -g neovim',
+ })
+ else
+ health.ok('Latest "neovim" npm/yarn/pnpm package is installed: ' .. current_npm)
+ end
+end
+
+local function perl()
+ health.start('Perl provider (optional)')
+
+ if health._provider_disabled('perl') then
+ return
+ end
+
+ local perl_exec, perl_warnings = vim.provider.perl.detect()
+
+ if not perl_exec then
+ health.warn(assert(perl_warnings), {
+ 'See :help provider-perl for more information.',
+ 'You may disable this provider (and warning) by adding `let g:loaded_perl_provider = 0` to your init.vim',
+ })
+ health.warn('No usable perl executable found')
+ return
+ end
+
+ health.info('perl executable: ' .. perl_exec)
+
+ -- we cannot use cpanm that is on the path, as it may not be for the perl
+ -- set with g:perl_host_prog
+ local ok = health._cmd_ok({ perl_exec, '-W', '-MApp::cpanminus', '-e', '' })
+ if not ok then
+ return { perl_exec, '"App::cpanminus" module is not installed' }
+ end
+
+ local latest_cpan_cmd = {
+ perl_exec,
+ '-MApp::cpanminus::fatscript',
+ '-e',
+ 'my $app = App::cpanminus::script->new; $app->parse_options ("--info", "-q", "Neovim::Ext"); exit $app->doit',
+ }
+ local latest_cpan
+ ok, latest_cpan = health._cmd_ok(latest_cpan_cmd)
+ if not ok or latest_cpan:find('^%s*$') then
+ health.error(
+ 'Failed to run: ' .. table.concat(latest_cpan_cmd, ' '),
+ { "Make sure you're connected to the internet.", 'Are you behind a firewall or proxy?' }
+ )
+ return
+ elseif latest_cpan[1] == '!' then
+ local cpanm_errs = vim.split(latest_cpan, '!')
+ if cpanm_errs[1]:find("Can't write to ") then
+ local advice = {}
+ for i = 2, #cpanm_errs do
+ advice[#advice + 1] = cpanm_errs[i]
+ end
+
+ health.warn(cpanm_errs[1], advice)
+ -- Last line is the package info
+ latest_cpan = cpanm_errs[#cpanm_errs]
+ else
+ health.error('Unknown warning from command: ' .. latest_cpan_cmd, cpanm_errs)
+ return
+ end
+ end
+ latest_cpan = vim.fn.matchstr(latest_cpan, [[\(\.\?\d\)\+]])
+ if latest_cpan:find('^%s*$') then
+ health.error('Cannot parse version number from cpanm output: ' .. latest_cpan)
+ return
+ end
+
+ local current_cpan_cmd = { perl_exec, '-W', '-MNeovim::Ext', '-e', 'print $Neovim::Ext::VERSION' }
+ local current_cpan
+ ok, current_cpan = health._cmd_ok(current_cpan_cmd)
+ if not ok then
+ health.error(
+ 'Failed to run: ' .. table.concat(current_cpan_cmd, ' '),
+ { 'Report this issue with the output of: ', table.concat(current_cpan_cmd, ' ') }
+ )
+ return
+ end
+
+ if vim.version.lt(current_cpan, latest_cpan) then
+ local message = 'Module "Neovim::Ext" is out-of-date. Installed: '
+ .. current_cpan
+ .. ', latest: '
+ .. latest_cpan
+ health.warn(message, 'Run in shell: cpanm -n Neovim::Ext')
+ else
+ health.ok('Latest "Neovim::Ext" cpan module is installed: ' .. current_cpan)
+ end
+end
+
+local function is(path, ty)
+ if not path then
+ return false
+ end
+ local stat = vim.uv.fs_stat(path)
+ if not stat then
+ return false
+ end
+ return stat.type == ty
+end
+
+-- Resolves Python executable path by invoking and checking `sys.executable`.
+local function python_exepath(invocation)
+ local p = vim.system({ invocation, '-c', 'import sys; sys.stdout.write(sys.executable)' }):wait()
+ assert(p.code == 0, p.stderr)
+ return vim.fs.normalize(vim.trim(p.stdout))
+end
+
+-- Check if pyenv is available and a valid pyenv root can be found, then return
+-- their respective paths. If either of those is invalid, return two empty
+-- strings, effectively ignoring pyenv.
+local function check_for_pyenv()
+ local pyenv_path = vim.fn.resolve(vim.fn.exepath('pyenv'))
+
+ if pyenv_path == '' then
+ return { '', '' }
+ end
+
+ health.info('pyenv: Path: ' .. pyenv_path)
+
+ local pyenv_root = vim.fn.resolve(os.getenv('PYENV_ROOT') or '')
+
+ if pyenv_root == '' then
+ pyenv_root = vim.fn.system({ pyenv_path, 'root' })
+ health.info('pyenv: $PYENV_ROOT is not set. Infer from `pyenv root`.')
+ end
+
+ if not is(pyenv_root, 'directory') then
+ local message = string.format(
+ 'pyenv: Root does not exist: %s. Ignoring pyenv for all following checks.',
+ pyenv_root
+ )
+ health.warn(message)
+ return { '', '' }
+ end
+
+ health.info('pyenv: Root: ' .. pyenv_root)
+
+ return { pyenv_path, pyenv_root }
+end
+
+-- Check the Python interpreter's usability.
+local function check_bin(bin)
+ if not is(bin, 'file') and (not iswin or not is(bin .. '.exe', 'file')) then
+ health.error('"' .. bin .. '" was not found.')
+ return false
+ elseif vim.fn.executable(bin) == 0 then
+ health.error('"' .. bin .. '" is not executable.')
+ return false
+ end
+ return true
+end
+
+-- Fetch the contents of a URL.
+local function download(url)
+ local has_curl = vim.fn.executable('curl') == 1
+ if has_curl and vim.fn.system({ 'curl', '-V' }):find('Protocols:.*https') then
+ local out, rc = health._system({ 'curl', '-sL', url }, { stderr = true, ignore_error = true })
+ if rc ~= 0 then
+ return 'curl error with ' .. url .. ': ' .. rc
+ else
+ return out
+ end
+ elseif vim.fn.executable('python') == 1 then
+ local script = "try:\n\
+ from urllib.request import urlopen\n\
+ except ImportError:\n\
+ from urllib2 import urlopen\n\
+ response = urlopen('" .. url .. "')\n\
+ print(response.read().decode('utf8'))\n"
+ local out, rc = health._system({ 'python', '-c', script })
+ if out == '' and rc ~= 0 then
+ return 'python urllib.request error: ' .. rc
+ else
+ return out
+ end
+ end
+
+ local message = 'missing `curl` '
+
+ if has_curl then
+ message = message .. '(with HTTPS support) '
+ end
+ message = message .. 'and `python`, cannot make web request'
+
+ return message
+end
+
+-- Get the latest Nvim Python client (pynvim) version from PyPI.
+local function latest_pypi_version()
+ local pypi_version = 'unable to get pypi response'
+ local pypi_response = download('https://pypi.python.org/pypi/pynvim/json')
+ if pypi_response ~= '' then
+ local pcall_ok, output = pcall(vim.fn.json_decode, pypi_response)
+ local pypi_data
+ if pcall_ok then
+ pypi_data = output
+ else
+ return 'error: ' .. pypi_response
+ end
+
+ local pypi_element = pypi_data['info'] or {}
+ pypi_version = pypi_element['version'] or 'unable to parse'
+ end
+ return pypi_version
+end
+
+local function is_bad_response(s)
+ local lower = s:lower()
+ return vim.startswith(lower, 'unable')
+ or vim.startswith(lower, 'error')
+ or vim.startswith(lower, 'outdated')
+end
+
+-- Get version information using the specified interpreter. The interpreter is
+-- used directly in case breaking changes were introduced since the last time
+-- Nvim's Python client was updated.
+--
+-- Returns: {
+-- {python executable version},
+-- {current nvim version},
+-- {current pypi nvim status},
+-- {installed version status}
+-- }
+local function version_info(python)
+ local pypi_version = latest_pypi_version()
+
+ local python_version, rc = health._system({
+ python,
+ '-c',
+ 'import sys; print(".".join(str(x) for x in sys.version_info[:3]))',
+ })
+
+ if rc ~= 0 or python_version == '' then
+ python_version = 'unable to parse ' .. python .. ' response'
+ end
+
+ local nvim_path
+ nvim_path, rc = health._system({
+ python,
+ '-c',
+ 'import sys; sys.path = [p for p in sys.path if p != ""]; import neovim; print(neovim.__file__)',
+ })
+ if rc ~= 0 or nvim_path == '' then
+ return { python_version, 'unable to load neovim Python module', pypi_version, nvim_path }
+ end
+
+ -- Assuming that multiple versions of a package are installed, sort them
+ -- numerically in descending order.
+ local function compare(metapath1, metapath2)
+ local a = vim.fn.matchstr(vim.fn.fnamemodify(metapath1, ':p:h:t'), [[[0-9.]\+]])
+ local b = vim.fn.matchstr(vim.fn.fnamemodify(metapath2, ':p:h:t'), [[[0-9.]\+]])
+ if a == b then
+ return 0
+ elseif a > b then
+ return 1
+ else
+ return -1
+ end
+ end
+
+ -- Try to get neovim.VERSION (added in 0.1.11dev).
+ local nvim_version
+ nvim_version, rc = health._system({
+ python,
+ '-c',
+ 'from neovim import VERSION as v; print("{}.{}.{}{}".format(v.major, v.minor, v.patch, v.prerelease))',
+ }, { stderr = true, ignore_error = true })
+ 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', 1, 1)
+ vim.list_extend(metas, vim.fn.glob(base .. '-*/PKG-INFO', 1, 1))
+ vim.list_extend(metas, vim.fn.glob(base .. '.egg-info/PKG-INFO', 1, 1))
+ metas = table.sort(metas, compare)
+
+ if metas and next(metas) ~= nil then
+ for line in io.lines(metas[1]) do
+ local version = line:match('^Version: (%S+)')
+ if version then
+ nvim_version = version
+ break
+ end
+ end
+ end
+ end
+
+ local nvim_path_base = vim.fn.fnamemodify(nvim_path, [[:~:h]])
+ local version_status = 'unknown; ' .. nvim_path_base
+ if is_bad_response(nvim_version) and is_bad_response(pypi_version) then
+ if vim.version.lt(nvim_version, pypi_version) then
+ version_status = 'outdated; from ' .. nvim_path_base
+ else
+ version_status = 'up to date'
+ end
+ end
+
+ return { python_version, nvim_version, pypi_version, version_status }
+end
+
+local function python()
+ health.start('Python 3 provider (optional)')
+
+ local pyname = 'python3' ---@type string?
+ local python_exe = ''
+ local virtual_env = os.getenv('VIRTUAL_ENV')
+ local venv = virtual_env and vim.fn.resolve(virtual_env) or ''
+ local host_prog_var = pyname .. '_host_prog'
+ local python_multiple = {}
+
+ if health._provider_disabled(pyname) then
+ return
+ end
+
+ local pyenv_table = check_for_pyenv()
+ local pyenv = pyenv_table[1]
+ local pyenv_root = pyenv_table[2]
+
+ if vim.g[host_prog_var] then
+ local message = string.format('Using: g:%s = "%s"', host_prog_var, vim.g[host_prog_var])
+ health.info(message)
+ end
+
+ local pythonx_warnings
+ pyname, pythonx_warnings = vim.provider.python.detect_by_module('neovim')
+
+ if not pyname then
+ health.warn(
+ 'No Python executable found that can `import neovim`. '
+ .. 'Using the first available executable for diagnostics.'
+ )
+ elseif vim.g[host_prog_var] then
+ python_exe = pyname
+ end
+
+ -- No Python executable could `import neovim`, or host_prog_var was used.
+ if pythonx_warnings then
+ health.warn(pythonx_warnings, {
+ 'See :help provider-python for more information.',
+ 'You may disable this provider (and warning) by adding `let g:loaded_python3_provider = 0` to your init.vim',
+ })
+ elseif pyname and pyname ~= '' and python_exe == '' then
+ if not vim.g[host_prog_var] then
+ local message = string.format(
+ '`g:%s` is not set. Searching for %s in the environment.',
+ host_prog_var,
+ pyname
+ )
+ health.info(message)
+ end
+
+ if pyenv ~= '' then
+ python_exe = health._system({ pyenv, 'which', pyname }, { stderr = true })
+ if python_exe == '' then
+ health.warn('pyenv could not find ' .. pyname .. '.')
+ end
+ end
+
+ if python_exe == '' then
+ python_exe = vim.fn.exepath(pyname)
+
+ if os.getenv('PATH') then
+ local path_sep = iswin and ';' or ':'
+ local paths = vim.split(os.getenv('PATH') or '', path_sep)
+
+ for _, path in ipairs(paths) do
+ local path_bin = vim.fs.normalize(path .. '/' .. pyname)
+ if
+ path_bin ~= vim.fs.normalize(python_exe)
+ and vim.tbl_contains(python_multiple, path_bin)
+ and vim.fn.executable(path_bin) == 1
+ then
+ python_multiple[#python_multiple + 1] = path_bin
+ end
+ end
+
+ if vim.tbl_count(python_multiple) > 0 then
+ -- This is worth noting since the user may install something
+ -- that changes $PATH, like homebrew.
+ local message = string.format(
+ 'Multiple %s executables found. Set `g:%s` to avoid surprises.',
+ pyname,
+ host_prog_var
+ )
+ health.info(message)
+ end
+
+ if python_exe:find('shims') then
+ local message = string.format('`%s` appears to be a pyenv shim.', python_exe)
+ local advice = string.format(
+ '`pyenv` is not in $PATH, your pyenv installation is broken. Set `g:%s` to avoid surprises.',
+ host_prog_var
+ )
+ health.warn(message, advice)
+ end
+ end
+ end
+ end
+
+ if python_exe ~= '' and not vim.g[host_prog_var] then
+ if
+ venv == ''
+ and pyenv ~= ''
+ and pyenv_root ~= ''
+ and vim.startswith(vim.fn.resolve(python_exe), pyenv_root .. '/')
+ then
+ local advice = string.format(
+ 'Create a virtualenv specifically for Nvim using pyenv, and set `g:%s`. This will avoid the need to install the pynvim module in each version/virtualenv.',
+ host_prog_var
+ )
+ health.warn('pyenv is not set up optimally.', advice)
+ elseif venv ~= '' then
+ local venv_root
+ if pyenv_root ~= '' then
+ venv_root = pyenv_root
+ else
+ venv_root = vim.fs.dirname(venv)
+ end
+
+ if vim.startswith(vim.fn.resolve(python_exe), venv_root .. '/') then
+ local advice = string.format(
+ 'Create a virtualenv specifically for Nvim and use `g:%s`. This will avoid the need to install the pynvim module in each virtualenv.',
+ host_prog_var
+ )
+ health.warn('Your virtualenv is not set up optimally.', advice)
+ end
+ end
+ end
+
+ if pyname and python_exe == '' and pyname ~= '' then
+ -- An error message should have already printed.
+ health.error('`' .. pyname .. '` was not found.')
+ elseif python_exe ~= '' and not check_bin(python_exe) then
+ python_exe = ''
+ end
+
+ -- Diagnostic output
+ health.info('Executable: ' .. (python_exe == '' and 'Not found' or python_exe))
+ if vim.tbl_count(python_multiple) > 0 then
+ for _, path_bin in ipairs(python_multiple) do
+ health.info('Other python executable: ' .. path_bin)
+ end
+ end
+
+ if python_exe == '' then
+ -- No Python executable can import 'neovim'. Check if any Python executable
+ -- can import 'pynvim'. If so, that Python failed to import 'neovim' as
+ -- well, which is most probably due to a failed pip upgrade:
+ -- https://github.com/neovim/neovim/wiki/Following-HEAD#20181118
+ local pynvim_exe = vim.provider.python.detect_by_module('pynvim')
+ if pynvim_exe then
+ local message = 'Detected pip upgrade failure: Python executable can import "pynvim" but not "neovim": '
+ .. pynvim_exe
+ local advice = {
+ 'Use that Python version to reinstall "pynvim" and optionally "neovim".',
+ pynvim_exe .. ' -m pip uninstall pynvim neovim',
+ pynvim_exe .. ' -m pip install pynvim',
+ pynvim_exe .. ' -m pip install neovim # only if needed by third-party software',
+ }
+ health.error(message, advice)
+ end
+ else
+ local version_info_table = version_info(python_exe)
+ local pyversion = version_info_table[1]
+ local current = version_info_table[2]
+ local latest = version_info_table[3]
+ local status = version_info_table[4]
+
+ if not vim.version.range('~3'):has(pyversion) then
+ health.warn('Unexpected Python version. This could lead to confusing error messages.')
+ end
+
+ health.info('Python version: ' .. pyversion)
+
+ if is_bad_response(status) then
+ health.info('pynvim version: ' .. current .. ' (' .. status .. ')')
+ else
+ health.info('pynvim version: ' .. current)
+ end
+
+ if is_bad_response(current) then
+ health.error(
+ 'pynvim is not installed.\nError: ' .. current,
+ 'Run in shell: ' .. python_exe .. ' -m pip install pynvim'
+ )
+ end
+
+ if is_bad_response(latest) then
+ health.warn('Could not contact PyPI to get latest version.')
+ health.error('HTTP request failed: ' .. latest)
+ elseif is_bad_response(status) then
+ health.warn('Latest pynvim is NOT installed: ' .. latest)
+ elseif not is_bad_response(current) then
+ health.ok('Latest pynvim is installed.')
+ end
+ end
+
+ health.start('Python virtualenv')
+ if not virtual_env then
+ health.ok('no $VIRTUAL_ENV')
+ return
+ end
+ local errors = {}
+ -- Keep hints as dict keys in order to discard duplicates.
+ local hints = {}
+ -- The virtualenv should contain some Python executables, and those
+ -- executables should be first both on Nvim's $PATH and the $PATH of
+ -- 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)
+ venv_bins = vim.tbl_filter(function(v)
+ -- XXX: Remove irrelevant executables found in bin/.
+ return not v:match('python%-config')
+ end, venv_bins)
+ if vim.tbl_count(venv_bins) > 0 then
+ for _, venv_bin in pairs(venv_bins) do
+ venv_bin = vim.fs.normalize(venv_bin)
+ local py_bin_basename = vim.fs.basename(venv_bin)
+ local nvim_py_bin = python_exepath(vim.fn.exepath(py_bin_basename))
+ local subshell_py_bin = python_exepath(py_bin_basename)
+ if venv_bin ~= nvim_py_bin then
+ errors[#errors + 1] = '$PATH yields this '
+ .. py_bin_basename
+ .. ' executable: '
+ .. nvim_py_bin
+ local hint = '$PATH ambiguities arise if the virtualenv is not '
+ .. 'properly activated prior to launching Nvim. Close Nvim, activate the virtualenv, '
+ .. 'check that invoking Python from the command line launches the correct one, '
+ .. 'then relaunch Nvim.'
+ hints[hint] = true
+ end
+ if venv_bin ~= subshell_py_bin then
+ errors[#errors + 1] = '$PATH in subshells yields this '
+ .. py_bin_basename
+ .. ' executable: '
+ .. subshell_py_bin
+ local hint = '$PATH ambiguities in subshells typically are '
+ .. 'caused by your shell config overriding the $PATH previously set by the '
+ .. 'virtualenv. Either prevent them from doing so, or use this workaround: '
+ .. 'https://vi.stackexchange.com/a/34996'
+ hints[hint] = true
+ end
+ end
+ else
+ errors[#errors + 1] = 'no Python executables found in the virtualenv '
+ .. bin_dir
+ .. ' directory.'
+ end
+
+ local msg = '$VIRTUAL_ENV is set to: ' .. virtual_env
+ if vim.tbl_count(errors) > 0 then
+ if vim.tbl_count(venv_bins) > 0 then
+ msg = string.format(
+ '%s\nAnd its %s directory contains: %s',
+ msg,
+ bin_dir,
+ table.concat(
+ vim.tbl_map(function(v)
+ return vim.fs.basename(v)
+ end, venv_bins),
+ ', '
+ )
+ )
+ end
+ local conj = '\nBut '
+ for _, err in ipairs(errors) do
+ msg = msg .. conj .. err
+ conj = '\nAnd '
+ end
+ msg = msg .. '\nSo invoking Python may lead to unexpected results.'
+ health.warn(msg, vim.tbl_keys(hints))
+ else
+ health.info(msg)
+ health.info(
+ 'Python version: '
+ .. health._system(
+ 'python -c "import platform, sys; sys.stdout.write(platform.python_version())"'
+ )
+ )
+ health.ok('$VIRTUAL_ENV provides :!python.')
+ end
+end
+
+local function ruby()
+ health.start('Ruby provider (optional)')
+
+ if health._provider_disabled('ruby') then
+ return
+ end
+
+ if vim.fn.executable('ruby') == 0 or vim.fn.executable('gem') == 0 then
+ health.warn(
+ '`ruby` and `gem` must be in $PATH.',
+ 'Install Ruby and verify that `ruby` and `gem` commands work.'
+ )
+ return
+ end
+ health.info('Ruby: ' .. health._system({ 'ruby', '-v' }))
+
+ local host, _ = vim.provider.ruby.detect()
+ if (not host) or host:find('^%s*$') then
+ health.warn('`neovim-ruby-host` not found.', {
+ 'Run `gem install neovim` to ensure the neovim RubyGem is installed.',
+ 'Run `gem environment` to ensure the gem bin directory is in $PATH.',
+ 'If you are using rvm/rbenv/chruby, try "rehashing".',
+ 'See :help g:ruby_host_prog for non-standard gem installations.',
+ 'You may disable this provider (and warning) by adding `let g:loaded_ruby_provider = 0` to your init.vim',
+ })
+ return
+ end
+ health.info('Host: ' .. host)
+
+ local latest_gem_cmd = (iswin and 'cmd /c gem list -ra "^^neovim$"' or 'gem list -ra ^neovim$')
+ local ok, latest_gem = health._cmd_ok(vim.split(latest_gem_cmd, ' '))
+ if not ok or latest_gem:find('^%s*$') then
+ health.error(
+ 'Failed to run: ' .. latest_gem_cmd,
+ { "Make sure you're connected to the internet.", 'Are you behind a firewall or proxy?' }
+ )
+ return
+ end
+ local gem_split = vim.split(latest_gem, [[neovim (\|, \|)$]])
+ latest_gem = gem_split[1] or 'not found'
+
+ local current_gem_cmd = { host, '--version' }
+ local current_gem
+ ok, current_gem = health._cmd_ok(current_gem_cmd)
+ if not ok then
+ health.error(
+ 'Failed to run: ' .. table.concat(current_gem_cmd, ' '),
+ { 'Report this issue with the output of: ', table.concat(current_gem_cmd, ' ') }
+ )
+ return
+ end
+
+ if vim.version.lt(current_gem, latest_gem) then
+ local message = 'Gem "neovim" is out-of-date. Installed: '
+ .. current_gem
+ .. ', latest: '
+ .. latest_gem
+ health.warn(message, 'Run in shell: gem update neovim')
+ else
+ health.ok('Latest "neovim" gem is installed: ' .. current_gem)
+ end
+end
+
+function M.check()
+ clipboard()
+ node()
+ perl()
+ python()
+ ruby()
+end
+
+return M
diff --git a/runtime/lua/vim/secure.lua b/runtime/lua/vim/secure.lua
index 3992eef78a..41a3d3ba25 100644
--- a/runtime/lua/vim/secure.lua
+++ b/runtime/lua/vim/secure.lua
@@ -126,7 +126,7 @@ end
---
--- The trust database is located at |$XDG_STATE_HOME|/nvim/trust.
---
----@param opts? vim.trust.opts
+---@param opts vim.trust.opts
---@return boolean success true if operation was successful
---@return string msg full path if operation was successful, else error message
function M.trust(opts)
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index bd553598c7..e9e4326057 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -356,7 +356,7 @@ end
--- We only merge empty tables or tables that are not an array (indexed by integers)
local function can_merge(v)
- return type(v) == 'table' and (vim.tbl_isempty(v) or not vim.tbl_isarray(v))
+ return type(v) == 'table' and (vim.tbl_isempty(v) or not vim.isarray(v))
end
local function tbl_extend(behavior, deep_extend, ...)
@@ -402,7 +402,7 @@ end
---
---@see |extend()|
---
----@param behavior string Decides what to do if a key is found in more than one map:
+---@param behavior 'error'|'keep'|'force' Decides what to do if a key is found in more than one map:
--- - "error": raise an error
--- - "keep": use value from the leftmost map
--- - "force": use value from the rightmost map
@@ -418,7 +418,7 @@ end
---
---@generic T1: table
---@generic T2: table
----@param behavior "error"|"keep"|"force" (string) Decides what to do if a key is found in more than one map:
+---@param behavior 'error'|'keep'|'force' Decides what to do if a key is found in more than one map:
--- - "error": raise an error
--- - "keep": use value from the leftmost map
--- - "force": use value from the rightmost map
@@ -502,7 +502,7 @@ end
---
---@param o table Table to index
---@param ... any Optional keys (0 or more, variadic) via which to index the table
----@return any : Nested value indexed by key (if it exists), else nil
+---@return any # Nested value indexed by key (if it exists), else nil
function vim.tbl_get(o, ...)
local keys = { ... }
if #keys == 0 then
@@ -544,6 +544,7 @@ function vim.list_extend(dst, src, start, finish)
return dst
end
+--- @deprecated
--- Creates a copy of a list-like table such that any nested tables are
--- "unrolled" and appended to the result.
---
@@ -552,6 +553,7 @@ end
---@param t table List-like table
---@return table Flattened copy of the given list-like table
function vim.tbl_flatten(t)
+ vim.deprecate('vim.tbl_flatten', 'vim.iter(…):flatten():totable()', '0.13')
local result = {}
--- @param _t table<any,any>
local function _tbl_flatten(_t)
@@ -578,7 +580,7 @@ end
---@return fun(table: table<K, V>, index?: K):K, V # |for-in| iterator over sorted keys and their values
---@return T
function vim.spairs(t)
- vim.validate({ t = { t, 't' } })
+ assert(type(t) == 'table', ('expected table, got %s'):format(type(t)))
--- @cast t table<any,any>
-- collect the keys
@@ -601,16 +603,16 @@ end
--- Tests if `t` is an "array": a table indexed _only_ by integers (potentially non-contiguous).
---
---- If the indexes start from 1 and are contiguous then the array is also a list. |vim.tbl_islist()|
+--- If the indexes start from 1 and are contiguous then the array is also a list. |vim.islist()|
---
--- Empty table `{}` is an array, unless it was created by |vim.empty_dict()| or returned as
--- a dict-like |API| or Vimscript result, for example from |rpcrequest()| or |vim.fn|.
---
---@see https://github.com/openresty/luajit2#tableisarray
---
----@param t table
+---@param t? table
---@return boolean `true` if array-like table, else `false`.
-function vim.tbl_isarray(t)
+function vim.isarray(t)
if type(t) ~= 'table' then
return false
end
@@ -640,17 +642,23 @@ function vim.tbl_isarray(t)
end
end
+--- @deprecated
+function vim.tbl_islist(t)
+ vim.deprecate('vim.tbl_islist', 'vim.islist', '0.12')
+ return vim.islist(t)
+end
+
--- Tests if `t` is a "list": a table indexed _only_ by contiguous integers starting from 1 (what
--- |lua-length| calls a "regular array").
---
--- Empty table `{}` is a list, unless it was created by |vim.empty_dict()| or returned as
--- a dict-like |API| or Vimscript result, for example from |rpcrequest()| or |vim.fn|.
---
----@see |vim.tbl_isarray()|
+---@see |vim.isarray()|
---
----@param t table
+---@param t? table
---@return boolean `true` if list-like table, else `false`.
-function vim.tbl_islist(t)
+function vim.islist(t)
if type(t) ~= 'table' then
return false
end
@@ -788,6 +796,61 @@ do
return type(val) == t or (t == 'callable' and vim.is_callable(val))
end
+ --- @param param_name string
+ --- @param spec vim.validate.Spec
+ --- @return string?
+ local function is_param_valid(param_name, spec)
+ if type(spec) ~= 'table' then
+ return string.format('opt[%s]: expected table, got %s', param_name, type(spec))
+ end
+
+ local val = spec[1] -- Argument value
+ local types = spec[2] -- Type name, or callable
+ local optional = (true == spec[3])
+
+ if type(types) == 'string' then
+ types = { types }
+ end
+
+ if vim.is_callable(types) then
+ -- Check user-provided validation function
+ local valid, optional_message = types(val)
+ if not valid then
+ local error_message =
+ string.format('%s: expected %s, got %s', param_name, (spec[3] or '?'), tostring(val))
+ if optional_message ~= nil then
+ error_message = string.format('%s. Info: %s', error_message, optional_message)
+ end
+
+ return error_message
+ end
+ elseif type(types) == 'table' then
+ local success = false
+ for i, t in ipairs(types) do
+ local t_name = type_names[t]
+ if not t_name then
+ return string.format('invalid type name: %s', t)
+ end
+ types[i] = t_name
+
+ if (optional and val == nil) or _is_type(val, t_name) then
+ success = true
+ break
+ end
+ end
+ if not success then
+ return string.format(
+ '%s: expected %s, got %s',
+ param_name,
+ table.concat(types, '|'),
+ type(val)
+ )
+ end
+ else
+ return string.format('invalid type name: %s', tostring(types))
+ end
+ end
+
--- @param opt table<vim.validate.Type,vim.validate.Spec>
--- @return boolean, string?
local function is_valid(opt)
@@ -795,63 +858,27 @@ do
return false, string.format('opt: expected table, got %s', type(opt))
end
- for param_name, spec in pairs(opt) do
- if type(spec) ~= 'table' then
- return false, string.format('opt[%s]: expected table, got %s', param_name, type(spec))
- end
-
- local val = spec[1] -- Argument value
- local types = spec[2] -- Type name, or callable
- local optional = (true == spec[3])
+ local report --- @type table<string,string>?
- if type(types) == 'string' then
- types = { types }
+ for param_name, spec in pairs(opt) do
+ local msg = is_param_valid(param_name, spec)
+ if msg then
+ report = report or {}
+ report[param_name] = msg
end
+ end
- if vim.is_callable(types) then
- -- Check user-provided validation function
- local valid, optional_message = types(val)
- if not valid then
- local error_message =
- string.format('%s: expected %s, got %s', param_name, (spec[3] or '?'), tostring(val))
- if optional_message ~= nil then
- error_message = error_message .. string.format('. Info: %s', optional_message)
- end
-
- return false, error_message
- end
- elseif type(types) == 'table' then
- local success = false
- for i, t in ipairs(types) do
- local t_name = type_names[t]
- if not t_name then
- return false, string.format('invalid type name: %s', t)
- end
- types[i] = t_name
-
- if (optional and val == nil) or _is_type(val, t_name) then
- success = true
- break
- end
- end
- if not success then
- return false,
- string.format(
- '%s: expected %s, got %s',
- param_name,
- table.concat(types, '|'),
- type(val)
- )
- end
- else
- return false, string.format('invalid type name: %s', tostring(types))
+ if report then
+ for _, msg in vim.spairs(report) do -- luacheck: ignore
+ return false, msg
end
end
return true
end
- --- Validates a parameter specification (types and values).
+ --- Validates a parameter specification (types and values). Specs are evaluated in alphanumeric
+ --- order, until the first failure.
---
--- Usage example:
---
diff --git a/runtime/lua/vim/snippet.lua b/runtime/lua/vim/snippet.lua
index 5e60efa778..3d8f73f362 100644
--- a/runtime/lua/vim/snippet.lua
+++ b/runtime/lua/vim/snippet.lua
@@ -254,9 +254,10 @@ local function display_choices(tabstop)
assert(tabstop.choices, 'Tabstop has no choices')
local start_col = tabstop:get_range()[2] + 1
- local matches = vim.iter.map(function(choice)
- return { word = choice }
- end, tabstop.choices)
+ local matches = {} --- @type table[]
+ for _, choice in ipairs(tabstop.choices) do
+ matches[#matches + 1] = { word = choice }
+ end
vim.defer_fn(function()
vim.fn.complete(start_col, matches)
@@ -342,7 +343,7 @@ local function setup_autocmds(bufnr)
or cursor_row > snippet_range[3]
or (cursor_row == snippet_range[3] and cursor_col > snippet_range[4])
then
- M.exit()
+ M.stop()
return true
end
@@ -361,7 +362,7 @@ local function setup_autocmds(bufnr)
end
-- The cursor is either not on a tabstop or we reached the end, so exit the session.
- M.exit()
+ M.stop()
return true
end,
})
@@ -377,7 +378,7 @@ local function setup_autocmds(bufnr)
(snippet_range[1] == snippet_range[3] and snippet_range[2] == snippet_range[4])
or snippet_range[3] + 1 > vim.fn.line('$')
then
- M.exit()
+ M.stop()
end
if not M.active() then
@@ -400,7 +401,7 @@ end
--- Refer to https://microsoft.github.io/language-server-protocol/specification/#snippet_syntax
--- for the specification of valid input.
---
---- Tabstops are highlighted with hl-SnippetTabstop.
+--- Tabstops are highlighted with |hl-SnippetTabstop|.
---
--- @param input string
function M.expand(input)
@@ -446,17 +447,22 @@ function M.expand(input)
base_indent = base_indent .. (snippet_lines[#snippet_lines]:match('(^%s*)%S') or '') --- @type string
end
- local lines = vim.iter.map(function(i, line)
+ local shiftwidth = vim.fn.shiftwidth()
+ local curbuf = vim.api.nvim_get_current_buf()
+ local expandtab = vim.bo[curbuf].expandtab
+
+ local lines = {} --- @type string[]
+ for i, line in ipairs(text_to_lines(text)) do
-- Replace tabs by spaces.
- if vim.o.expandtab then
- line = line:gsub('\t', (' '):rep(vim.fn.shiftwidth())) --- @type string
+ if expandtab then
+ line = line:gsub('\t', (' '):rep(shiftwidth)) --- @type string
end
-- Add the base indentation.
if i > 1 then
line = base_indent .. line
end
- return line
- end, ipairs(text_to_lines(text)))
+ lines[#lines + 1] = line
+ end
table.insert(snippet_text, table.concat(lines, '\n'))
end
@@ -526,37 +532,13 @@ end
--- @alias vim.snippet.Direction -1 | 1
---- Returns `true` if there is an active snippet which can be jumped in the given direction.
---- You can use this function to navigate a snippet as follows:
+--- Jumps to the next (or previous) placeholder in the current snippet, if possible.
---
---- ```lua
---- vim.keymap.set({ 'i', 's' }, '<Tab>', function()
---- if vim.snippet.jumpable(1) then
---- return '<cmd>lua vim.snippet.jump(1)<cr>'
---- else
---- return '<Tab>'
---- end
---- end, { expr = true })
---- ```
----
---- @param direction (vim.snippet.Direction) Navigation direction. -1 for previous, 1 for next.
---- @return boolean
-function M.jumpable(direction)
- if not M.active() then
- return false
- end
-
- return M._session:get_dest_index(direction) ~= nil
-end
-
---- Jumps within the active snippet in the given direction.
---- If the jump isn't possible, the function call does nothing.
----
---- You can use this function to navigate a snippet as follows:
+--- For example, map `<Tab>` to jump while a snippet is active:
---
--- ```lua
--- vim.keymap.set({ 'i', 's' }, '<Tab>', function()
---- if vim.snippet.jumpable(1) then
+--- if vim.snippet.active({ direction = 1 }) then
--- return '<cmd>lua vim.snippet.jump(1)<cr>'
--- else
--- return '<Tab>'
@@ -598,15 +580,41 @@ function M.jump(direction)
setup_autocmds(M._session.bufnr)
end
---- Returns `true` if there's an active snippet in the current buffer.
+--- @class vim.snippet.ActiveFilter
+--- @field direction vim.snippet.Direction Navigation direction. -1 for previous, 1 for next.
+
+--- Returns `true` if there's an active snippet in the current buffer,
+--- applying the given filter if provided.
+---
+--- You can use this function to navigate a snippet as follows:
+---
+--- ```lua
+--- vim.keymap.set({ 'i', 's' }, '<Tab>', function()
+--- if vim.snippet.active({ direction = 1 }) then
+--- return '<cmd>lua vim.snippet.jump(1)<cr>'
+--- else
+--- return '<Tab>'
+--- end
+--- end, { expr = true })
+--- ```
---
+--- @param filter? vim.snippet.ActiveFilter Filter to constrain the search with:
+--- - `direction` (vim.snippet.Direction): Navigation direction. Will return `true` if the snippet
+--- can be jumped in the given direction.
--- @return boolean
-function M.active()
- return M._session ~= nil and M._session.bufnr == vim.api.nvim_get_current_buf()
+function M.active(filter)
+ local active = M._session ~= nil and M._session.bufnr == vim.api.nvim_get_current_buf()
+
+ local in_direction = true
+ if active and filter and filter.direction then
+ in_direction = M._session:get_dest_index(filter.direction) ~= nil
+ end
+
+ return active and in_direction
end
--- Exits the current snippet.
-function M.exit()
+function M.stop()
if not M.active() then
return
end
diff --git a/runtime/lua/vim/termcap.lua b/runtime/lua/vim/termcap.lua
index ec29acca48..1da2e71839 100644
--- a/runtime/lua/vim/termcap.lua
+++ b/runtime/lua/vim/termcap.lua
@@ -12,7 +12,7 @@ local M = {}
--- emulator supports the XTGETTCAP sequence.
---
--- @param caps string|table A terminal capability or list of capabilities to query
---- @param cb function(cap:string, found:bool, seq:string?) Callback function which is called for
+--- @param cb fun(cap:string, found:boolean, seq:string?) Callback function which is called for
--- each capability in {caps}. {found} is set to true if the capability was found or false
--- otherwise. {seq} is the control sequence for the capability if found, or nil for
--- boolean capabilities.
diff --git a/runtime/lua/vim/text.lua b/runtime/lua/vim/text.lua
index 576b962838..bc90d490aa 100644
--- a/runtime/lua/vim/text.lua
+++ b/runtime/lua/vim/text.lua
@@ -5,7 +5,7 @@ local M = {}
--- Hex encode a string.
---
--- @param str string String to encode
---- @return string Hex encoded string
+--- @return string : Hex encoded string
function M.hexencode(str)
local bytes = { str:byte(1, #str) }
local enc = {} ---@type string[]
@@ -18,7 +18,7 @@ end
--- Hex decode a string.
---
--- @param enc string String to decode
---- @return string Decoded string
+--- @return string : Decoded string
function M.hexdecode(enc)
assert(#enc % 2 == 0, 'string must have an even number of hex characters')
local str = {} ---@type string[]
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
index a09619f369..db544c1ab1 100644
--- a/runtime/lua/vim/treesitter.lua
+++ b/runtime/lua/vim/treesitter.lua
@@ -257,7 +257,7 @@ end
---@param row integer Position row
---@param col integer Position column
---
----@return table[] List of captures `{ capture = "name", metadata = { ... } }`
+---@return {capture: string, lang: string, metadata: table}[]
function M.get_captures_at_pos(bufnr, row, col)
if bufnr == 0 then
bufnr = api.nvim_get_current_buf()
@@ -327,7 +327,7 @@ function M.get_captures_at_cursor(winnr)
end
--- Optional keyword arguments:
---- @class vim.treesitter.get_node.Opts
+--- @class vim.treesitter.get_node.Opts : vim.treesitter.LanguageTree.tree_for_range.Opts
--- @inlinedoc
---
--- Buffer number (nil or 0 for current buffer)
diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua
index d96cc966de..eecf1ad6b1 100644
--- a/runtime/lua/vim/treesitter/_fold.lua
+++ b/runtime/lua/vim/treesitter/_fold.lua
@@ -4,10 +4,21 @@ local Range = require('vim.treesitter._range')
local api = vim.api
+---Treesitter folding is done in two steps:
+---(1) compute the fold levels with the syntax tree and cache the result (`compute_folds_levels`)
+---(2) evaluate foldexpr for each window, which reads from the cache (`foldupdate`)
---@class TS.FoldInfo
----@field levels string[] the foldexpr result for each line
----@field levels0 integer[] the raw fold levels
----@field edits? {[1]: integer, [2]: integer} line range edited since the last invocation of the callback scheduled in on_bytes. 0-indexed, end-exclusive.
+---
+---@field levels string[] the cached foldexpr result for each line
+---@field levels0 integer[] the cached raw fold levels
+---
+---The range edited since the last invocation of the callback scheduled in on_bytes.
+---Should compute fold levels in this range.
+---@field on_bytes_range? Range2
+---
+---The range on which to evaluate foldexpr.
+---When in insert mode, the evaluation is deferred to InsertLeave.
+---@field foldupdate_range? Range2
local FoldInfo = {}
FoldInfo.__index = FoldInfo
@@ -80,45 +91,16 @@ function FoldInfo:add_range(srow, erow)
list_insert(self.levels0, srow + 1, erow, -1)
end
----@package
+---@param range Range2
---@param srow integer
---@param erow_old integer
---@param erow_new integer 0-indexed, exclusive
-function FoldInfo:edit_range(srow, erow_old, erow_new)
- if self.edits then
- self.edits[1] = math.min(srow, self.edits[1])
- if erow_old <= self.edits[2] then
- self.edits[2] = self.edits[2] + (erow_new - erow_old)
- end
- self.edits[2] = math.max(self.edits[2], erow_new)
- else
- self.edits = { srow, erow_new }
+local function edit_range(range, srow, erow_old, erow_new)
+ range[1] = math.min(srow, range[1])
+ if erow_old <= range[2] then
+ range[2] = range[2] + (erow_new - erow_old)
end
-end
-
----@package
----@return integer? srow
----@return integer? erow 0-indexed, exclusive
-function FoldInfo:flush_edit()
- if self.edits then
- local srow, erow = self.edits[1], self.edits[2]
- self.edits = nil
- return srow, erow
- end
-end
-
---- If a parser doesn't have any ranges explicitly set, treesitter will
---- return a range with end_row and end_bytes with a value of UINT32_MAX,
---- so clip end_row to the max buffer line.
----
---- TODO(lewis6991): Handle this generally
----
---- @param bufnr integer
---- @param erow integer? 0-indexed, exclusive
---- @return integer
-local function normalise_erow(bufnr, erow)
- local max_erow = api.nvim_buf_line_count(bufnr)
- return math.min(erow or max_erow, max_erow)
+ range[2] = math.max(range[2], erow_new)
end
-- TODO(lewis6991): Setup a decor provider so injections folds can be parsed
@@ -128,9 +110,9 @@ end
---@param srow integer?
---@param erow integer? 0-indexed, exclusive
---@param parse_injections? boolean
-local function get_folds_levels(bufnr, info, srow, erow, parse_injections)
+local function compute_folds_levels(bufnr, info, srow, erow, parse_injections)
srow = srow or 0
- erow = normalise_erow(bufnr, erow)
+ erow = erow or api.nvim_buf_line_count(bufnr)
local parser = ts.get_parser(bufnr)
@@ -149,27 +131,43 @@ local function get_folds_levels(bufnr, info, srow, erow, parse_injections)
-- 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 id, node, metadata in query:iter_captures(tree:root(), bufnr, math.max(srow - 1, 0), erow) do
- if query.captures[id] == 'fold' then
- local range = ts.get_range(node, bufnr, metadata[id])
- local start, _, stop, stop_col = Range.unpack4(range)
-
- 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
+ for _, match, metadata in
+ query:iter_matches(tree:root(), bufnr, math.max(srow - 1, 0), erow, { all = true })
+ 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)
+
+ for i = 2, #nodes, 1 do
+ local node_range = ts.get_range(nodes[i], bufnr, metadata[id])
+ local node_start, _, node_stop, node_stop_col = Range.unpack4(node_range)
+ if node_start < start then
+ start = node_start
+ end
+ if node_stop > stop then
+ stop = node_stop
+ stop_col = node_stop_col
+ end
+ 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
@@ -215,7 +213,7 @@ local function get_folds_levels(bufnr, info, srow, erow, parse_injections)
clamped = nestmax
end
- -- Record the "real" level, so that it can be used as "base" of later get_folds_levels().
+ -- 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)
@@ -236,18 +234,17 @@ local group = api.nvim_create_augroup('treesitter/fold', {})
---
--- Nvim usually automatically updates folds when text changes, but it doesn't work here because
--- FoldInfo update is scheduled. So we do it manually.
-local function foldupdate(bufnr)
- local function do_update()
- for _, win in ipairs(vim.fn.win_findbuf(bufnr)) do
- api.nvim_win_call(win, function()
- if vim.wo.foldmethod == 'expr' then
- vim._foldupdate()
- end
- end)
- end
+---@package
+---@param srow integer
+---@param erow integer 0-indexed, exclusive
+function FoldInfo:foldupdate(bufnr, srow, erow)
+ if self.foldupdate_range then
+ edit_range(self.foldupdate_range, srow, erow, erow)
+ else
+ self.foldupdate_range = { srow, erow }
end
- if api.nvim_get_mode().mode == 'i' then
+ if api.nvim_get_mode().mode:match('^i') then
-- foldUpdate() is guarded in insert mode. So update folds on InsertLeave
if #(api.nvim_get_autocmds({
group = group,
@@ -259,12 +256,25 @@ local function foldupdate(bufnr)
group = group,
buffer = bufnr,
once = true,
- callback = do_update,
+ callback = function()
+ self:do_foldupdate(bufnr)
+ end,
})
return
end
- do_update()
+ self:do_foldupdate(bufnr)
+end
+
+---@package
+function FoldInfo:do_foldupdate(bufnr)
+ local srow, erow = self.foldupdate_range[1], self.foldupdate_range[2]
+ self.foldupdate_range = nil
+ for _, win in ipairs(vim.fn.win_findbuf(bufnr)) do
+ if vim.wo[win].foldmethod == 'expr' then
+ vim._foldupdate(win, srow, erow)
+ end
+ end
end
--- Schedule a function only if bufnr is loaded.
@@ -272,7 +282,7 @@ end
--- * queries seem to use the old buffer state in on_bytes for some unknown reason;
--- * to avoid textlock;
--- * to avoid infinite recursion:
---- get_folds_levels → parse → _do_callback → on_changedtree → get_folds_levels.
+--- compute_folds_levels → parse → _do_callback → on_changedtree → compute_folds_levels.
---@param bufnr integer
---@param fn function
local function schedule_if_loaded(bufnr, fn)
@@ -289,16 +299,27 @@ end
---@param tree_changes Range4[]
local function on_changedtree(bufnr, foldinfo, tree_changes)
schedule_if_loaded(bufnr, function()
+ local srow_upd, erow_upd ---@type integer?, integer?
+ local max_erow = api.nvim_buf_line_count(bufnr)
for _, change in ipairs(tree_changes) do
local srow, _, erow, ecol = Range.unpack4(change)
- if ecol > 0 then
+ -- If a parser doesn't have any ranges explicitly set, treesitter will
+ -- return a range with end_row and end_bytes with a value of UINT32_MAX,
+ -- so clip end_row to the max buffer line.
+ -- TODO(lewis6991): Handle this generally
+ if erow > max_erow then
+ erow = max_erow
+ elseif ecol > 0 then
erow = erow + 1
end
-- Start from `srow - foldminlines`, because this edit may have shrunken the fold below limit.
- get_folds_levels(bufnr, foldinfo, math.max(srow - vim.wo.foldminlines, 0), erow)
+ 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
- foldupdate(bufnr)
+ foldinfo:foldupdate(bufnr, srow_upd, erow_upd)
end
end)
end
@@ -335,19 +356,29 @@ local function on_bytes(bufnr, foldinfo, start_row, start_col, old_row, old_col,
foldinfo:add_range(end_row_old, end_row_new)
end
end
- foldinfo:edit_range(start_row, end_row_old, end_row_new)
+
+ if foldinfo.on_bytes_range then
+ edit_range(foldinfo.on_bytes_range, start_row, end_row_old, end_row_new)
+ else
+ foldinfo.on_bytes_range = { start_row, end_row_new }
+ end
+ if foldinfo.foldupdate_range then
+ edit_range(foldinfo.foldupdate_range, start_row, end_row_old, end_row_new)
+ end
-- This callback must not use on_bytes arguments, because they can be outdated when the callback
-- is invoked. For example, `J` with non-zero count triggers multiple on_bytes before executing
- -- the scheduled callback. So we should collect the edits.
+ -- the scheduled callback. So we accumulate the edited ranges in `on_bytes_range`.
schedule_if_loaded(bufnr, function()
- local srow, erow = foldinfo:flush_edit()
- if not srow then
+ if not foldinfo.on_bytes_range 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.
- get_folds_levels(bufnr, foldinfo, math.max(srow - vim.wo.foldminlines, 0), erow)
- foldupdate(bufnr)
+ srow = math.max(srow - vim.wo.foldminlines, 0)
+ compute_folds_levels(bufnr, foldinfo, srow, erow)
+ foldinfo:foldupdate(bufnr, srow, erow)
end)
end
end
@@ -366,7 +397,7 @@ function M.foldexpr(lnum)
if not foldinfos[bufnr] then
foldinfos[bufnr] = FoldInfo.new()
- get_folds_levels(bufnr, foldinfos[bufnr])
+ compute_folds_levels(bufnr, foldinfos[bufnr])
parser:register_cbs({
on_changedtree = function(tree_changes)
@@ -390,10 +421,10 @@ api.nvim_create_autocmd('OptionSet', {
pattern = { 'foldminlines', 'foldnestmax' },
desc = 'Refresh treesitter folds',
callback = function()
- for _, bufnr in ipairs(vim.tbl_keys(foldinfos)) do
+ for bufnr, _ in pairs(foldinfos) do
foldinfos[bufnr] = FoldInfo.new()
- get_folds_levels(bufnr, foldinfos[bufnr])
- foldupdate(bufnr)
+ compute_folds_levels(bufnr, foldinfos[bufnr])
+ foldinfos[bufnr]:foldupdate(bufnr, 0, api.nvim_buf_line_count(bufnr))
end
end,
})
diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua
index 19d97d2820..177699a207 100644
--- a/runtime/lua/vim/treesitter/_meta.lua
+++ b/runtime/lua/vim/treesitter/_meta.lua
@@ -20,6 +20,7 @@ error('Cannot require a meta file')
---@field descendant_for_range fun(self: TSNode, start_row: integer, start_col: integer, end_row: integer, end_col: integer): TSNode?
---@field named_descendant_for_range fun(self: TSNode, start_row: integer, start_col: integer, end_row: integer, end_col: integer): TSNode?
---@field parent fun(self: TSNode): TSNode?
+---@field child_containing_descendant fun(self: TSNode, descendant: TSNode): TSNode?
---@field next_sibling fun(self: TSNode): TSNode?
---@field prev_sibling fun(self: TSNode): TSNode?
---@field next_named_sibling fun(self: TSNode): TSNode?
@@ -34,22 +35,6 @@ error('Cannot require a meta file')
---@field byte_length fun(self: TSNode): integer
local TSNode = {}
----@param query TSQuery
----@param captures true
----@param start? integer
----@param end_? integer
----@param opts? table
----@return fun(): integer, TSNode, vim.treesitter.query.TSMatch
-function TSNode:_rawquery(query, captures, start, end_, opts) end
-
----@param query TSQuery
----@param captures false
----@param start? integer
----@param end_? integer
----@param opts? table
----@return fun(): integer, vim.treesitter.query.TSMatch
-function TSNode:_rawquery(query, captures, start, end_, opts) end
-
---@alias TSLoggerCallback fun(logtype: 'parse'|'lex', msg: string)
---@class TSParser: userdata
@@ -76,9 +61,17 @@ function TSNode:_rawquery(query, captures, start, end_, opts) end
---@field captures string[]
---@field patterns table<integer, (integer|string)[][]>
+--- @param lang string
+vim._ts_inspect_language = function(lang) end
+
---@return integer
vim._ts_get_language_version = function() end
+--- @param path string
+--- @param lang string
+--- @param symbol_name? string
+vim._ts_add_language = function(path, lang, symbol_name) end
+
---@return integer
vim._ts_get_minimum_language_version = function() end
@@ -90,3 +83,31 @@ vim._ts_parse_query = function(lang, query) end
---@param lang string
---@return TSParser
vim._create_ts_parser = function(lang) end
+
+--- @class TSQueryMatch: userdata
+--- @field captures fun(self: TSQueryMatch): table<integer,TSNode[]>
+local TSQueryMatch = {}
+
+--- @return integer match_id
+--- @return integer pattern_index
+function TSQueryMatch:info() end
+
+--- @class TSQueryCursor: userdata
+--- @field remove_match fun(self: TSQueryCursor, id: integer)
+local TSQueryCursor = {}
+
+--- @return integer capture
+--- @return TSNode captured_node
+--- @return TSQueryMatch match
+function TSQueryCursor:next_capture() end
+
+--- @return TSQueryMatch match
+function TSQueryCursor:next_match() end
+
+--- @param node TSNode
+--- @param query TSQuery
+--- @param start integer?
+--- @param stop integer?
+--- @param opts? { max_start_depth?: integer, match_limit?: integer}
+--- @return TSQueryCursor
+function vim._create_ts_querycursor(node, query, start, stop, opts) end
diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua
index 6216d4e891..12b4cbc7b9 100644
--- a/runtime/lua/vim/treesitter/_query_linter.lua
+++ b/runtime/lua/vim/treesitter/_query_linter.lua
@@ -122,7 +122,7 @@ local parse = vim.func._memoize(hash_parse, function(node, buf, lang)
end)
--- @param buf integer
---- @param match vim.treesitter.query.TSMatch
+--- @param match table<integer,TSNode[]>
--- @param query vim.treesitter.Query
--- @param lang_context QueryLinterLanguageContext
--- @param diagnostics vim.Diagnostic[]
diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua
index dc2a14d238..5c91f101c0 100644
--- a/runtime/lua/vim/treesitter/dev.lua
+++ b/runtime/lua/vim/treesitter/dev.lua
@@ -226,7 +226,7 @@ function TSTreeView:draw(bufnr)
text = string.format('(%s', item.node:type())
end
else
- text = string.format('"%s"', item.node:type():gsub('\n', '\\n'):gsub('"', '\\"'))
+ text = string.format('%q', item.node:type()):gsub('\n', 'n')
end
local next = self:get(i + 1)
diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua
index a9b066d158..ed3616ef46 100644
--- a/runtime/lua/vim/treesitter/health.lua
+++ b/runtime/lua/vim/treesitter/health.lua
@@ -24,7 +24,7 @@ function M.check()
else
local lang = ts.language.inspect(parsername)
health.ok(
- string.format('Parser: %-10s ABI: %d, path: %s', parsername, lang._abi_version, parser)
+ string.format('Parser: %-20s ABI: %d, path: %s', parsername, lang._abi_version, parser)
)
end
end
diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua
index 388680259a..d2f986b874 100644
--- a/runtime/lua/vim/treesitter/highlighter.lua
+++ b/runtime/lua/vim/treesitter/highlighter.lua
@@ -4,7 +4,7 @@ local Range = require('vim.treesitter._range')
local ns = api.nvim_create_namespace('treesitter/highlighter')
----@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata
+---@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch
---@class (private) vim.treesitter.highlighter.Query
---@field private _query vim.treesitter.Query?
@@ -215,7 +215,7 @@ end
---@param start_row integer
---@param new_end integer
function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end)
- api.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1)
+ api.nvim__redraw({ buf = self.bufnr, range = { start_row, start_row + new_end + 1 } })
end
---@package
@@ -227,7 +227,7 @@ end
---@param changes Range6[]
function TSHighlighter:on_changedtree(changes)
for _, ch in ipairs(changes) do
- api.nvim__buf_redraw_range(self.bufnr, ch[1], ch[4] + 1)
+ api.nvim__redraw({ buf = self.bufnr, range = { ch[1], ch[4] + 1 } })
end
end
@@ -243,6 +243,46 @@ function TSHighlighter:get_query(lang)
return self._queries[lang]
end
+--- @param match TSQueryMatch
+--- @param bufnr integer
+--- @param capture integer
+--- @param metadata vim.treesitter.query.TSMetadata
+--- @return string?
+local function get_url(match, bufnr, capture, metadata)
+ ---@type string|number|nil
+ local url = metadata[capture] and metadata[capture].url
+
+ if not url or type(url) == 'string' then
+ return url
+ end
+
+ local captures = match:captures()
+
+ if not captures[url] then
+ return
+ end
+
+ -- Assume there is only one matching node. If there is more than one, take the URL
+ -- from the first.
+ local other_node = captures[url][1]
+
+ return vim.treesitter.get_node_text(other_node, bufnr, {
+ metadata = metadata[url],
+ })
+end
+
+--- @param capture_name string
+--- @return boolean?, integer
+local function get_spell(capture_name)
+ if capture_name == 'spell' then
+ return true, 0
+ elseif capture_name == 'nospell' then
+ -- Give nospell a higher priority so it always overrides spell captures.
+ return false, 1
+ end
+ return nil, 0
+end
+
---@param self vim.treesitter.highlighter
---@param buf integer
---@param line integer
@@ -258,12 +298,16 @@ local function on_line_impl(self, buf, line, is_spell_nav)
end
if state.iter == nil or state.next_row < line then
+ -- Mainly used to skip over folds
+
+ -- TODO(lewis6991): Creating a new iterator loses the cached predicate results for query
+ -- matches. Move this logic inside iter_captures() so we can maintain the cache.
state.iter =
state.highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1)
end
while line >= state.next_row do
- local capture, node, metadata = state.iter(line)
+ local capture, node, metadata, match = state.iter(line)
local range = { root_end_row + 1, 0, root_end_row + 1, 0 }
if node then
@@ -275,27 +319,30 @@ local function on_line_impl(self, buf, line, is_spell_nav)
local hl = state.highlighter_query:get_hl_from_capture(capture)
local capture_name = state.highlighter_query:query().captures[capture]
- local spell = nil ---@type boolean?
- if capture_name == 'spell' then
- spell = true
- elseif capture_name == 'nospell' then
- spell = false
- end
- -- Give nospell a higher priority so it always overrides spell captures.
- local spell_pri_offset = capture_name == 'nospell' and 1 or 0
+ local spell, spell_pri_offset = get_spell(capture_name)
+
+ -- The "priority" attribute can be set at the pattern level or on a particular capture
+ local priority = (
+ tonumber(metadata.priority or metadata[capture] and metadata[capture].priority)
+ or vim.highlight.priorities.treesitter
+ ) + spell_pri_offset
+
+ -- The "conceal" attribute can be set at the pattern level or on a particular capture
+ local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal
+
+ local url = get_url(match, buf, capture, metadata)
if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then
- local priority = (tonumber(metadata.priority) or vim.highlight.priorities.treesitter)
- + spell_pri_offset
api.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
hl_group = hl,
ephemeral = true,
priority = priority,
- conceal = metadata.conceal,
+ conceal = conceal,
spell = spell,
+ url = url,
})
end
end
diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua
index 47abf65332..d0a74daa6c 100644
--- a/runtime/lua/vim/treesitter/language.lua
+++ b/runtime/lua/vim/treesitter/language.lua
@@ -88,6 +88,9 @@ function M.add(lang, opts)
filetype = { filetype, { 'string', 'table' }, true },
})
+ -- parser names are assumed to be lowercase (consistent behavior on case-insensitive file systems)
+ lang = lang:lower()
+
if vim._ts_has_language(lang) then
M.register(lang, filetype)
return
diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua
index 62714d3f1b..b0812123b9 100644
--- a/runtime/lua/vim/treesitter/languagetree.lua
+++ b/runtime/lua/vim/treesitter/languagetree.lua
@@ -81,7 +81,7 @@ local TSCallbackNames = {
---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
----@field private _parent_lang? string Parent language name
+---@field private _parent? vim.treesitter.LanguageTree Parent LanguageTree
---@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.
@@ -106,9 +106,8 @@ LanguageTree.__index = LanguageTree
---@param source (integer|string) Buffer or text string to parse
---@param lang string Root language of this tree
---@param opts vim.treesitter.LanguageTree.new.Opts?
----@param parent_lang? string Parent language name of this tree
---@return vim.treesitter.LanguageTree parser object
-function LanguageTree.new(source, lang, opts, parent_lang)
+function LanguageTree.new(source, lang, opts)
language.add(lang)
opts = opts or {}
@@ -122,7 +121,6 @@ function LanguageTree.new(source, lang, opts, parent_lang)
local self = {
_source = source,
_lang = lang,
- _parent_lang = parent_lang,
_children = {},
_trees = {},
_opts = opts,
@@ -158,8 +156,10 @@ function LanguageTree:_set_logger()
local lang = self:lang()
- vim.fn.mkdir(vim.fn.stdpath('log'), 'p')
- local logfilename = vim.fs.joinpath(vim.fn.stdpath('log'), 'treesitter.log')
+ local logdir = vim.fn.stdpath('log') --[[@as string]]
+
+ vim.fn.mkdir(logdir, 'p')
+ local logfilename = vim.fs.joinpath(logdir, 'treesitter.log')
local logfile, openerr = io.open(logfilename, 'a+')
@@ -225,7 +225,10 @@ function LanguageTree:_log(...)
self._logger('nvim', table.concat(msg, ' '))
end
---- Invalidates this parser and all its children
+--- Invalidates this parser and its children.
+---
+--- Should only be called when the tracked state of the LanguageTree is not valid against the parse
+--- 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
@@ -460,24 +463,6 @@ function LanguageTree:parse(range)
return self._trees
end
----@deprecated Misleading name. Use `LanguageTree:children()` (non-recursive) instead,
---- add recursion yourself if needed.
---- Invokes the callback for each |LanguageTree| and its children recursively
----
----@param fn fun(tree: vim.treesitter.LanguageTree, lang: string)
----@param include_self boolean|nil Whether to include the invoking tree in the results
-function LanguageTree:for_each_child(fn, include_self)
- vim.deprecate('LanguageTree:for_each_child()', 'LanguageTree:children()', '0.11')
- if include_self then
- fn(self, self._lang)
- end
-
- for _, child in pairs(self._children) do
- --- @diagnostic disable-next-line:deprecated
- child:for_each_child(fn, true)
- end
-end
-
--- Invokes the callback for each |LanguageTree| recursively.
---
--- Note: This includes the invoking tree's child trees as well.
@@ -505,19 +490,25 @@ function LanguageTree:add_child(lang)
self:remove_child(lang)
end
- local child = LanguageTree.new(self._source, lang, self._opts, self:lang())
+ local child = LanguageTree.new(self._source, lang, self._opts)
-- Inherit recursive callbacks
for nm, cb in pairs(self._callbacks_rec) do
vim.list_extend(child._callbacks_rec[nm], cb)
end
+ child._parent = self
self._children[lang] = child
self:_do_callback('child_added', self._children[lang])
return self._children[lang]
end
+--- @package
+function LanguageTree:parent()
+ return self._parent
+end
+
--- Removes a child language from this |LanguageTree|.
---
---@private
@@ -752,7 +743,6 @@ local has_parser = vim.func._memoize(1, function(lang)
end)
--- Return parser name for language (if exists) or filetype (if registered and exists).
---- Also attempts with the input lower-cased.
---
---@param alias string language or filetype name
---@return string? # resolved parser name
@@ -766,19 +756,10 @@ local function resolve_lang(alias)
return alias
end
- if has_parser(alias:lower()) then
- return alias:lower()
- end
-
local lang = vim.treesitter.language.get_lang(alias)
if lang and has_parser(lang) then
return lang
end
-
- lang = vim.treesitter.language.get_lang(alias:lower())
- if lang and has_parser(lang) then
- return lang
- end
end
---@private
@@ -792,7 +773,7 @@ function LanguageTree:_get_injection(match, metadata)
local combined = metadata['injection.combined'] ~= nil
local injection_lang = metadata['injection.language'] --[[@as string?]]
local lang = metadata['injection.self'] ~= nil and self:lang()
- or metadata['injection.parent'] ~= nil and self._parent_lang
+ or metadata['injection.parent'] ~= nil and self._parent:lang()
or (injection_lang and resolve_lang(injection_lang))
local include_children = metadata['injection.include-children'] ~= nil
@@ -802,7 +783,11 @@ function LanguageTree:_get_injection(match, metadata)
-- Lang should override any other language tag
if name == 'injection.language' then
local text = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] })
- lang = resolve_lang(text)
+ lang = resolve_lang(text:lower()) -- language names are always lower case
+ elseif name == 'injection.filename' then
+ local text = vim.treesitter.get_node_text(node, self._source, { metadata = metadata[id] })
+ local ft = vim.filetype.match({ filename = text })
+ lang = ft and resolve_lang(ft)
elseif name == 'injection.content' then
ranges = get_node_ranges(node, self._source, metadata[id], include_children)
end
@@ -1054,20 +1039,19 @@ function LanguageTree:_on_detach(...)
end
end
---- Registers callbacks for the |LanguageTree|.
----@param cbs table An |nvim_buf_attach()|-like table argument with the following handlers:
---- - `on_bytes` : see |nvim_buf_attach()|, but this will be called _after_ the parsers callback.
+--- Registers callbacks for the [LanguageTree].
+---@param cbs table<TSCallbackNameOn,function> An [nvim_buf_attach()]-like table argument with the following handlers:
+--- - `on_bytes` : see [nvim_buf_attach()], but this will be called _after_ the parsers callback.
--- - `on_changedtree` : a callback that will be called every time the tree has syntactical changes.
--- It will be passed two arguments: a table of the ranges (as node ranges) that
--- changed and the changed tree.
--- - `on_child_added` : emitted when a child is added to the tree.
--- - `on_child_removed` : emitted when a child is removed from the tree.
---- - `on_detach` : emitted when the buffer is detached, see |nvim_buf_detach_event|.
+--- - `on_detach` : emitted when the buffer is detached, see [nvim_buf_detach_event].
--- Takes one argument, the number of the buffer.
--- @param recursive? boolean Apply callbacks recursively for all children. Any new children will
--- also inherit the callbacks.
function LanguageTree:register_cbs(cbs, recursive)
- ---@cast cbs table<TSCallbackNameOn,function>
if not cbs then
return
end
@@ -1091,7 +1075,14 @@ end
---@param range Range
---@return boolean
local function tree_contains(tree, range)
- return Range.contains({ tree:root():range() }, range)
+ local tree_ranges = tree:included_ranges(false)
+
+ return Range.contains({
+ tree_ranges[1][1],
+ tree_ranges[1][2],
+ tree_ranges[#tree_ranges][3],
+ tree_ranges[#tree_ranges][4],
+ }, range)
end
--- Determines whether {range} is contained in the |LanguageTree|.
@@ -1108,12 +1099,18 @@ function LanguageTree:contains(range)
return false
end
+--- @class vim.treesitter.LanguageTree.tree_for_range.Opts
+--- @inlinedoc
+---
+--- Ignore injected languages
+--- (default: `true`)
+--- @field ignore_injections? boolean
+
--- Gets the tree that contains {range}.
---
---@param range Range4 `{ start_line, start_col, end_line, end_col }`
----@param opts table|nil Optional keyword arguments:
---- - ignore_injections boolean Ignore injected languages (default true)
----@return TSTree|nil
+---@param opts? vim.treesitter.LanguageTree.tree_for_range.Opts
+---@return TSTree?
function LanguageTree:tree_for_range(range, opts)
opts = opts or {}
local ignore = vim.F.if_nil(opts.ignore_injections, true)
@@ -1139,9 +1136,8 @@ end
--- Gets the smallest named node that contains {range}.
---
---@param range Range4 `{ start_line, start_col, end_line, end_col }`
----@param opts table|nil Optional keyword arguments:
---- - ignore_injections boolean Ignore injected languages (default true)
----@return TSNode | nil Found node
+---@param opts? vim.treesitter.LanguageTree.tree_for_range.Opts
+---@return TSNode?
function LanguageTree:named_node_for_range(range, opts)
local tree = self:tree_for_range(range, opts)
if tree then
@@ -1152,7 +1148,7 @@ end
--- Gets the appropriate language that contains {range}.
---
---@param range Range4 `{ start_line, start_col, end_line, end_col }`
----@return vim.treesitter.LanguageTree Managing {range}
+---@return vim.treesitter.LanguageTree tree Managing {range}
function LanguageTree:language_for_range(range)
for _, child in pairs(self._children) do
if child:contains(range) then
diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua
index a086f5e876..ef5c2143a7 100644
--- a/runtime/lua/vim/treesitter/query.lua
+++ b/runtime/lua/vim/treesitter/query.lua
@@ -1,5 +1,6 @@
local api = vim.api
local language = require('vim.treesitter.language')
+local memoize = vim.func._memoize
local M = {}
@@ -88,7 +89,7 @@ end
---
---@param lang string Language to get query for
---@param query_name string Name of the query to load (e.g., "highlights")
----@param is_included (boolean|nil) Internal parameter, most of the time left as `nil`
+---@param is_included? boolean Internal parameter, most of the time left as `nil`
---@return string[] query_files List of files to load for given query and language
function M.get_files(lang, query_name, is_included)
local query_path = string.format('queries/%s/%s.scm', lang, query_name)
@@ -211,8 +212,8 @@ end
---@param lang string Language to use for the query
---@param query_name string Name of the query (e.g. "highlights")
---
----@return vim.treesitter.Query|nil : Parsed query. `nil` if no query files are found.
-M.get = vim.func._memoize('concat-2', function(lang, query_name)
+---@return vim.treesitter.Query? : Parsed query. `nil` if no query files are found.
+M.get = memoize('concat-2', function(lang, query_name)
if explicit_queries[lang][query_name] then
return explicit_queries[lang][query_name]
end
@@ -242,10 +243,10 @@ end)
---@param lang string Language to use for the query
---@param query string Query in s-expr syntax
---
----@return vim.treesitter.Query Parsed query
+---@return vim.treesitter.Query : Parsed query
---
----@see |vim.treesitter.query.get()|
-M.parse = vim.func._memoize('concat-2', function(lang, query)
+---@see [vim.treesitter.query.get()]
+M.parse = memoize('concat-2', function(lang, query)
language.add(lang)
local ts_query = vim._ts_parse_query(lang, query)
@@ -258,7 +259,7 @@ end)
--- handling the "any" vs "all" semantics. They are called from the
--- predicate_handlers table with the appropriate arguments for each predicate.
local impl = {
- --- @param match vim.treesitter.query.TSMatch
+ --- @param match table<integer,TSNode[]>
--- @param source integer|string
--- @param predicate any[]
--- @param any boolean
@@ -293,7 +294,7 @@ local impl = {
return not any
end,
- --- @param match vim.treesitter.query.TSMatch
+ --- @param match table<integer,TSNode[]>
--- @param source integer|string
--- @param predicate any[]
--- @param any boolean
@@ -333,7 +334,7 @@ local impl = {
end,
})
- --- @param match vim.treesitter.query.TSMatch
+ --- @param match table<integer,TSNode[]>
--- @param source integer|string
--- @param predicate any[]
--- @param any boolean
@@ -356,7 +357,7 @@ local impl = {
end
end)(),
- --- @param match vim.treesitter.query.TSMatch
+ --- @param match table<integer,TSNode[]>
--- @param source integer|string
--- @param predicate any[]
--- @param any boolean
@@ -383,13 +384,7 @@ local impl = {
end,
}
----@nodoc
----@class vim.treesitter.query.TSMatch
----@field pattern? integer
----@field active? boolean
----@field [integer] TSNode[]
-
----@alias TSPredicate fun(match: vim.treesitter.query.TSMatch, pattern: integer, source: integer|string, predicate: any[]): boolean
+---@alias TSPredicate fun(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[]): boolean
-- Predicate handler receive the following arguments
-- (match, pattern, bufnr, predicate)
@@ -462,17 +457,8 @@ local predicate_handlers = {
end
for _, node in ipairs(nodes) do
- local ancestor_types = {} --- @type table<string, boolean>
- for _, type in ipairs({ unpack(predicate, 3) }) do
- ancestor_types[type] = true
- end
-
- local cur = node:parent()
- while cur do
- if ancestor_types[cur:type()] then
- return true
- end
- cur = cur:parent()
+ if node:__has_ancestor(predicate) then
+ return true
end
end
return false
@@ -504,7 +490,7 @@ predicate_handlers['any-vim-match?'] = predicate_handlers['any-match?']
---@field [integer] vim.treesitter.query.TSMetadata
---@field [string] integer|string
----@alias TSDirective fun(match: vim.treesitter.query.TSMatch, _, _, predicate: (string|integer)[], metadata: vim.treesitter.query.TSMetadata)
+---@alias TSDirective fun(match: table<integer,TSNode[]>, _, _, predicate: (string|integer)[], metadata: vim.treesitter.query.TSMetadata)
-- Predicate handler receive the following arguments
-- (match, pattern, bufnr, predicate)
@@ -534,6 +520,9 @@ local directive_handlers = {
['offset!'] = function(match, _, _, pred, metadata)
local capture_id = pred[2] --[[@as integer]]
local nodes = match[capture_id]
+ if not nodes or #nodes == 0 then
+ return
+ end
assert(#nodes == 1, '#offset! does not support captures on multiple nodes')
local node = nodes[1]
@@ -567,6 +556,9 @@ local directive_handlers = {
assert(type(id) == 'number')
local nodes = match[id]
+ if not nodes or #nodes == 0 then
+ return
+ end
assert(#nodes == 1, '#gsub! does not support captures on multiple nodes')
local node = nodes[1]
local text = vim.treesitter.get_node_text(node, bufnr, { metadata = metadata[id] }) or ''
@@ -589,6 +581,9 @@ local directive_handlers = {
assert(type(capture_id) == 'number')
local nodes = match[capture_id]
+ if not nodes or #nodes == 0 then
+ return
+ end
assert(#nodes == 1, '#trim! does not support captures on multiple nodes')
local node = nodes[1]
@@ -618,20 +613,23 @@ local directive_handlers = {
end,
}
+--- @class vim.treesitter.query.add_predicate.Opts
+--- @inlinedoc
+---
+--- Override an existing predicate of the same name
+--- @field force? boolean
+---
+--- Use the correct implementation of the match table where capture IDs map to
+--- a list of nodes instead of a single node. Defaults to false (for backward
+--- compatibility). This option will eventually become the default and removed.
+--- @field all? boolean
+
--- Adds a new predicate to be used in queries
---
---@param name string Name of the predicate, without leading #
----@param handler function(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: table)
+---@param handler fun(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: table)
--- - see |vim.treesitter.query.add_directive()| for argument meanings
----@param opts table<string, any> Optional options:
---- - force (boolean): Override an existing
---- predicate of the same name
---- - all (boolean): Use the correct
---- implementation of the match table where
---- capture IDs map to a list of nodes instead
---- of a single node. Defaults to false (for
---- backward compatibility). This option will
---- eventually become the default and removed.
+---@param opts vim.treesitter.query.add_predicate.Opts
function M.add_predicate(name, handler, opts)
-- Backward compatibility: old signature had "force" as boolean argument
if type(opts) == 'boolean' then
@@ -669,20 +667,12 @@ end
--- metadata table `metadata[capture_id].key = value`
---
---@param name string Name of the directive, without leading #
----@param handler function(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: table)
+---@param handler fun(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: table)
--- - match: A table mapping capture IDs to a list of captured nodes
--- - pattern: the index of the matching pattern in the query file
--- - predicate: list of strings containing the full directive being called, e.g.
--- `(node (#set! conceal "-"))` would get the predicate `{ "#set!", "conceal", "-" }`
----@param opts table<string, any> Optional options:
---- - force (boolean): Override an existing
---- predicate of the same name
---- - all (boolean): Use the correct
---- implementation of the match table where
---- capture IDs map to a list of nodes instead
---- of a single node. Defaults to false (for
---- backward compatibility). This option will
---- eventually become the default and removed.
+---@param opts vim.treesitter.query.add_predicate.Opts
function M.add_directive(name, handler, opts)
-- Backward compatibility: old signature had "force" as boolean argument
if type(opts) == 'boolean' then
@@ -711,13 +701,13 @@ function M.add_directive(name, handler, opts)
end
--- Lists the currently available directives to use in queries.
----@return string[] List of supported directives.
+---@return string[] : Supported directives.
function M.list_directives()
return vim.tbl_keys(directive_handlers)
end
--- Lists the currently available predicates to use in queries.
----@return string[] List of supported predicates.
+---@return string[] : Supported predicates.
function M.list_predicates()
return vim.tbl_keys(predicate_handlers)
end
@@ -731,13 +721,19 @@ local function is_directive(name)
end
---@private
----@param match vim.treesitter.query.TSMatch
----@param pattern integer
+---@param match TSQueryMatch
---@param source integer|string
-function Query:match_preds(match, pattern, source)
+function Query:match_preds(match, source)
+ local _, pattern = match:info()
local preds = self.info.patterns[pattern]
- for _, pred in pairs(preds or {}) do
+ 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).
@@ -759,7 +755,7 @@ function Query:match_preds(match, pattern, source)
return false
end
- local pred_matches = handler(match, pattern, source, pred)
+ local pred_matches = handler(captures, pattern, source, pred)
if not xor(is_not, pred_matches) then
return false
@@ -770,30 +766,40 @@ function Query:match_preds(match, pattern, source)
end
---@private
----@param match vim.treesitter.query.TSMatch
----@param metadata vim.treesitter.query.TSMetadata
-function Query:apply_directives(match, pattern, source, metadata)
+---@param match TSQueryMatch
+---@return vim.treesitter.query.TSMetadata metadata
+function Query:apply_directives(match, source)
+ ---@type vim.treesitter.query.TSMetadata
+ local metadata = {}
+ local _, pattern = match:info()
local preds = self.info.patterns[pattern]
- for _, pred in pairs(preds or {}) do
+ 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]))
- return
end
- handler(match, pattern, source, pred, metadata)
+ handler(captures, pattern, source, pred, metadata)
end
end
+
+ return metadata
end
--- Returns the start and stop value if set else the node's range.
-- When the node's range is used, the stop is incremented by 1
-- to make the search inclusive.
----@param start integer|nil
----@param stop integer|nil
+---@param start integer?
+---@param stop integer?
---@param node TSNode
---@return integer, integer
local function value_or_node_range(start, stop, node)
@@ -807,6 +813,12 @@ 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}
---
--- {source} is needed if the query contains predicates; then the caller
@@ -816,12 +828,13 @@ end
--- 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 three values: a numeric id identifying the capture,
---- the captured node, and metadata from any directives processing the match.
+--- 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:
---
--- ```lua
---- for id, node, metadata in query:iter_captures(tree:root(), bufnr, first, last) do
+--- 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
--- -- typically useful info about the node:
--- local type = node:type() -- type of the captured node
@@ -835,8 +848,10 @@ 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):
---- capture id, capture node, metadata
+---@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch):
+--- capture id, capture node, metadata, match
+---
+---@note Captures are only returned if the query pattern of a specific capture contained predicates.
function Query:iter_captures(node, source, start, stop)
if type(source) == 'number' and source == 0 then
source = api.nvim_get_current_buf()
@@ -844,24 +859,30 @@ function Query:iter_captures(node, source, start, stop)
start, stop = value_or_node_range(start, stop, node)
- local raw_iter = node:_rawquery(self.query, true, start, stop) ---@type fun(): integer, TSNode, vim.treesitter.query.TSMatch
+ 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)
+
local function iter(end_line)
- local capture, captured_node, match = raw_iter()
- local metadata = {}
-
- if match ~= nil then
- local active = self:match_preds(match, match.pattern, source)
- match.active = active
- if not active then
- if end_line and captured_node:range() > end_line then
- return nil, captured_node, nil
- end
- return iter(end_line) -- tail call: try next match
- end
+ local capture, captured_node, match = cursor:next_capture()
- self:apply_directives(match, match.pattern, source, metadata)
+ if not capture then
+ return
end
- return capture, captured_node, metadata
+
+ 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
+ end
+
+ local metadata = apply_directives(self, match, source)
+
+ return capture, captured_node, metadata, match
end
return iter
end
@@ -903,45 +924,55 @@ end
---@param opts? table Optional keyword arguments:
--- - max_start_depth (integer) if non-zero, sets the maximum start depth
--- for each match. This is used to prevent traversing too deep into a tree.
+--- - match_limit (integer) Set the maximum number of in-progress matches (Default: 256).
--- - all (boolean) When set, the returned match table maps capture IDs to a list of nodes.
--- Older versions of iter_matches incorrectly mapped capture IDs to a single node, which is
--- incorrect behavior. This option will eventually become the default and removed.
---
---@return (fun(): integer, table<integer, TSNode[]>, table): pattern id, match, metadata
function Query:iter_matches(node, source, start, stop, opts)
- local all = opts and opts.all
+ opts = opts or {}
+ opts.match_limit = opts.match_limit or 256
+
if type(source) == 'number' and source == 0 then
source = api.nvim_get_current_buf()
end
start, stop = value_or_node_range(start, stop, node)
- local raw_iter = node:_rawquery(self.query, false, start, stop, opts) ---@type fun(): integer, vim.treesitter.query.TSMatch
+ local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts)
+
local function iter()
- local pattern, match = raw_iter()
- local metadata = {}
+ local match = cursor:next_match()
- if match ~= nil then
- local active = self:match_preds(match, pattern, source)
- if not active then
- return iter() -- tail call: try next match
- end
+ if not match then
+ return
+ end
- self:apply_directives(match, pattern, source, metadata)
+ local match_id, pattern = match:info()
+
+ if not self:match_preds(match, source) then
+ cursor:remove_match(match_id)
+ return iter() -- tail call: try next match
end
- if not all then
+ local metadata = self:apply_directives(match, source)
+
+ local captures = match:captures()
+
+ if not opts.all then
-- Convert the match table into the old buggy version for backward
-- compatibility. This is slow. Plugin authors, if you're reading this, set the "all"
-- option!
local old_match = {} ---@type table<integer, TSNode>
- for k, v in pairs(match or {}) do
+ for k, v in pairs(captures or {}) do
old_match[k] = v[#v]
end
return pattern, old_match, metadata
end
- return pattern, match, metadata
+ -- TODO(lewis6991): create a new function that returns {match, metadata}
+ return pattern, captures, metadata
end
return iter
end
diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua
index b0e7ca1a35..99b9b78e2a 100644
--- a/runtime/lua/vim/ui.lua
+++ b/runtime/lua/vim/ui.lua
@@ -114,15 +114,20 @@ end
--- Examples:
---
--- ```lua
+--- -- Asynchronous.
--- vim.ui.open("https://neovim.io/")
--- vim.ui.open("~/path/to/file")
---- vim.ui.open("$VIMRUNTIME")
+--- -- Synchronous (wait until the process exits).
+--- local cmd, err = vim.ui.open("$VIMRUNTIME")
+--- if cmd then
+--- cmd:wait()
+--- end
--- ```
---
---@param path string Path or URL to open
---
----@return vim.SystemCompleted|nil # Command result, or nil if not found.
----@return string|nil # Error message on failure
+---@return vim.SystemObj|nil # Command object, or nil if not found.
+---@return nil|string # Error message on failure, or nil on success.
---
---@see |vim.system()|
function M.open(path)
@@ -144,21 +149,37 @@ function M.open(path)
else
return nil, 'vim.ui.open: rundll32 not found'
end
+ elseif vim.fn.executable('wslview') == 1 then
+ cmd = { 'wslview', path }
elseif vim.fn.executable('explorer.exe') == 1 then
cmd = { 'explorer.exe', path }
elseif vim.fn.executable('xdg-open') == 1 then
cmd = { 'xdg-open', path }
else
- return nil, 'vim.ui.open: no handler found (tried: explorer.exe, xdg-open)'
+ return nil, 'vim.ui.open: no handler found (tried: wslview, explorer.exe, xdg-open)'
end
- local rv = vim.system(cmd, { text = true, detach = true }):wait()
- if rv.code ~= 0 then
- local msg = ('vim.ui.open: command failed (%d): %s'):format(rv.code, vim.inspect(cmd))
- return rv, msg
- end
+ return vim.system(cmd, { text = true, detach = true }), nil
+end
- return rv, nil
+--- Gets the URL at cursor, if any.
+function M._get_url()
+ if vim.bo.filetype == 'markdown' then
+ local range = vim.api.nvim_win_get_cursor(0)
+ vim.treesitter.get_parser():parse(range)
+ -- marking the node as `markdown_inline` is required. Setting it to `markdown` does not
+ -- work.
+ local current_node = vim.treesitter.get_node { lang = 'markdown_inline' }
+ while current_node do
+ local type = current_node:type()
+ if type == 'inline_link' or type == 'image' then
+ local child = assert(current_node:named_child(1))
+ return vim.treesitter.get_node_text(child, 0)
+ end
+ current_node = current_node:parent()
+ end
+ end
+ return vim.fn.expand('<cfile>')
end
return M