diff options
author | Josh Rahm <joshuarahm@gmail.com> | 2024-05-24 19:18:11 +0000 |
---|---|---|
committer | Josh Rahm <joshuarahm@gmail.com> | 2024-05-24 19:18:11 +0000 |
commit | ff7ed8f586589d620a806c3758fac4a47a8e7e15 (patch) | |
tree | 729bbcb92231538fa61dab6c3d890b025484b7f5 /runtime/lua/vim | |
parent | 376914f419eb08fdf4c1a63a77e1f035898a0f10 (diff) | |
parent | 28c04948a1c887a1cc0cb64de79fa32631700466 (diff) | |
download | rneovim-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')
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 |