diff options
Diffstat (limited to 'runtime/lua/vim')
56 files changed, 3050 insertions, 2764 deletions
diff --git a/runtime/lua/vim/_buf.lua b/runtime/lua/vim/_buf.lua new file mode 100644 index 0000000000..0631c96f77 --- /dev/null +++ b/runtime/lua/vim/_buf.lua @@ -0,0 +1,23 @@ +local M = {} + +--- Adds one or more blank lines above or below the cursor. +-- TODO: move to _defaults.lua once it is possible to assign a Lua function to options #25672 +--- @param above? boolean Place blank line(s) above the cursor +local function add_blank(above) + local offset = above and 1 or 0 + local repeated = vim.fn['repeat']({ '' }, vim.v.count1) + local linenr = vim.api.nvim_win_get_cursor(0)[1] + vim.api.nvim_buf_set_lines(0, linenr - offset, linenr - offset, true, repeated) +end + +-- TODO: move to _defaults.lua once it is possible to assign a Lua function to options #25672 +function M.space_above() + add_blank(true) +end + +-- TODO: move to _defaults.lua once it is possible to assign a Lua function to options #25672 +function M.space_below() + add_blank() +end + +return M diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index 6cad1dbca9..06f6ed6829 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -157,7 +157,7 @@ do --- 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|. + --- See |grr|, |grn|, |gra|, |gri|, |gO|, |i_CTRL-S|. do vim.keymap.set('n', 'grn', function() vim.lsp.buf.rename() @@ -171,7 +171,15 @@ do vim.lsp.buf.references() end, { desc = 'vim.lsp.buf.references()' }) - vim.keymap.set('i', '<C-S>', function() + vim.keymap.set('n', 'gri', function() + vim.lsp.buf.implementation() + end, { desc = 'vim.lsp.buf.implementation()' }) + + vim.keymap.set('n', 'gO', function() + vim.lsp.buf.document_symbol() + end, { desc = 'vim.lsp.buf.document_symbol()' }) + + vim.keymap.set({ 'i', 's' }, '<C-S>', function() vim.lsp.buf.signature_help() end, { desc = 'vim.lsp.buf.signature_help()' }) end @@ -211,173 +219,163 @@ do --- vim-unimpaired style mappings. See: https://github.com/tpope/vim-unimpaired do + --- Execute a command and print errors without a stacktrace. + --- @param opts table Arguments to |nvim_cmd()| + local function cmd(opts) + local _, err = pcall(vim.api.nvim_cmd, opts, {}) + if err then + vim.api.nvim_err_writeln(err:sub(#'Vim:' + 1)) + end + end + -- Quickfix mappings vim.keymap.set('n', '[q', function() - vim.cmd.cprevious({ count = vim.v.count1 }) - end, { - desc = ':cprevious', - }) + cmd({ cmd = 'cprevious', count = vim.v.count1 }) + end, { desc = ':cprevious' }) vim.keymap.set('n', ']q', function() - vim.cmd.cnext({ count = vim.v.count1 }) - end, { - desc = ':cnext', - }) + cmd({ cmd = 'cnext', count = vim.v.count1 }) + end, { desc = ':cnext' }) vim.keymap.set('n', '[Q', function() - vim.cmd.crewind({ count = vim.v.count ~= 0 and vim.v.count or nil }) - end, { - desc = ':crewind', - }) + cmd({ cmd = 'crewind', count = vim.v.count ~= 0 and vim.v.count or nil }) + end, { desc = ':crewind' }) vim.keymap.set('n', ']Q', function() - vim.cmd.clast({ count = vim.v.count ~= 0 and vim.v.count or nil }) - end, { - desc = ':clast', - }) + cmd({ cmd = 'clast', count = vim.v.count ~= 0 and vim.v.count or nil }) + end, { desc = ':clast' }) vim.keymap.set('n', '[<C-Q>', function() - vim.cmd.cpfile({ count = vim.v.count1 }) - end, { - desc = ':cpfile', - }) + cmd({ cmd = 'cpfile', count = vim.v.count1 }) + end, { desc = ':cpfile' }) vim.keymap.set('n', ']<C-Q>', function() - vim.cmd.cnfile({ count = vim.v.count1 }) - end, { - desc = ':cnfile', - }) + cmd({ cmd = 'cnfile', count = vim.v.count1 }) + end, { desc = ':cnfile' }) -- Location list mappings vim.keymap.set('n', '[l', function() - vim.cmd.lprevious({ count = vim.v.count1 }) - end, { - desc = ':lprevious', - }) + cmd({ cmd = 'lprevious', count = vim.v.count1 }) + end, { desc = ':lprevious' }) vim.keymap.set('n', ']l', function() - vim.cmd.lnext({ count = vim.v.count1 }) - end, { - desc = ':lnext', - }) + cmd({ cmd = 'lnext', count = vim.v.count1 }) + end, { desc = ':lnext' }) vim.keymap.set('n', '[L', function() - vim.cmd.lrewind({ count = vim.v.count ~= 0 and vim.v.count or nil }) - end, { - desc = ':lrewind', - }) + cmd({ cmd = 'lrewind', count = vim.v.count ~= 0 and vim.v.count or nil }) + end, { desc = ':lrewind' }) vim.keymap.set('n', ']L', function() - vim.cmd.llast({ count = vim.v.count ~= 0 and vim.v.count or nil }) - end, { - desc = ':llast', - }) + cmd({ cmd = 'llast', count = vim.v.count ~= 0 and vim.v.count or nil }) + end, { desc = ':llast' }) vim.keymap.set('n', '[<C-L>', function() - vim.cmd.lpfile({ count = vim.v.count1 }) - end, { - desc = ':lpfile', - }) + cmd({ cmd = 'lpfile', count = vim.v.count1 }) + end, { desc = ':lpfile' }) vim.keymap.set('n', ']<C-L>', function() - vim.cmd.lnfile({ count = vim.v.count1 }) - end, { - desc = ':lnfile', - }) + cmd({ cmd = 'lnfile', count = vim.v.count1 }) + end, { desc = ':lnfile' }) -- Argument list vim.keymap.set('n', '[a', function() - vim.cmd.previous({ count = vim.v.count1 }) - end, { - desc = ':previous', - }) + cmd({ cmd = 'previous', count = vim.v.count1 }) + end, { desc = ':previous' }) vim.keymap.set('n', ']a', function() -- count doesn't work with :next, must use range. See #30641. - vim.cmd.next({ range = { vim.v.count1 } }) - end, { - desc = ':next', - }) + cmd({ cmd = 'next', range = { vim.v.count1 } }) + end, { desc = ':next' }) vim.keymap.set('n', '[A', function() if vim.v.count ~= 0 then - vim.cmd.argument({ count = vim.v.count }) + cmd({ cmd = 'argument', count = vim.v.count }) else - vim.cmd.rewind() + cmd({ cmd = 'rewind' }) end - end, { - desc = ':rewind', - }) + end, { desc = ':rewind' }) vim.keymap.set('n', ']A', function() if vim.v.count ~= 0 then - vim.cmd.argument({ count = vim.v.count }) + cmd({ cmd = 'argument', count = vim.v.count }) else - vim.cmd.last() + cmd({ cmd = 'last' }) end - end, { - desc = ':last', - }) + end, { desc = ':last' }) -- Tags vim.keymap.set('n', '[t', function() -- count doesn't work with :tprevious, must use range. See #30641. - vim.cmd.tprevious({ range = { vim.v.count1 } }) + cmd({ cmd = 'tprevious', range = { vim.v.count1 } }) end, { desc = ':tprevious' }) vim.keymap.set('n', ']t', function() -- count doesn't work with :tnext, must use range. See #30641. - vim.cmd.tnext({ range = { vim.v.count1 } }) + cmd({ cmd = 'tnext', range = { vim.v.count1 } }) end, { desc = ':tnext' }) vim.keymap.set('n', '[T', function() -- count doesn't work with :trewind, must use range. See #30641. - vim.cmd.trewind({ range = vim.v.count ~= 0 and { vim.v.count } or nil }) + cmd({ cmd = 'trewind', range = vim.v.count ~= 0 and { vim.v.count } or nil }) end, { desc = ':trewind' }) vim.keymap.set('n', ']T', function() -- :tlast does not accept a count, so use :trewind if count given if vim.v.count ~= 0 then - vim.cmd.trewind({ range = { vim.v.count } }) + cmd({ cmd = 'trewind', range = { vim.v.count } }) else - vim.cmd.tlast() + cmd({ cmd = 'tlast' }) end end, { desc = ':tlast' }) vim.keymap.set('n', '[<C-T>', function() -- count doesn't work with :ptprevious, must use range. See #30641. - vim.cmd.ptprevious({ range = { vim.v.count1 } }) + cmd({ cmd = 'ptprevious', range = { vim.v.count1 } }) end, { desc = ' :ptprevious' }) vim.keymap.set('n', ']<C-T>', function() -- count doesn't work with :ptnext, must use range. See #30641. - vim.cmd.ptnext({ range = { vim.v.count1 } }) + cmd({ cmd = 'ptnext', range = { vim.v.count1 } }) end, { desc = ':ptnext' }) -- Buffers vim.keymap.set('n', '[b', function() - vim.cmd.bprevious({ count = vim.v.count1 }) + cmd({ cmd = 'bprevious', count = vim.v.count1 }) end, { desc = ':bprevious' }) vim.keymap.set('n', ']b', function() - vim.cmd.bnext({ count = vim.v.count1 }) + cmd({ cmd = 'bnext', count = vim.v.count1 }) end, { desc = ':bnext' }) vim.keymap.set('n', '[B', function() if vim.v.count ~= 0 then - vim.cmd.buffer({ count = vim.v.count }) + cmd({ cmd = 'buffer', count = vim.v.count }) else - vim.cmd.brewind() + cmd({ cmd = 'brewind' }) end end, { desc = ':brewind' }) vim.keymap.set('n', ']B', function() if vim.v.count ~= 0 then - vim.cmd.buffer({ count = vim.v.count }) + cmd({ cmd = 'buffer', count = vim.v.count }) else - vim.cmd.blast() + cmd({ cmd = 'blast' }) end end, { desc = ':blast' }) + + -- Add empty lines + vim.keymap.set('n', '[<Space>', function() + -- TODO: update once it is possible to assign a Lua function to options #25672 + vim.go.operatorfunc = "v:lua.require'vim._buf'.space_above" + return 'g@l' + end, { expr = true, desc = 'Add empty line above cursor' }) + + vim.keymap.set('n', ']<Space>', function() + -- TODO: update once it is possible to assign a Lua function to options #25672 + vim.go.operatorfunc = "v:lua.require'vim._buf'.space_below" + return 'g@l' + end, { expr = true, desc = 'Add empty line below cursor' }) end end diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua index 2e829578a7..44f17b3f85 100644 --- a/runtime/lua/vim/_editor.lua +++ b/runtime/lua/vim/_editor.lua @@ -32,7 +32,7 @@ for k, v in pairs({ func = true, F = true, lsp = true, - highlight = true, + hl = true, diagnostic = true, keymap = true, ui = true, @@ -68,6 +68,12 @@ vim.log = { }, } +local utfs = { + ['utf-8'] = true, + ['utf-16'] = true, + ['utf-32'] = true, +} + -- TODO(lewis6991): document that the signature is system({cmd}, [{opts},] {on_exit}) --- Runs a system command or throws an error if {cmd} cannot be run. --- @@ -467,15 +473,11 @@ vim.cmd = setmetatable({}, { -- These are the vim.env/v/g/o/bo/wo variable magic accessors. do - local validate = vim.validate - --- @param scope string --- @param handle? false|integer --- @return vim.var_accessor local function make_dict_accessor(scope, handle) - validate({ - scope = { scope, 's' }, - }) + vim.validate('scope', scope, 'string') local mt = {} function mt:__newindex(k, v) return vim._setvar(scope, handle or 0, k, v) @@ -543,7 +545,7 @@ function vim.region(bufnr, pos1, pos2, regtype, inclusive) -- TODO: handle double-width characters if regtype:byte() == 22 then local bufline = vim.api.nvim_buf_get_lines(bufnr, pos1[1], pos1[1] + 1, true)[1] - pos1[2] = vim.str_utfindex(bufline, pos1[2]) + pos1[2] = vim.str_utfindex(bufline, 'utf-32', pos1[2]) end local region = {} @@ -555,14 +557,14 @@ function vim.region(bufnr, pos1, pos2, regtype, inclusive) c2 = c1 + tonumber(regtype:sub(2)) -- and adjust for non-ASCII characters local bufline = vim.api.nvim_buf_get_lines(bufnr, l, l + 1, true)[1] - local utflen = vim.str_utfindex(bufline, #bufline) + local utflen = vim.str_utfindex(bufline, 'utf-32', #bufline) if c1 <= utflen then - c1 = assert(tonumber(vim.str_byteindex(bufline, c1))) + c1 = assert(tonumber(vim.str_byteindex(bufline, 'utf-32', c1))) else c1 = #bufline + 1 end if c2 <= utflen then - c2 = assert(tonumber(vim.str_byteindex(bufline, c2))) + c2 = assert(tonumber(vim.str_byteindex(bufline, 'utf-32', c2))) else c2 = #bufline + 1 end @@ -591,7 +593,7 @@ end ---@param timeout integer Number of milliseconds to wait before calling `fn` ---@return table timer luv timer object function vim.defer_fn(fn, timeout) - vim.validate({ fn = { fn, 'c', true } }) + vim.validate('fn', fn, 'callable', true) local timer = assert(vim.uv.new_timer()) timer:start( timeout, @@ -649,7 +651,7 @@ do end end -local on_key_cbs = {} --- @type table<integer,function> +local on_key_cbs = {} --- @type table<integer,[function, table]> --- Adds Lua function {fn} with namespace id {ns_id} as a listener to every, --- yes every, input key. @@ -658,64 +660,225 @@ local on_key_cbs = {} --- @type table<integer,function> --- and cannot be toggled dynamically. --- ---@note {fn} will be removed on error. +---@note {fn} won't be invoked recursively, i.e. if {fn} itself consumes input, +--- it won't be invoked for those keys. ---@note {fn} will not be cleared by |nvim_buf_clear_namespace()| --- ----@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 fn nil|fun(key: string, typed: string): string? Function invoked for every input key, +--- after mappings have been applied but before further processing. Arguments +--- {key} and {typed} are raw keycodes, where {key} is the key after mappings +--- are applied, and {typed} is the key(s) before mappings are applied. +--- {typed} may be empty if {key} is produced by non-typed key(s) or by the +--- same typed key(s) that produced a previous {key}. +--- If {fn} returns an empty string, {key} is discarded/ignored. +--- When {fn} is `nil`, 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. +--- new |nvim_create_namespace()| id. +---@param opts table? Optional parameters +--- +---@see |keytrans()| --- ---@return integer Namespace id associated with {fn}. Or count of all callbacks ---if on_key() is called without arguments. -function vim.on_key(fn, ns_id) +function vim.on_key(fn, ns_id, opts) if fn == nil and ns_id == nil then return vim.tbl_count(on_key_cbs) end - vim.validate({ - fn = { fn, 'c', true }, - ns_id = { ns_id, 'n', true }, - }) + vim.validate('fn', fn, 'callable', true) + vim.validate('ns_id', ns_id, 'number', true) + vim.validate('opts', opts, 'table', true) + opts = opts or {} if ns_id == nil or ns_id == 0 then ns_id = vim.api.nvim_create_namespace('') end - on_key_cbs[ns_id] = fn + on_key_cbs[ns_id] = fn and { fn, opts } return ns_id end --- Executes the on_key callbacks. ---@private function vim._on_key(buf, typed_buf) - local failed_ns_ids = {} - local failed_messages = {} + local failed = {} ---@type [integer, string][] + local discard = false for k, v in pairs(on_key_cbs) do - local ok, err_msg = pcall(v, buf, typed_buf) + local fn = v[1] + local ok, rv = xpcall(function() + return fn(buf, typed_buf) + end, debug.traceback) + if ok and rv ~= nil then + if type(rv) == 'string' and #rv == 0 then + discard = true + -- break -- Without break deliver to all callbacks even when it eventually discards. + -- "break" does not make sense unless callbacks are sorted by ???. + else + ok = false + rv = 'return string must be empty' + end + end if not ok then vim.on_key(nil, k) - table.insert(failed_ns_ids, k) - table.insert(failed_messages, err_msg) + table.insert(failed, { k, rv }) + end + end + + if #failed > 0 then + local errmsg = '' + for _, v in ipairs(failed) do + errmsg = errmsg .. string.format('\nWith ns_id %d: %s', v[1], v[2]) + end + error(errmsg) + end + return discard +end + +--- Convert UTF-32, UTF-16 or UTF-8 {index} to byte index. +--- If {strict_indexing} is false +--- then then an out of range index will return byte length +--- instead of throwing an error. +--- +--- Invalid UTF-8 and NUL is treated like in |vim.str_utfindex()|. +--- An {index} in the middle of a UTF-16 sequence is rounded upwards to +--- the end of that sequence. +---@param s string +---@param encoding "utf-8"|"utf-16"|"utf-32" +---@param index integer +---@param strict_indexing? boolean # default: true +---@return integer +function vim.str_byteindex(s, encoding, index, strict_indexing) + if type(encoding) == 'number' then + -- Legacy support for old API + -- Parameters: ~ + -- • {str} (`string`) + -- • {index} (`integer`) + -- • {use_utf16} (`boolean?`) + vim.deprecate( + 'vim.str_byteindex', + 'vim.str_byteindex(s, encoding, index, strict_indexing)', + '1.0' + ) + local old_index = encoding + local use_utf16 = index or false + return vim._str_byteindex(s, old_index, use_utf16) or error('index out of range') + end + + -- Avoid vim.validate for performance. + if type(s) ~= 'string' or type(index) ~= 'number' then + vim.validate('s', s, 'string') + vim.validate('index', index, 'number') + end + + local len = #s + + if index == 0 or len == 0 then + return 0 + end + + if not utfs[encoding] then + vim.validate('encoding', encoding, function(v) + return utfs[v], 'invalid encoding' + end) + end + + if strict_indexing ~= nil and type(strict_indexing) ~= 'boolean' then + vim.validate('strict_indexing', strict_indexing, 'boolean', true) + end + if strict_indexing == nil then + strict_indexing = true + end + + if encoding == 'utf-8' then + if index > len then + return strict_indexing and error('index out of range') or len end + return index end + return vim._str_byteindex(s, index, encoding == 'utf-16') + or strict_indexing and error('index out of range') + or len +end - if failed_ns_ids[1] then - error( - string.format( - "Error executing 'on_key' with ns_ids '%s'\n Messages: %s", - table.concat(failed_ns_ids, ', '), - table.concat(failed_messages, '\n') - ) +--- Convert byte index to UTF-32, UTF-16 or UTF-8 indices. If {index} is not +--- supplied, the length of the string is used. All indices are zero-based. +--- +--- If {strict_indexing} is false then an out of range index will return string +--- length instead of throwing an error. +--- Invalid UTF-8 bytes, and embedded surrogates are counted as one code point +--- each. An {index} in the middle of a UTF-8 sequence is rounded upwards to the end of +--- that sequence. +---@param s string +---@param encoding "utf-8"|"utf-16"|"utf-32" +---@param index? integer +---@param strict_indexing? boolean # default: true +---@return integer +function vim.str_utfindex(s, encoding, index, strict_indexing) + if encoding == nil or type(encoding) == 'number' then + -- Legacy support for old API + -- Parameters: ~ + -- • {str} (`string`) + -- • {index} (`integer?`) + vim.deprecate( + 'vim.str_utfindex', + 'vim.str_utfindex(s, encoding, index, strict_indexing)', + '1.0' ) + local old_index = encoding + local col32, col16 = vim._str_utfindex(s, old_index) --[[@as integer,integer]] + if not col32 or not col16 then + error('index out of range') + end + -- Return (multiple): ~ + -- (`integer`) UTF-32 index + -- (`integer`) UTF-16 index + return col32, col16 + end + + if type(s) ~= 'string' or (index ~= nil and type(index) ~= 'number') then + vim.validate('s', s, 'string') + vim.validate('index', index, 'number', true) + end + + if not index then + index = math.huge + strict_indexing = false + end + + if index == 0 then + return 0 + end + + if not utfs[encoding] then + vim.validate('encoding', encoding, function(v) + return utfs[v], 'invalid encoding' + end) end + + if strict_indexing ~= nil and type(strict_indexing) ~= 'boolean' then + vim.validate('strict_indexing', strict_indexing, 'boolean', true) + end + if strict_indexing == nil then + strict_indexing = true + end + + if encoding == 'utf-8' then + local len = #s + return index <= len and index or (strict_indexing and error('index out of range') or len) + end + local col32, col16 = vim._str_utfindex(s, index) --[[@as integer?,integer?]] + local col = encoding == 'utf-16' and col16 or col32 + if col then + return col + end + if strict_indexing then + error('index out of range') + end + local max32, max16 = vim._str_utfindex(s)--[[@as integer integer]] + return encoding == 'utf-16' and max16 or max32 end ---- Generates a list of possible completions for the string. +--- Generates a list of possible completions for the str --- String has the pattern. --- --- 1. Can we get it to just return things in the global namespace with that name prefix @@ -988,21 +1151,16 @@ end --- @param ... any --- @return any # given arguments. function vim.print(...) - if vim.in_fast_event() then - print(...) - return ... - end - + local msg = {} for i = 1, select('#', ...) do local o = select(i, ...) if type(o) == 'string' then - vim.api.nvim_out_write(o) + table.insert(msg, o) else - vim.api.nvim_out_write(vim.inspect(o, { newline = '\n', indent = ' ' })) + table.insert(msg, vim.inspect(o, { newline = '\n', indent = ' ' })) end - vim.api.nvim_out_write('\n') end - + print(table.concat(msg, '\n')) return ... end @@ -1146,16 +1304,22 @@ function vim.deprecate(name, alternative, version, plugin, backtrace) if plugin == 'Nvim' then 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 + -- Show a warning only if feature is hard-deprecated (see 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 nvim_major = 0 --- Current Nvim major version. + + -- We can't "subtract" from a major version, so: + -- * Always treat `major > nvim_major` as soft-deprecation. + -- * Compare `minor - 1` if `major == nvim_major`. + if major > nvim_major then + return -- Always soft-deprecation (see MAINTAIN.md). + end 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 + if major == nvim_major and vim.fn.has(hard_deprecated_since) == 0 then return end @@ -1166,12 +1330,10 @@ function vim.deprecate(name, alternative, version, plugin, backtrace) 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 }, - } + vim.validate('name', name, 'string') + vim.validate('alternative', alternative, 'string', true) + vim.validate('version', version, 'string', true) + vim.validate('plugin', plugin, 'string', true) local msg = ('%s is deprecated'):format(name) msg = alternative and ('%s, use %s instead.'):format(msg, alternative) or (msg .. '.') @@ -1190,4 +1352,7 @@ require('vim._options') ---@deprecated vim.loop = vim.uv +-- Deprecated. Remove at Nvim 2.0 +vim.highlight = vim._defer_deprecated_module('vim.highlight', 'vim.hl') + return vim diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua index c9f207cb20..f8672d1f1f 100644 --- a/runtime/lua/vim/_meta.lua +++ b/runtime/lua/vim/_meta.lua @@ -15,7 +15,7 @@ vim.fs = require('vim.fs') vim.func = require('vim.func') vim.glob = require('vim.glob') vim.health = require('vim.health') -vim.highlight = require('vim.highlight') +vim.hl = require('vim.hl') vim.iter = require('vim.iter') vim.keymap = require('vim.keymap') vim.loader = require('vim.loader') diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua index c66b295d3a..3c9b9d4f44 100644 --- a/runtime/lua/vim/_meta/api.lua +++ b/runtime/lua/vim/_meta/api.lua @@ -592,8 +592,9 @@ function vim.api.nvim_buf_line_count(buffer) end --- - id : id of the extmark to edit. --- - end_row : ending line of the mark, 0-based inclusive. --- - end_col : ending col of the mark, 0-based exclusive. ---- - hl_group : name of the highlight group used to highlight ---- this mark. +--- - hl_group : highlight group used for the text range. This and below +--- highlight groups can be supplied either as a string or as an integer, +--- the latter of which can be obtained using `nvim_get_hl_id_by_name()`. --- - hl_eol : when true, for a multiline highlight covering the --- EOL of a line, continue the highlight for the rest --- of the screen line (just like for diff and @@ -603,9 +604,7 @@ function vim.api.nvim_buf_line_count(buffer) end --- text chunk with specified highlight. `highlight` element --- can either be a single highlight group, or an array of --- multiple highlight groups that will be stacked ---- (highest priority last). A highlight group can be supplied ---- either as a string or as an integer, the latter which ---- can be obtained using `nvim_get_hl_id_by_name()`. +--- (highest priority last). --- - virt_text_pos : position of virtual text. Possible values: --- - "eol": right after eol character (default). --- - "overlay": display over the specified column, without @@ -676,15 +675,12 @@ function vim.api.nvim_buf_line_count(buffer) end --- buffer or end of the line respectively. Defaults to true. --- - sign_text: string of length 1-2 used to display in the --- sign column. ---- - sign_hl_group: name of the highlight group used to ---- highlight the sign column text. ---- - number_hl_group: name of the highlight group used to ---- highlight the number column. ---- - line_hl_group: name of the highlight group used to ---- highlight the whole line. ---- - cursorline_hl_group: name of the highlight group used to ---- highlight the sign column text when the cursor is on ---- the same line as the mark and 'cursorline' is enabled. +--- - sign_hl_group: highlight group used for the sign column text. +--- - number_hl_group: highlight group used for the number column. +--- - line_hl_group: highlight group used for the whole line. +--- - cursorline_hl_group: highlight group used for the sign +--- column text when the cursor is on the same line as the +--- mark and 'cursorline' is enabled. --- - conceal: string which should be either empty or a single --- character. Enable concealing similar to `:syn-conceal`. --- When a character is supplied it is used as `:syn-cchar`. @@ -1106,12 +1102,12 @@ function vim.api.nvim_del_var(name) end --- Echo a message. --- --- @param chunks any[] A list of `[text, hl_group]` arrays, each representing a ---- text chunk with specified highlight. `hl_group` element ---- can be omitted for no highlight. +--- text chunk with specified highlight group name or ID. +--- `hl_group` element can be omitted for no highlight. --- @param history boolean if true, add to `message-history`. --- @param opts vim.api.keyset.echo_opts Optional parameters. ---- - verbose: Message was printed as a result of 'verbose' option ---- if Nvim was invoked with -V3log_file, the message will be +--- - verbose: Message is printed as a result of 'verbose' option. +--- If Nvim was invoked with -V3log_file, the message will be --- redirected to the log_file and suppressed from direct output. function vim.api.nvim_echo(chunks, history, opts) end @@ -1767,7 +1763,12 @@ function vim.api.nvim_open_term(buffer, opts) end --- fractional. --- - focusable: Enable focus by user actions (wincmds, mouse events). --- Defaults to true. Non-focusable windows can be entered by ---- `nvim_set_current_win()`. +--- `nvim_set_current_win()`, or, when the `mouse` field is set to true, +--- by mouse events. See `focusable`. +--- - mouse: Specify how this window interacts with mouse events. +--- Defaults to `focusable` value. +--- - If false, mouse events pass through this window. +--- - If true, mouse events interact with this window normally. --- - external: GUI should display the window as an external --- top-level window. Currently accepts no other positioning --- configuration together with this. diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua index 2fe5c32faf..bf184dee2d 100644 --- a/runtime/lua/vim/_meta/api_keysets.lua +++ b/runtime/lua/vim/_meta/api_keysets.lua @@ -295,6 +295,7 @@ error('Cannot require a meta file') --- @field bufpos? any[] --- @field external? boolean --- @field focusable? boolean +--- @field mouse? boolean --- @field vertical? boolean --- @field zindex? integer --- @field border? any diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua index 13bd1c1294..b8779b66fe 100644 --- a/runtime/lua/vim/_meta/builtin.lua +++ b/runtime/lua/vim/_meta/builtin.lua @@ -112,18 +112,6 @@ function vim.rpcrequest(channel, method, ...) end --- equal, {a} is greater than {b} or {a} is lesser than {b}, respectively. 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 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 integer ---- @param use_utf16? boolean ---- @return integer -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. @@ -173,19 +161,6 @@ function vim.str_utf_start(str, index) end --- @return integer function vim.str_utf_end(str, index) end ---- Convert byte index to UTF-32 and UTF-16 indices. If {index} is not ---- supplied, the length of the string is used. All indices are zero-based. ---- ---- Embedded NUL bytes are treated as terminating the string. Invalid UTF-8 ---- bytes, and embedded surrogates are counted as one code point each. An ---- {index} in the middle of a UTF-8 sequence is rounded upwards to the end of ---- that sequence. ---- @param str string ---- @param index? integer ---- @return integer # UTF-32 index ---- @return integer # UTF-16 index -function vim.str_utfindex(str, index) end - --- The result is a String, which is the text {str} converted from --- encoding {from} to encoding {to}. When the conversion fails `nil` is --- returned. When some characters could not be converted they @@ -258,6 +233,12 @@ function vim.wait(time, callback, interval, fast_only) end --- {callback} receives event name plus additional parameters. See |ui-popupmenu| --- and the sections below for event format for respective events. --- +--- Callbacks for `msg_show` events are executed in |api-fast| context unless +--- Nvim will wait for input, in which case messages should be shown +--- immediately. +--- +--- Excessive errors inside the callback will result in forced detachment. +--- --- WARNING: This api is considered experimental. Usability will vary for --- different screen elements. In particular `ext_messages` behavior is subject --- to further changes and usability improvements. This is expected to be diff --git a/runtime/lua/vim/_meta/builtin_types.lua b/runtime/lua/vim/_meta/builtin_types.lua index aca6649957..eae76d80d7 100644 --- a/runtime/lua/vim/_meta/builtin_types.lua +++ b/runtime/lua/vim/_meta/builtin_types.lua @@ -66,6 +66,97 @@ --- @field winnr integer --- @field winrow integer +--- @class vim.quickfix.entry +--- buffer number; must be the number of a valid buffer +--- @field bufnr? integer +--- +--- name of a file; only used when "bufnr" is not +--- present or it is invalid. +--- @field filename? string +--- +--- name of a module; if given it will be used in +--- quickfix error window instead of the filename. +--- @field module? string +--- +--- line number in the file +--- @field lnum? integer +--- +--- end of lines, if the item spans multiple lines +--- @field end_lnum? integer +--- +--- search pattern used to locate the error +--- @field pattern? string +--- +--- column number +--- @field col? integer +--- +--- when non-zero: "col" is visual column +--- when zero: "col" is byte index +--- @field vcol? integer +--- +--- end column, if the item spans multiple columns +--- @field end_col? integer +--- +--- error number +--- @field nr? integer +--- +--- description of the error +--- @field text? string +--- +--- single-character error type, 'E', 'W', etc. +--- @field type? string +--- +--- recognized error message +--- @field valid? boolean +--- +--- custom data associated with the item, can be +--- any type. +--- @field user_data? any + +--- @class vim.fn.setqflist.what +--- +--- quickfix list context. See |quickfix-context| +--- @field context? table +--- +--- errorformat to use when parsing text from +--- "lines". If this is not present, then the +--- 'errorformat' option value is used. +--- See |quickfix-parse| +--- @field efm? string +--- +--- quickfix list identifier |quickfix-ID| +--- @field id? integer +--- index of the current entry in the quickfix +--- list specified by "id" or "nr". If set to '$', +--- then the last entry in the list is set as the +--- current entry. See |quickfix-index| +--- @field idx? integer +--- +--- list of quickfix entries. Same as the {list} +--- argument. +--- @field items? vim.quickfix.entry[] +--- +--- use 'errorformat' to parse a list of lines and +--- add the resulting entries to the quickfix list +--- {nr} or {id}. Only a |List| value is supported. +--- See |quickfix-parse| +--- @field lines? string[] +--- +--- list number in the quickfix stack; zero +--- means the current quickfix list and "$" means +--- the last quickfix list. +--- @field nr? integer +--- +--- function to get the text to display in the +--- quickfix window. The value can be the name of +--- a function or a funcref or a lambda. Refer +--- to |quickfix-window-function| for an explanation +--- of how to write the function and an example. +--- @field quickfixtextfunc? function +--- +--- quickfix list title text. See |quickfix-title| +--- @field title? string + --- @class vim.fn.sign_define.dict --- @field text string --- @field icon? string diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index ce3ff4f861..cb783720ac 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -289,12 +289,13 @@ vim.go.bk = vim.go.backup --- useful for example in source trees where all the files are symbolic or --- hard links and any changes should stay in the local source tree, not --- be propagated back to the original source. ---- *crontab* +--- *crontab* --- One situation where "no" and "auto" will cause problems: A program --- that opens a file, invokes Vim to edit that file, and then tests if --- the open file was changed (through the file descriptor) will check the --- backup file instead of the newly created file. "crontab -e" is an ---- example. +--- example, as are several `file-watcher` daemons like inotify. In that +--- case you probably want to switch this option. --- --- When a copy is made, the original file is truncated and then filled --- with the new text. This means that protection bits, owner and @@ -429,6 +430,7 @@ vim.go.bsk = vim.go.backupskip --- separated list of items. For each item that is present, the bell --- will be silenced. This is most useful to specify specific events in --- insert mode to be silenced. +--- You can also make it flash by using 'visualbell'. --- --- item meaning when present ~ --- all All events. @@ -452,6 +454,7 @@ vim.go.bsk = vim.go.backupskip --- register Unknown register after <C-R> in `Insert-mode`. --- shell Bell from shell output `:!`. --- spell Error happened on spell suggest. +--- term Bell from `:terminal` output. --- wildmode More matches in `cmdline-completion` available --- (depends on the 'wildmode' setting). --- @@ -572,19 +575,6 @@ vim.o.briopt = vim.o.breakindentopt vim.wo.breakindentopt = vim.o.breakindentopt vim.wo.briopt = vim.wo.breakindentopt ---- Which directory to use for the file browser: ---- last Use same directory as with last file browser, where a ---- file was opened or saved. ---- buffer Use the directory of the related buffer. ---- current Use the current directory. ---- {path} Use the specified directory ---- ---- @type string -vim.o.browsedir = "" -vim.o.bsdir = vim.o.browsedir -vim.go.browsedir = vim.o.browsedir -vim.go.bsdir = vim.go.browsedir - --- This option specifies what happens when a buffer is no longer --- displayed in a window: --- <empty> follow the global 'hidden' option @@ -1116,7 +1106,7 @@ vim.bo.cot = vim.bo.completeopt vim.go.completeopt = vim.o.completeopt vim.go.cot = vim.go.completeopt ---- only for MS-Windows +--- only modifiable in MS-Windows --- When this option is set it overrules 'shellslash' for completion: --- - When this option is set to "slash", a forward slash is used for path --- completion in insert mode. This is useful when editing HTML tag, or @@ -2304,6 +2294,62 @@ vim.wo.fcs = vim.wo.fillchars vim.go.fillchars = vim.o.fillchars vim.go.fcs = vim.go.fillchars +--- Function that is called to obtain the filename(s) for the `:find` +--- command. When this option is empty, the internal `file-searching` +--- mechanism is used. +--- +--- The value can be the name of a function, a `lambda` or a `Funcref`. +--- See `option-value-function` for more information. +--- +--- The function is called with two arguments. The first argument is a +--- `String` and is the `:find` command argument. The second argument is +--- a `Boolean` and is set to `v:true` when the function is called to get +--- a List of command-line completion matches for the `:find` command. +--- The function should return a List of strings. +--- +--- The function is called only once per `:find` command invocation. +--- The function can process all the directories specified in 'path'. +--- +--- If a match is found, the function should return a `List` containing +--- one or more file names. If a match is not found, the function +--- should return an empty List. +--- +--- If any errors are encountered during the function invocation, an +--- empty List is used as the return value. +--- +--- It is not allowed to change text or jump to another window while +--- executing the 'findfunc' `textlock`. +--- +--- This option cannot be set from a `modeline` or in the `sandbox`, for +--- security reasons. +--- +--- Examples: +--- +--- ```vim +--- " Use glob() +--- func FindFuncGlob(cmdarg, cmdcomplete) +--- let pat = a:cmdcomplete ? $'{a:cmdarg}*' : a:cmdarg +--- return glob(pat, v:false, v:true) +--- endfunc +--- set findfunc=FindFuncGlob +--- +--- " Use the 'git ls-files' output +--- func FindGitFiles(cmdarg, cmdcomplete) +--- let fnames = systemlist('git ls-files') +--- return fnames->filter('v:val =~? a:cmdarg') +--- endfunc +--- set findfunc=FindGitFiles +--- ``` +--- +--- +--- @type string +vim.o.findfunc = "" +vim.o.ffu = vim.o.findfunc +vim.bo.findfunc = vim.o.findfunc +vim.bo.ffu = vim.bo.findfunc +vim.go.findfunc = vim.o.findfunc +vim.go.ffu = vim.go.findfunc + --- When writing a file and this option is on, <EOL> at the end of file --- will be restored if missing. Turn this option off if you want to --- preserve the situation from the original file. @@ -2897,148 +2943,6 @@ vim.o.gfw = vim.o.guifontwide vim.go.guifontwide = vim.o.guifontwide vim.go.gfw = vim.go.guifontwide ---- This option only has an effect in the GUI version of Vim. It is a ---- sequence of letters which describes what components and options of the ---- GUI should be used. ---- To avoid problems with flags that are added in the future, use the ---- "+=" and "-=" feature of ":set" `add-option-flags`. ---- ---- Valid letters are as follows: ---- *guioptions_a* *'go-a'* ---- 'a' Autoselect: If present, then whenever VISUAL mode is started, ---- or the Visual area extended, Vim tries to become the owner of ---- the windowing system's global selection. This means that the ---- Visually highlighted text is available for pasting into other ---- applications as well as into Vim itself. When the Visual mode ---- ends, possibly due to an operation on the text, or when an ---- application wants to paste the selection, the highlighted text ---- is automatically yanked into the "* selection register. ---- Thus the selection is still available for pasting into other ---- applications after the VISUAL mode has ended. ---- If not present, then Vim won't become the owner of the ---- windowing system's global selection unless explicitly told to ---- by a yank or delete operation for the "* register. ---- The same applies to the modeless selection. ---- *'go-P'* ---- 'P' Like autoselect but using the "+ register instead of the "* ---- register. ---- *'go-A'* ---- 'A' Autoselect for the modeless selection. Like 'a', but only ---- applies to the modeless selection. ---- ---- 'guioptions' autoselect Visual autoselect modeless ~ ---- "" - - ---- "a" yes yes ---- "A" - yes ---- "aA" yes yes ---- ---- *'go-c'* ---- 'c' Use console dialogs instead of popup dialogs for simple ---- choices. ---- *'go-d'* ---- 'd' Use dark theme variant if available. ---- *'go-e'* ---- 'e' Add tab pages when indicated with 'showtabline'. ---- 'guitablabel' can be used to change the text in the labels. ---- When 'e' is missing a non-GUI tab pages line may be used. ---- The GUI tabs are only supported on some systems, currently ---- Mac OS/X and MS-Windows. ---- *'go-i'* ---- 'i' Use a Vim icon. ---- *'go-m'* ---- 'm' Menu bar is present. ---- *'go-M'* ---- 'M' The system menu "$VIMRUNTIME/menu.vim" is not sourced. Note ---- that this flag must be added in the vimrc file, before ---- switching on syntax or filetype recognition (when the `gvimrc` ---- file is sourced the system menu has already been loaded; the ---- `:syntax on` and `:filetype on` commands load the menu too). ---- *'go-g'* ---- 'g' Grey menu items: Make menu items that are not active grey. If ---- 'g' is not included inactive menu items are not shown at all. ---- *'go-T'* ---- 'T' Include Toolbar. Currently only in Win32 GUI. ---- *'go-r'* ---- 'r' Right-hand scrollbar is always present. ---- *'go-R'* ---- 'R' Right-hand scrollbar is present when there is a vertically ---- split window. ---- *'go-l'* ---- 'l' Left-hand scrollbar is always present. ---- *'go-L'* ---- 'L' Left-hand scrollbar is present when there is a vertically ---- split window. ---- *'go-b'* ---- 'b' Bottom (horizontal) scrollbar is present. Its size depends on ---- the longest visible line, or on the cursor line if the 'h' ---- flag is included. `gui-horiz-scroll` ---- *'go-h'* ---- 'h' Limit horizontal scrollbar size to the length of the cursor ---- line. Reduces computations. `gui-horiz-scroll` ---- ---- And yes, you may even have scrollbars on the left AND the right if ---- you really want to :-). See `gui-scrollbars` for more information. ---- ---- *'go-v'* ---- 'v' Use a vertical button layout for dialogs. When not included, ---- a horizontal layout is preferred, but when it doesn't fit a ---- vertical layout is used anyway. Not supported in GTK 3. ---- *'go-p'* ---- 'p' Use Pointer callbacks for X11 GUI. This is required for some ---- window managers. If the cursor is not blinking or hollow at ---- the right moment, try adding this flag. This must be done ---- before starting the GUI. Set it in your `gvimrc`. Adding or ---- removing it after the GUI has started has no effect. ---- *'go-k'* ---- 'k' Keep the GUI window size when adding/removing a scrollbar, or ---- toolbar, tabline, etc. Instead, the behavior is similar to ---- when the window is maximized and will adjust 'lines' and ---- 'columns' to fit to the window. Without the 'k' flag Vim will ---- try to keep 'lines' and 'columns' the same when adding and ---- removing GUI components. ---- ---- @type string -vim.o.guioptions = "" -vim.o.go = vim.o.guioptions -vim.go.guioptions = vim.o.guioptions -vim.go.go = vim.go.guioptions - ---- When non-empty describes the text to use in a label of the GUI tab ---- pages line. When empty and when the result is empty Vim will use a ---- default label. See `setting-guitablabel` for more info. ---- ---- The format of this option is like that of 'statusline'. ---- 'guitabtooltip' is used for the tooltip, see below. ---- The expression will be evaluated in the `sandbox` when set from a ---- modeline, see `sandbox-option`. ---- This option cannot be set in a modeline when 'modelineexpr' is off. ---- ---- Only used when the GUI tab pages line is displayed. 'e' must be ---- present in 'guioptions'. For the non-GUI tab pages line 'tabline' is ---- used. ---- ---- @type string -vim.o.guitablabel = "" -vim.o.gtl = vim.o.guitablabel -vim.go.guitablabel = vim.o.guitablabel -vim.go.gtl = vim.go.guitablabel - ---- When non-empty describes the text to use in a tooltip for the GUI tab ---- pages line. When empty Vim will use a default tooltip. ---- This option is otherwise just like 'guitablabel' above. ---- You can include a line break. Simplest method is to use `:let`: ---- ---- ```vim ---- let &guitabtooltip = "line one\nline two" ---- ``` ---- ---- ---- @type string -vim.o.guitabtooltip = "" -vim.o.gtt = vim.o.guitabtooltip -vim.go.guitabtooltip = vim.o.guitabtooltip -vim.go.gtt = vim.go.guitabtooltip - --- Name of the main help file. All distributed help files should be --- placed together in one directory. Additionally, all "doc" directories --- in 'runtimepath' will be used. @@ -3112,7 +3016,8 @@ vim.go.hid = vim.go.hidden --- A history of ":" commands, and a history of previous search patterns --- is remembered. This option decides how many entries may be stored in ---- each of these histories (see `cmdline-editing`). +--- each of these histories (see `cmdline-editing` and 'msghistory' for +--- the number of messages to remember). --- The maximum value is 10000. --- --- @type integer @@ -3181,29 +3086,6 @@ vim.o.ic = vim.o.ignorecase vim.go.ignorecase = vim.o.ignorecase vim.go.ic = vim.go.ignorecase ---- When set the Input Method is always on when starting to edit a command ---- line, unless entering a search pattern (see 'imsearch' for that). ---- Setting this option is useful when your input method allows entering ---- English characters directly, e.g., when it's used to type accented ---- characters with dead keys. ---- ---- @type boolean -vim.o.imcmdline = false -vim.o.imc = vim.o.imcmdline -vim.go.imcmdline = vim.o.imcmdline -vim.go.imc = vim.go.imcmdline - ---- When set the Input Method is never used. This is useful to disable ---- the IM when it doesn't work properly. ---- Currently this option is on by default for SGI/IRIX machines. This ---- may change in later releases. ---- ---- @type boolean -vim.o.imdisable = false -vim.o.imd = vim.o.imdisable -vim.go.imdisable = vim.o.imdisable -vim.go.imd = vim.go.imdisable - --- Specifies whether :lmap or an Input Method (IM) is to be used in --- Insert mode. Valid values: --- 0 :lmap is off and IM is off @@ -4488,74 +4370,6 @@ vim.go.mousemev = vim.go.mousemoveevent vim.o.mousescroll = "ver:3,hor:6" vim.go.mousescroll = vim.o.mousescroll ---- This option tells Vim what the mouse pointer should look like in ---- different modes. The option is a comma-separated list of parts, much ---- like used for 'guicursor'. Each part consist of a mode/location-list ---- and an argument-list: ---- mode-list:shape,mode-list:shape,.. ---- The mode-list is a dash separated list of these modes/locations: ---- In a normal window: ~ ---- n Normal mode ---- v Visual mode ---- ve Visual mode with 'selection' "exclusive" (same as 'v', ---- if not specified) ---- o Operator-pending mode ---- i Insert mode ---- r Replace mode ---- ---- Others: ~ ---- c appending to the command-line ---- ci inserting in the command-line ---- cr replacing in the command-line ---- m at the 'Hit ENTER' or 'More' prompts ---- ml idem, but cursor in the last line ---- e any mode, pointer below last window ---- s any mode, pointer on a status line ---- sd any mode, while dragging a status line ---- vs any mode, pointer on a vertical separator line ---- vd any mode, while dragging a vertical separator line ---- a everywhere ---- ---- The shape is one of the following: ---- avail name looks like ~ ---- w x arrow Normal mouse pointer ---- w x blank no pointer at all (use with care!) ---- w x beam I-beam ---- w x updown up-down sizing arrows ---- w x leftright left-right sizing arrows ---- w x busy The system's usual busy pointer ---- w x no The system's usual "no input" pointer ---- x udsizing indicates up-down resizing ---- x lrsizing indicates left-right resizing ---- x crosshair like a big thin + ---- x hand1 black hand ---- x hand2 white hand ---- x pencil what you write with ---- x question big ? ---- x rightup-arrow arrow pointing right-up ---- w x up-arrow arrow pointing up ---- x <number> any X11 pointer number (see X11/cursorfont.h) ---- ---- The "avail" column contains a 'w' if the shape is available for Win32, ---- x for X11. ---- Any modes not specified or shapes not available use the normal mouse ---- pointer. ---- ---- Example: ---- ---- ```vim ---- set mouseshape=s:udsizing,m:no ---- ``` ---- will make the mouse turn to a sizing arrow over the status lines and ---- indicate no input when the hit-enter prompt is displayed (since ---- clicking the mouse has no effect in this state.) ---- ---- @type string -vim.o.mouseshape = "" -vim.o.mouses = vim.o.mouseshape -vim.go.mouseshape = vim.o.mouseshape -vim.go.mouses = vim.go.mouseshape - --- Defines the maximum time in msec between two mouse clicks for the --- second click to be recognized as a multi click. --- @@ -4565,6 +4379,15 @@ vim.o.mouset = vim.o.mousetime vim.go.mousetime = vim.o.mousetime vim.go.mouset = vim.go.mousetime +--- Determines how many entries are remembered in the `:messages` history. +--- The maximum value is 10000. +--- +--- @type integer +vim.o.msghistory = 500 +vim.o.mhi = vim.o.msghistory +vim.go.msghistory = vim.o.msghistory +vim.go.mhi = vim.go.msghistory + --- This defines what bases Vim will consider for numbers when using the --- CTRL-A and CTRL-X commands for adding to and subtracting from a number --- respectively; see `CTRL-A` for more info on these commands. @@ -4675,19 +4498,6 @@ vim.o.ofu = vim.o.omnifunc vim.bo.omnifunc = vim.o.omnifunc vim.bo.ofu = vim.bo.omnifunc ---- only for Windows ---- Enable reading and writing from devices. This may get Vim stuck on a ---- device that can be opened but doesn't actually do the I/O. Therefore ---- it is off by default. ---- Note that on Windows editing "aux.h", "lpt1.txt" and the like also ---- result in editing a device. ---- ---- @type boolean -vim.o.opendevice = false -vim.o.odev = vim.o.opendevice -vim.go.opendevice = vim.o.opendevice -vim.go.odev = vim.go.opendevice - --- This option specifies a function to be called by the `g@` operator. --- See `:map-operator` for more info and an example. The value can be --- the name of a function, a `lambda` or a `Funcref`. See @@ -5634,7 +5444,7 @@ vim.go.sdf = vim.go.shadafile --- --- ```vim --- let &shell = executable('pwsh') ? 'pwsh' : 'powershell' ---- let &shellcmdflag = '-NoLogo -ExecutionPolicy RemoteSigned -Command [Console]::InputEncoding=[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new();$PSDefaultParameterValues[''Out-File:Encoding'']=''utf8'';Remove-Alias -Force -ErrorAction SilentlyContinue tee;' +--- let &shellcmdflag = '-NoLogo -NonInteractive -ExecutionPolicy RemoteSigned -Command [Console]::InputEncoding=[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new();$PSDefaultParameterValues[''Out-File:Encoding'']=''utf8'';$PSStyle.OutputRendering=''plaintext'';Remove-Alias -Force -ErrorAction SilentlyContinue tee;' --- let &shellredir = '2>&1 | %%{ "$_" } | Out-File %s; exit $LastExitCode' --- let &shellpipe = '2>&1 | %%{ "$_" } | tee %s; exit $LastExitCode' --- set shellquote= shellxquote= @@ -5747,7 +5557,7 @@ vim.o.srr = vim.o.shellredir vim.go.shellredir = vim.o.shellredir vim.go.srr = vim.go.shellredir ---- only for MS-Windows +--- only modifiable in MS-Windows --- When set, a forward slash is used when expanding file names. This is --- useful when a Unix-like shell is used instead of cmd.exe. Backward --- slashes can still be typed, but they are changed to forward slashes by @@ -5764,7 +5574,7 @@ vim.go.srr = vim.go.shellredir --- Also see 'completeslash'. --- --- @type boolean -vim.o.shellslash = false +vim.o.shellslash = true vim.o.ssl = vim.o.shellslash vim.go.shellslash = vim.o.shellslash vim.go.ssl = vim.go.shellslash @@ -6695,7 +6505,7 @@ vim.wo.stc = vim.wo.statuscolumn --- Emulate standard status line with 'ruler' set --- --- ```vim ---- set statusline=%<%f\ %h%m%r%=%-14.(%l,%c%V%)\ %P +--- set statusline=%<%f\ %h%w%m%r%=%-14.(%l,%c%V%)\ %P --- ``` --- Similar, but add ASCII value of char under the cursor (like "ga") --- @@ -7309,7 +7119,9 @@ vim.go.titleold = vim.o.titleold --- window. This happens only when the 'title' option is on. --- --- When this option contains printf-style '%' items, they will be ---- expanded according to the rules used for 'statusline'. +--- expanded according to the rules used for 'statusline'. If it contains +--- an invalid '%' format, the value is used as-is and no error or warning +--- will be given when the value is set. --- This option cannot be set in a modeline when 'modelineexpr' is off. --- --- Example: diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index 3f6deba092..5eb15e1eee 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -221,16 +221,16 @@ function vim.fn.assert_beeps(cmd) end --- @return 0|1 function vim.fn.assert_equal(expected, actual, msg) end ---- When the files {fname-one} and {fname-two} do not contain +--- When the files {fname_one} and {fname_two} do not contain --- exactly the same text an error message is added to |v:errors|. --- Also see |assert-return|. ---- When {fname-one} or {fname-two} does not exist the error will +--- When {fname_one} or {fname_two} does not exist the error will --- mention that. --- ---- @param fname-one string ---- @param fname-two string +--- @param fname_one string +--- @param fname_two string --- @return 0|1 -function vim.fn.assert_equalfile(fname-one, fname-two) end +function vim.fn.assert_equalfile(fname_one, fname_two) end --- When v:exception does not contain the string {error} an error --- message is added to |v:errors|. Also see |assert-return|. @@ -809,7 +809,7 @@ function vim.fn.char2nr(string, utf8) end --- The character class is one of: --- 0 blank --- 1 punctuation ---- 2 word character +--- 2 word character (depends on 'iskeyword') --- 3 emoji --- other specific Unicode class --- The class is used in patterns and word motions. @@ -828,7 +828,7 @@ function vim.fn.charclass(string) end --- echo col('.') " returns 7 --- < --- ---- @param expr string|integer[] +--- @param expr string|any[] --- @param winid? integer --- @return integer function vim.fn.charcol(expr, winid) end @@ -956,7 +956,7 @@ function vim.fn.clearmatches(win) end --- imap <F2> <Cmd>echo col(".").."\n"<CR> --- < --- ---- @param expr string|integer[] +--- @param expr string|any[] --- @param winid? integer --- @return integer function vim.fn.col(expr, winid) end @@ -2879,12 +2879,22 @@ function vim.fn.getcharsearch() end --- @return string function vim.fn.getcharstr(expr) end +--- Return completion pattern of the current command-line. +--- Only works when the command line is being edited, thus +--- requires use of |c_CTRL-\_e| or |c_CTRL-R_=|. +--- Also see |getcmdtype()|, |setcmdpos()|, |getcmdline()|, +--- |getcmdprompt()|, |getcmdcompltype()| and |setcmdline()|. +--- Returns an empty string when completion is not defined. +--- +--- @return string +function vim.fn.getcmdcomplpat() end + --- Return the type of the current command-line completion. --- Only works when the command line is being edited, thus --- requires use of |c_CTRL-\_e| or |c_CTRL-R_=|. --- See |:command-completion| for the return string. --- Also see |getcmdtype()|, |setcmdpos()|, |getcmdline()|, ---- |getcmdprompt()| and |setcmdline()|. +--- |getcmdprompt()|, |getcmdcomplpat()| and |setcmdline()|. --- Returns an empty string when completion is not defined. --- --- @return string @@ -2998,6 +3008,7 @@ function vim.fn.getcmdwintype() end --- runtime |:runtime| completion --- scriptnames sourced script names |:scriptnames| --- shellcmd Shell command +--- shellcmdline Shell command line with filename arguments --- sign |:sign| suboptions --- syntax syntax file names |'syntax'| --- syntime |:syntime| suboptions @@ -5181,7 +5192,7 @@ function vim.fn.log(expr) end function vim.fn.log10(expr) end --- {expr1} must be a |List|, |String|, |Blob| or |Dictionary|. ---- When {expr1} is a |List|| or |Dictionary|, replace each +--- When {expr1} is a |List| or |Dictionary|, replace each --- item in {expr1} with the result of evaluating {expr2}. --- For a |Blob| each byte is replaced. --- For a |String|, each character, including composing @@ -7912,7 +7923,7 @@ function vim.fn.setbufvar(buf, varname, val) end --- To clear the overrides pass an empty {list}: >vim --- call setcellwidths([]) --- ---- <You can use the script $VIMRUNTIME/tools/emoji_list.lua to see +--- <You can use the script $VIMRUNTIME/scripts/emoji_list.lua to see --- the effect for known emoji characters. Move the cursor --- through the text to check if the cell widths of your terminal --- match with what Vim knows about each emoji. If it doesn't @@ -8218,6 +8229,8 @@ function vim.fn.setpos(expr, list) end --- clear the list: >vim --- call setqflist([], 'r') --- < +--- 'u' Like 'r', but tries to preserve the current selection +--- in the quickfix list. --- 'f' All the quickfix lists in the quickfix stack are --- freed. --- @@ -8273,9 +8286,9 @@ function vim.fn.setpos(expr, list) end --- independent of the 'errorformat' setting. Use a command like --- `:cc 1` to jump to the first position. --- ---- @param list any[] +--- @param list vim.quickfix.entry[] --- @param action? string ---- @param what? table +--- @param what? vim.fn.setqflist.what --- @return any function vim.fn.setqflist(list, action, what) end @@ -10533,7 +10546,7 @@ function vim.fn.values(dict) end --- echo max(map(range(1, line('$')), "virtcol([v:val, '$'])")) --- < --- ---- @param expr string|integer[] +--- @param expr string|any[] --- @param list? boolean --- @param winid? integer --- @return any @@ -10616,7 +10629,7 @@ function vim.fn.wait(timeout, condition, interval) end --- For example to make <c-j> work like <down> in wildmode, use: >vim --- cnoremap <expr> <C-j> wildmenumode() ? "\<Down>\<Tab>" : "\<c-j>" --- < ---- (Note, this needs the 'wildcharm' option set appropriately). +--- (Note: this needs the 'wildcharm' option set appropriately). --- --- @return any function vim.fn.wildmenumode() end diff --git a/runtime/lua/vim/_meta/vvars.lua b/runtime/lua/vim/_meta/vvars.lua index e00402ab3f..8784fdbac9 100644 --- a/runtime/lua/vim/_meta/vvars.lua +++ b/runtime/lua/vim/_meta/vvars.lua @@ -160,13 +160,14 @@ vim.v.errors = ... --- an aborting condition (e.g. `c_Esc` or --- `c_CTRL-C` for `CmdlineLeave`). --- chan `channel-id` +--- info Dict of arbitrary event data. --- cmdlevel Level of cmdline. --- cmdtype Type of cmdline, `cmdline-char`. --- cwd Current working directory. --- inclusive Motion is `inclusive`, else exclusive. --- scope Event-specific scope name. --- operator Current `operator`. Also set for Ex ---- commands (unlike `v:operator`). For +--- commands (unlike `v:operator`). For --- example if `TextYankPost` is triggered --- by the `:yank` Ex command then --- `v:event.operator` is "y". diff --git a/runtime/lua/vim/_options.lua b/runtime/lua/vim/_options.lua index a61fa61256..77d7054626 100644 --- a/runtime/lua/vim/_options.lua +++ b/runtime/lua/vim/_options.lua @@ -274,11 +274,9 @@ vim.go = setmetatable({}, { }) --- Get or set buffer-scoped |options| for the buffer with number {bufnr}. ---- If {bufnr} is omitted then the current buffer is used. +--- Like `:setlocal`. If {bufnr} is omitted then the current buffer is used. --- Invalid {bufnr} or key is an error. --- ---- Note: this is equivalent to `:setlocal` for |global-local| options and `:set` otherwise. ---- --- Example: --- --- ```lua diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua index d603971495..ce5dbffeaa 100644 --- a/runtime/lua/vim/_system.lua +++ b/runtime/lua/vim/_system.lua @@ -230,6 +230,8 @@ local function default_handler(stream, text, bucket) end end +local is_win = vim.fn.has('win32') == 1 + local M = {} --- @param cmd string @@ -238,6 +240,13 @@ local M = {} --- @param on_error fun() --- @return uv.uv_process_t, integer local function spawn(cmd, opts, on_exit, on_error) + if is_win then + local cmd1 = vim.fn.exepath(cmd) + if cmd1 ~= '' then + cmd = cmd1 + end + end + local handle, pid_or_err = uv.spawn(cmd, opts, on_exit) if not handle then on_error() @@ -309,11 +318,9 @@ end --- @param on_exit? fun(out: vim.SystemCompleted) --- @return vim.SystemObj function M.run(cmd, opts, on_exit) - vim.validate({ - cmd = { cmd, 'table' }, - opts = { opts, 'table', true }, - on_exit = { on_exit, 'function', true }, - }) + vim.validate('cmd', cmd, 'table') + vim.validate('opts', opts, 'table', true) + vim.validate('on_exit', on_exit, 'function', true) opts = opts or {} diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index 11f6742941..13894c6147 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -59,11 +59,9 @@ end --- @param callback vim._watch.Callback Callback for new events --- @return fun() cancel Stops the watcher function M.watch(path, opts, callback) - vim.validate({ - path = { path, 'string', false }, - opts = { opts, 'table', true }, - callback = { callback, 'function', false }, - }) + vim.validate('path', path, 'string') + vim.validate('opts', opts, 'table', true) + vim.validate('callback', callback, 'function') opts = opts or {} @@ -127,11 +125,9 @@ end --- @param callback vim._watch.Callback Callback for new events --- @return fun() cancel Stops the watcher function M.watchdirs(path, opts, callback) - vim.validate({ - path = { path, 'string', false }, - opts = { opts, 'table', true }, - callback = { callback, 'function', false }, - }) + vim.validate('path', path, 'string') + vim.validate('opts', opts, 'table', true) + vim.validate('callback', callback, 'function') opts = opts or {} local debounce = opts.debounce or 500 diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index ef00a1fa51..4fb8c6a686 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -71,9 +71,9 @@ local M = {} --- (default: `false`) --- @field update_in_insert? boolean --- ---- Sort diagnostics by severity. This affects the order in which signs and ---- virtual text are displayed. When true, higher severities are displayed ---- before lower severities (e.g. ERROR is displayed before WARN). +--- Sort diagnostics by severity. This affects the order in which signs, +--- virtual text, and highlights are displayed. When true, higher severities are +--- displayed before lower severities (e.g. ERROR is displayed before WARN). --- Options: --- - {reverse}? (boolean) Reverse sort order --- (default: `false`) @@ -282,6 +282,11 @@ M.severity = { [2] = 'WARN', [3] = 'INFO', [4] = 'HINT', + --- Mappings from qflist/loclist error types to severities + E = 1, + W = 2, + I = 3, + N = 4, } --- @alias vim.diagnostic.SeverityInt 1|2|3|4 @@ -289,12 +294,6 @@ M.severity = { --- See |diagnostic-severity| and |vim.diagnostic.get()| --- @alias vim.diagnostic.SeverityFilter vim.diagnostic.Severity|vim.diagnostic.Severity[]|{min:vim.diagnostic.Severity,max:vim.diagnostic.Severity} --- Mappings from qflist/loclist error types to severities -M.severity.E = M.severity.ERROR -M.severity.W = M.severity.WARN -M.severity.I = M.severity.INFO -M.severity.N = M.severity.HINT - --- @type vim.diagnostic.Opts local global_diagnostic_options = { signs = true, @@ -320,7 +319,7 @@ local global_diagnostic_options = { --- @type table<string,vim.diagnostic.Handler> M.handlers = setmetatable({}, { __newindex = function(t, name, handler) - vim.validate({ handler = { handler, 't' } }) + vim.validate('handler', handler, 'table') rawset(t, name, handler) if global_diagnostic_options[name] == nil then global_diagnostic_options[name] = true @@ -477,10 +476,8 @@ end --- @param diagnostics vim.Diagnostic[] --- @return vim.Diagnostic[] local function reformat_diagnostics(format, diagnostics) - vim.validate({ - format = { format, 'f' }, - diagnostics = { diagnostics, 't' }, - }) + vim.validate('format', format, 'function') + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') local formatted = vim.deepcopy(diagnostics, true) for _, diagnostic in ipairs(formatted) do @@ -659,6 +656,28 @@ local function save_extmarks(namespace, bufnr) api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, { details = true }) end +--- Create a function that converts a diagnostic severity to an extmark priority. +--- @param priority integer Base priority +--- @param opts vim.diagnostic.OptsResolved +--- @return fun(severity: vim.diagnostic.Severity): integer +local function severity_to_extmark_priority(priority, opts) + if opts.severity_sort then + if type(opts.severity_sort) == 'table' and opts.severity_sort.reverse then + return function(severity) + return priority + (severity - vim.diagnostic.severity.ERROR) + end + end + + return function(severity) + return priority + (vim.diagnostic.severity.HINT - severity) + end + end + + return function() + return priority + end +end + --- @type table<string,true> local registered_autocmds = {} @@ -871,14 +890,14 @@ local function next_diagnostic(search_forward, opts) if opts.win_id then vim.deprecate('opts.win_id', 'opts.winid', '0.13') opts.winid = opts.win_id - opts.win_id = nil + opts.win_id = nil --- @diagnostic disable-line end -- Support deprecated cursor_position alias if opts.cursor_position then vim.deprecate('opts.cursor_position', 'opts.pos', '0.13') opts.pos = opts.cursor_position - opts.cursor_position = nil + opts.cursor_position = nil --- @diagnostic disable-line end local winid = opts.winid or api.nvim_get_current_win() @@ -959,7 +978,7 @@ local function goto_diagnostic(diagnostic, opts) if opts.win_id then vim.deprecate('opts.win_id', 'opts.winid', '0.13') opts.winid = opts.win_id - opts.win_id = nil + opts.win_id = nil --- @diagnostic disable-line end local winid = opts.winid or api.nvim_get_current_win() @@ -972,8 +991,9 @@ local function goto_diagnostic(diagnostic, opts) vim.cmd('normal! zv') end) - if opts.float then - local float_opts = type(opts.float) == 'table' and opts.float or {} + local float_opts = opts.float + if float_opts then + float_opts = type(float_opts) == 'table' and float_opts or {} vim.schedule(function() M.open_float(vim.tbl_extend('keep', float_opts, { bufnr = api.nvim_win_get_buf(winid), @@ -1012,10 +1032,8 @@ end --- When omitted, update the global diagnostic options. ---@return vim.diagnostic.Opts? : Current diagnostic config if {opts} is omitted. function M.config(opts, namespace) - vim.validate({ - opts = { opts, 't', true }, - namespace = { namespace, 'n', true }, - }) + vim.validate('opts', opts, 'table', true) + vim.validate('namespace', namespace, 'number', true) local t --- @type vim.diagnostic.Opts if namespace then @@ -1058,16 +1076,10 @@ end ---@param diagnostics vim.Diagnostic[] ---@param opts? vim.diagnostic.Opts Display options to pass to |vim.diagnostic.show()| function M.set(namespace, bufnr, diagnostics, opts) - vim.validate({ - namespace = { namespace, 'n' }, - bufnr = { bufnr, 'n' }, - diagnostics = { - diagnostics, - vim.islist, - 'a list of diagnostics', - }, - opts = { opts, 't', true }, - }) + vim.validate('namespace', namespace, 'number') + vim.validate('bufnr', bufnr, 'number') + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') + vim.validate('opts', opts, 'table', true) bufnr = get_bufnr(bufnr) @@ -1092,7 +1104,7 @@ end ---@param namespace integer Diagnostic namespace ---@return vim.diagnostic.NS : Namespace metadata function M.get_namespace(namespace) - vim.validate({ namespace = { namespace, 'n' } }) + vim.validate('namespace', namespace, 'number') if not all_namespaces[namespace] then local name --- @type string? for k, v in pairs(api.nvim_get_namespaces()) do @@ -1131,10 +1143,8 @@ end ---@return vim.Diagnostic[] : Fields `bufnr`, `end_lnum`, `end_col`, and `severity` --- are guaranteed to be present. function M.get(bufnr, opts) - vim.validate({ - bufnr = { bufnr, 'n', true }, - opts = { opts, 't', true }, - }) + vim.validate('bufnr', bufnr, 'number', true) + vim.validate('opts', opts, 'table', true) return vim.deepcopy(get_diagnostics(bufnr, opts, false), true) end @@ -1147,10 +1157,8 @@ end ---@return table : Table with actually present severity values as keys --- (see |diagnostic-severity|) and integer counts as values. function M.count(bufnr, opts) - vim.validate({ - bufnr = { bufnr, 'n', true }, - opts = { opts, 't', true }, - }) + vim.validate('bufnr', bufnr, 'number', true) + vim.validate('opts', opts, 'table', true) local diagnostics = get_diagnostics(bufnr, opts, false) local count = {} --- @type table<integer,integer> @@ -1309,7 +1317,7 @@ function M.jump(opts) if opts.cursor_position then vim.deprecate('opts.cursor_position', 'opts.pos', '0.13') opts.pos = opts.cursor_position - opts.cursor_position = nil + opts.cursor_position = nil --- @diagnostic disable-line end local diag = nil @@ -1348,16 +1356,10 @@ end M.handlers.signs = { show = function(namespace, bufnr, diagnostics, opts) - vim.validate({ - namespace = { namespace, 'n' }, - bufnr = { bufnr, 'n' }, - diagnostics = { - diagnostics, - vim.islist, - 'a list of diagnostics', - }, - opts = { opts, 't', true }, - }) + vim.validate('namespace', namespace, 'number') + vim.validate('bufnr', bufnr, 'number') + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') + vim.validate('opts', opts, 'table', true) bufnr = get_bufnr(bufnr) opts = opts or {} @@ -1372,22 +1374,7 @@ M.handlers.signs = { -- 10 is the default sign priority when none is explicitly specified local priority = opts.signs and opts.signs.priority or 10 - local get_priority --- @type function - if opts.severity_sort then - if type(opts.severity_sort) == 'table' and opts.severity_sort.reverse then - get_priority = function(severity) - return priority + (severity - vim.diagnostic.severity.ERROR) - end - else - get_priority = function(severity) - return priority + (vim.diagnostic.severity.HINT - severity) - end - end - else - get_priority = function() - return priority - end - end + local get_priority = severity_to_extmark_priority(priority, opts) local ns = M.get_namespace(namespace) if not ns.user_data.sign_ns then @@ -1475,16 +1462,10 @@ M.handlers.signs = { M.handlers.underline = { show = function(namespace, bufnr, diagnostics, opts) - vim.validate({ - namespace = { namespace, 'n' }, - bufnr = { bufnr, 'n' }, - diagnostics = { - diagnostics, - vim.islist, - 'a list of diagnostics', - }, - opts = { opts, 't', true }, - }) + vim.validate('namespace', namespace, 'number') + vim.validate('bufnr', bufnr, 'number') + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') + vim.validate('opts', opts, 'table', true) bufnr = get_bufnr(bufnr) opts = opts or {} @@ -1504,15 +1485,12 @@ M.handlers.underline = { end local underline_ns = ns.user_data.underline_ns + local get_priority = severity_to_extmark_priority(vim.hl.priorities.diagnostics, opts) + for _, diagnostic in ipairs(diagnostics) do - --- @type string? + -- Default to error if we don't have a highlight associated local higroup = underline_highlight_map[assert(diagnostic.severity)] - - if higroup == nil then - -- Default to error if we don't have a highlight associated - -- TODO(lewis6991): this is always nil since underline_highlight_map only has integer keys - higroup = underline_highlight_map.Error - end + or underline_highlight_map[vim.diagnostic.severity.ERROR] if diagnostic._tags then -- TODO(lewis6991): we should be able to stack these. @@ -1524,13 +1502,13 @@ M.handlers.underline = { end end - vim.highlight.range( + vim.hl.range( bufnr, underline_ns, higroup, { diagnostic.lnum, diagnostic.col }, { diagnostic.end_lnum, diagnostic.end_col }, - { priority = vim.highlight.priorities.diagnostics } + { priority = get_priority(diagnostic.severity) } ) end save_extmarks(underline_ns, bufnr) @@ -1548,16 +1526,10 @@ M.handlers.underline = { M.handlers.virtual_text = { show = function(namespace, bufnr, diagnostics, opts) - vim.validate({ - namespace = { namespace, 'n' }, - bufnr = { bufnr, 'n' }, - diagnostics = { - diagnostics, - vim.islist, - 'a list of diagnostics', - }, - opts = { opts, 't', true }, - }) + vim.validate('namespace', namespace, 'number') + vim.validate('bufnr', bufnr, 'number') + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') + vim.validate('opts', opts, 'table', true) bufnr = get_bufnr(bufnr) opts = opts or {} @@ -1681,10 +1653,8 @@ end ---@param bufnr integer? Buffer number, or 0 for current buffer. When --- omitted, hide diagnostics in all buffers. function M.hide(namespace, bufnr) - vim.validate({ - namespace = { namespace, 'n', true }, - bufnr = { bufnr, 'n', true }, - }) + vim.validate('namespace', namespace, 'number', true) + vim.validate('bufnr', bufnr, 'number', true) local buffers = bufnr and { get_bufnr(bufnr) } or vim.tbl_keys(diagnostic_cache) for _, iter_bufnr in ipairs(buffers) do @@ -1741,18 +1711,10 @@ end --- or {bufnr} is nil. ---@param opts? vim.diagnostic.Opts Display options. function M.show(namespace, bufnr, diagnostics, opts) - vim.validate({ - namespace = { namespace, 'n', true }, - bufnr = { bufnr, 'n', true }, - diagnostics = { - diagnostics, - function(v) - return v == nil or vim.islist(v) - end, - 'a list of diagnostics', - }, - opts = { opts, 't', true }, - }) + vim.validate('namespace', namespace, 'number', true) + vim.validate('bufnr', bufnr, 'number', true) + vim.validate('diagnostics', diagnostics, vim.islist, true, 'a list of diagnostics') + vim.validate('opts', opts, 'table', true) if not bufnr or not namespace then assert(not diagnostics, 'Cannot show diagnostics without a buffer and namespace') @@ -1825,9 +1787,7 @@ function M.open_float(opts, ...) bufnr = opts opts = ... --- @type vim.diagnostic.Opts.Float else - vim.validate({ - opts = { opts, 't', true }, - }) + vim.validate('opts', opts, 'table', true) end opts = opts or {} @@ -1905,13 +1865,7 @@ function M.open_float(opts, ...) local highlights = {} --- @type table[] local header = if_nil(opts.header, 'Diagnostics:') if header then - vim.validate({ - header = { - header, - { 'string', 'table' }, - "'string' or 'table'", - }, - }) + vim.validate('header', header, { 'string', 'table' }, "'string' or 'table'") if type(header) == 'table' then -- Don't insert any lines for an empty string if string.len(if_nil(header[1], '')) > 0 then @@ -1939,13 +1893,12 @@ function M.open_float(opts, ...) local prefix, prefix_hl_group --- @type string?, string? if prefix_opt then - vim.validate({ - prefix = { - prefix_opt, - { 'string', 'table', 'function' }, - "'string' or 'table' or 'function'", - }, - }) + vim.validate( + 'prefix', + prefix_opt, + { 'string', 'table', 'function' }, + "'string' or 'table' or 'function'" + ) if type(prefix_opt) == 'string' then prefix, prefix_hl_group = prefix_opt, 'NormalFloat' elseif type(prefix_opt) == 'table' then @@ -1959,13 +1912,12 @@ function M.open_float(opts, ...) local suffix, suffix_hl_group --- @type string?, string? if suffix_opt then - vim.validate({ - suffix = { - suffix_opt, - { 'string', 'table', 'function' }, - "'string' or 'table' or 'function'", - }, - }) + vim.validate( + 'suffix', + suffix_opt, + { 'string', 'table', 'function' }, + "'string' or 'table' or 'function'" + ) if type(suffix_opt) == 'string' then suffix, suffix_hl_group = suffix_opt, 'NormalFloat' elseif type(suffix_opt) == 'table' then @@ -2038,10 +1990,8 @@ end ---@param bufnr integer? Remove diagnostics for the given buffer. When omitted, --- diagnostics are removed for all buffers. function M.reset(namespace, bufnr) - vim.validate({ - namespace = { namespace, 'n', true }, - bufnr = { bufnr, 'n', true }, - }) + vim.validate('namespace', namespace, 'number', true) + vim.validate('bufnr', bufnr, 'number', true) local buffers = bufnr and { get_bufnr(bufnr) } or vim.tbl_keys(diagnostic_cache) for _, iter_bufnr in ipairs(buffers) do @@ -2144,10 +2094,8 @@ function M.enable(enable, filter) '0.12' ) - vim.validate({ - enable = { enable, 'n', true }, -- Legacy `bufnr` arg. - filter = { filter, 'n', true }, -- Legacy `namespace` arg. - }) + vim.validate('enable', enable, 'number', true) -- Legacy `bufnr` arg. + vim.validate('filter', filter, 'number', true) -- Legacy `namespace` arg. local ns_id = type(filter) == 'number' and filter or nil filter = {} @@ -2156,17 +2104,16 @@ function M.enable(enable, filter) enable = true else filter = filter or {} - vim.validate({ - enable = { enable, 'b', true }, - filter = { filter, 't', true }, - }) + vim.validate('enable', enable, 'boolean', true) + vim.validate('filter', filter, 'table', true) end enable = enable == nil and true or enable local bufnr = filter.bufnr + local ns_id = filter.ns_id - if bufnr == nil then - if filter.ns_id == nil then + if not bufnr then + if not ns_id then diagnostic_disabled = ( enable -- Enable everything by setting diagnostic_disabled to an empty table. @@ -2180,12 +2127,12 @@ function M.enable(enable, filter) }) ) else - local ns = M.get_namespace(filter.ns_id) + local ns = M.get_namespace(ns_id) ns.disabled = not enable end else bufnr = get_bufnr(bufnr) - if filter.ns_id == nil then + if not ns_id then diagnostic_disabled[bufnr] = (not enable) and true or nil else if type(diagnostic_disabled[bufnr]) ~= 'table' then @@ -2195,14 +2142,14 @@ function M.enable(enable, filter) diagnostic_disabled[bufnr] = {} end end - diagnostic_disabled[bufnr][filter.ns_id] = (not enable) and true or nil + diagnostic_disabled[bufnr][ns_id] = (not enable) and true or nil end end if enable then - M.show(filter.ns_id, bufnr) + M.show(ns_id, bufnr) else - M.hide(filter.ns_id, bufnr) + M.hide(ns_id, bufnr) end end @@ -2234,13 +2181,11 @@ end --- ERROR. ---@return vim.Diagnostic?: |vim.Diagnostic| structure or `nil` if {pat} fails to match {str}. function M.match(str, pat, groups, severity_map, defaults) - vim.validate({ - str = { str, 's' }, - pat = { pat, 's' }, - groups = { groups, 't' }, - severity_map = { severity_map, 't', true }, - defaults = { defaults, 't', true }, - }) + vim.validate('str', str, 'string') + vim.validate('pat', pat, 'string') + vim.validate('groups', groups, 'table') + vim.validate('severity_map', severity_map, 'table', true) + vim.validate('defaults', defaults, 'table', true) --- @type table<string,vim.diagnostic.Severity> severity_map = severity_map or M.severity @@ -2283,13 +2228,7 @@ local errlist_type_map = { ---@param diagnostics vim.Diagnostic[] ---@return table[] : Quickfix list items |setqflist-what| function M.toqflist(diagnostics) - vim.validate({ - diagnostics = { - diagnostics, - vim.islist, - 'a list of diagnostics', - }, - }) + vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics') local list = {} --- @type table[] for _, v in ipairs(diagnostics) do @@ -2323,13 +2262,7 @@ end ---@param list table[] List of quickfix items from |getqflist()| or |getloclist()|. ---@return vim.Diagnostic[] function M.fromqflist(list) - vim.validate({ - list = { - list, - vim.islist, - 'a list of quickfix items', - }, - }) + vim.validate('list', list, 'table') local diagnostics = {} --- @type vim.Diagnostic[] for _, item in ipairs(list) do diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index d3910e26eb..e1e73d63fe 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -4,15 +4,22 @@ local fn = vim.fn local M = {} --- @alias vim.filetype.mapfn fun(path:string,bufnr:integer, ...):string?, fun(b:integer)? ---- @alias vim.filetype.mapopts { parent: string, priority: number } +--- @alias vim.filetype.mapopts { priority: number } --- @alias vim.filetype.maptbl [string|vim.filetype.mapfn, vim.filetype.mapopts] --- @alias vim.filetype.mapping.value string|vim.filetype.mapfn|vim.filetype.maptbl --- @alias vim.filetype.mapping table<string,vim.filetype.mapping.value> +--- @class vim.filetype.mapping.sorted +--- @nodoc +--- @field [1] string parent pattern +--- @field [2] string pattern +--- @field [3] string|vim.filetype.mapfn +--- @field [4] integer priority + --- @param ft string|vim.filetype.mapfn ---- @param opts? vim.filetype.mapopts +--- @param priority? integer --- @return vim.filetype.maptbl -local function starsetf(ft, opts) +local function starsetf(ft, priority) return { function(path, bufnr) -- Note: when `ft` is a function its return value may be nil. @@ -27,11 +34,8 @@ local function starsetf(ft, opts) end end, { - -- Allow setting "parent" to be reused in closures, but don't have default as it will be - -- assigned later from grouping - parent = opts and opts.parent, -- Starset matches should have lowest priority by default - priority = (opts and opts.priority) or -math.huge, + priority = priority or -math.huge, }, } end @@ -402,6 +406,7 @@ local extension = { dtso = 'dts', its = 'dts', keymap = 'dts', + overlay = 'dts', dylan = 'dylan', intr = 'dylanintr', lid = 'dylanlid', @@ -587,6 +592,7 @@ local extension = { ibi = 'ibasic', icn = 'icon', idl = detect.idl, + idr = 'idris2', inc = detect.inc, inf = 'inform', INF = 'inform', @@ -594,6 +600,7 @@ local extension = { inko = 'inko', inp = detect.inp, ms = detect_seq(detect.nroff, 'xmath'), + ipkg = 'ipkg', iss = 'iss', mst = 'ist', ist = 'ist', @@ -667,13 +674,14 @@ local extension = { journal = 'ledger', ldg = 'ledger', ledger = 'ledger', + leo = 'leo', less = 'less', lex = 'lex', lxx = 'lex', ['l++'] = 'lex', l = 'lex', lhs = 'lhaskell', - ll = 'lifelines', + lidr = 'lidris2', ly = 'lilypond', ily = 'lilypond', liquid = 'liquid', @@ -688,6 +696,7 @@ local extension = { lt = 'lite', lite = 'lite', livemd = 'livebook', + ll = detect.ll, log = detect.log, Log = detect.log, LOG = detect.log, @@ -752,6 +761,7 @@ local extension = { mib = 'mib', mix = 'mix', mixal = 'mix', + mlir = 'mlir', mm = detect.mm, nb = 'mma', wl = 'mma', @@ -782,6 +792,7 @@ local extension = { mof = 'msidl', odl = 'msidl', msql = 'msql', + mss = 'mss', mu = 'mupad', mush = 'mush', mustache = 'mustache', @@ -1067,6 +1078,9 @@ local extension = { envrc = detect.sh, ksh = detect.ksh, sh = detect.sh, + lo = 'sh', + la = 'sh', + lai = 'sh', mdd = 'sh', sieve = 'sieve', siv = 'sieve', @@ -1146,6 +1160,7 @@ local extension = { sface = 'surface', svelte = 'svelte', svg = 'svg', + sw = 'sway', swift = 'swift', swiftinterface = 'swift', swig = 'swig', @@ -1303,6 +1318,7 @@ local extension = { xpfm = 'xml', spfm = 'xml', bxml = 'xml', + mmi = 'xml', xcu = 'xml', xlb = 'xml', xlc = 'xml', @@ -1610,6 +1626,7 @@ local filename = { ['ldaprc'] = 'ldapconf', ['.ldaprc'] = 'ldapconf', ['ldap.conf'] = 'ldapconf', + ['lfrc'] = 'lf', ['lftp.conf'] = 'lftp', ['.lftprc'] = 'lftp', ['/.libao'] = 'libao', @@ -1862,11 +1879,10 @@ local filename = { } -- Re-use closures as much as possible -local detect_apache_diretc = starsetf('apache', { parent = '/etc/' }) -local detect_apache_dotconf = starsetf('apache', { parent = '%.conf' }) -local detect_muttrc = starsetf('muttrc', { parent = 'utt' }) -local detect_neomuttrc = starsetf('neomuttrc', { parent = 'utt' }) -local detect_xkb = starsetf('xkb', { parent = '/usr/' }) +local detect_apache = starsetf('apache') +local detect_muttrc = starsetf('muttrc') +local detect_neomuttrc = starsetf('neomuttrc') +local detect_xkb = starsetf('xkb') ---@type table<string,vim.filetype.mapping> local pattern = { @@ -1883,14 +1899,14 @@ local pattern = { ['/etc/asound%.conf$'] = 'alsaconf', ['/etc/apache2/sites%-.*/.*%.com$'] = 'apache', ['/etc/httpd/.*%.conf$'] = 'apache', - ['/etc/apache2/.*%.conf'] = detect_apache_diretc, - ['/etc/apache2/conf%..*/'] = detect_apache_diretc, - ['/etc/apache2/mods%-.*/'] = detect_apache_diretc, - ['/etc/apache2/sites%-.*/'] = detect_apache_diretc, - ['/etc/httpd/conf%..*/'] = detect_apache_diretc, - ['/etc/httpd/conf%.d/.*%.conf'] = detect_apache_diretc, - ['/etc/httpd/mods%-.*/'] = detect_apache_diretc, - ['/etc/httpd/sites%-.*/'] = detect_apache_diretc, + ['/etc/apache2/.*%.conf'] = detect_apache, + ['/etc/apache2/conf%..*/'] = detect_apache, + ['/etc/apache2/mods%-.*/'] = detect_apache, + ['/etc/apache2/sites%-.*/'] = detect_apache, + ['/etc/httpd/conf%..*/'] = detect_apache, + ['/etc/httpd/conf%.d/.*%.conf'] = detect_apache, + ['/etc/httpd/mods%-.*/'] = detect_apache, + ['/etc/httpd/sites%-.*/'] = detect_apache, ['/etc/proftpd/.*%.conf'] = starsetf('apachestyle'), ['/etc/proftpd/conf%..*/'] = starsetf('apachestyle'), ['/etc/cdrdao%.conf$'] = 'cdrdaoconf', @@ -2106,6 +2122,7 @@ local pattern = { ['/build/conf/.*%.conf$'] = 'bitbake', ['/meta%-.*/conf/.*%.conf$'] = 'bitbake', ['/meta/conf/.*%.conf$'] = 'bitbake', + ['/project%-spec/configs/.*%.conf$'] = 'bitbake', ['/%.cabal/config$'] = 'cabalconfig', ['/cabal/config$'] = 'cabalconfig', ['/%.aws/config$'] = 'confini', @@ -2132,6 +2149,7 @@ local pattern = { ['/sway/config$'] = 'swayconfig', ['/%.cargo/config$'] = 'toml', ['/%.bundle/config$'] = 'yaml', + ['/%.kube/config$'] = 'yaml', }, ['/%.'] = { ['/%.aws/credentials$'] = 'confini', @@ -2180,11 +2198,13 @@ local pattern = { }, ['%.conf'] = { ['^proftpd%.conf'] = starsetf('apachestyle'), - ['^access%.conf'] = detect_apache_dotconf, - ['^apache%.conf'] = detect_apache_dotconf, - ['^apache2%.conf'] = detect_apache_dotconf, - ['^httpd%.conf'] = detect_apache_dotconf, - ['^srm%.conf'] = detect_apache_dotconf, + ['^access%.conf'] = detect_apache, + ['^apache%.conf'] = detect_apache, + ['^apache2%.conf'] = detect_apache, + ['^httpd%.conf'] = detect_apache, + ['^httpd%-.*%.conf'] = detect_apache, + ['^proxy%-html%.conf'] = detect_apache, + ['^srm%.conf'] = detect_apache, ['asterisk/.*%.conf'] = starsetf('asterisk'), ['asterisk.*/.*voicemail%.conf'] = starsetf('asteriskvm'), ['^dictd.*%.conf$'] = 'dictdconf', @@ -2260,6 +2280,7 @@ local pattern = { ['^%.?neomuttrc'] = detect_neomuttrc, ['/%.neomutt/neomuttrc'] = detect_neomuttrc, ['^Neomuttrc'] = detect_neomuttrc, + ['%.neomuttdebug'] = 'neomuttlog', }, ['^%.'] = { ['^%.cshrc'] = detect.csh, @@ -2293,6 +2314,8 @@ local pattern = { ['^crontab%.'] = starsetf('crontab'), ['^cvs%d+$'] = 'cvs', ['^php%.ini%-'] = 'dosini', + ['^php%-fpm%.conf'] = 'dosini', + ['^www%.conf'] = 'dosini', ['^drac%.'] = starsetf('dracula'), ['/dtrace/.*%.d$'] = 'dtrace', ['esmtprc$'] = 'esmtprc', @@ -2354,7 +2377,7 @@ local pattern = { ['/app%-defaults/'] = starsetf('xdefaults'), ['^Xresources'] = starsetf('xdefaults'), -- Increase priority to run before the pattern below - ['^XF86Config%-4'] = starsetf(detect.xfree86_v4, { priority = -math.huge + 1 }), + ['^XF86Config%-4'] = starsetf(detect.xfree86_v4, -math.huge + 1), ['^XF86Config'] = starsetf(detect.xfree86_v3), ['Xmodmap$'] = 'xmodmap', ['xmodmap'] = starsetf('xmodmap'), @@ -2380,8 +2403,10 @@ local pattern = { --- @type table<string,vim.filetype.pattern_cache> local pattern_lookup = {} +--- @param a vim.filetype.mapping.sorted +--- @param b vim.filetype.mapping.sorted local function compare_by_priority(a, b) - return a[next(a)][2].priority > b[next(b)][2].priority + return a[4] > b[4] end --- @param pat string @@ -2391,30 +2416,30 @@ local function parse_pattern(pat) end --- @param t table<string,vim.filetype.mapping> ---- @return vim.filetype.mapping[] ---- @return vim.filetype.mapping[] +--- @return vim.filetype.mapping.sorted[] +--- @return vim.filetype.mapping.sorted[] local function sort_by_priority(t) -- Separate patterns with non-negative and negative priority because they -- will be processed separately - local pos = {} --- @type vim.filetype.mapping[] - local neg = {} --- @type vim.filetype.mapping[] + local pos = {} --- @type vim.filetype.mapping.sorted[] + local neg = {} --- @type vim.filetype.mapping.sorted[] for parent, ft_map in pairs(t) do pattern_lookup[parent] = pattern_lookup[parent] or parse_pattern(parent) for pat, maptbl in pairs(ft_map) do - local ft = type(maptbl) == 'table' and maptbl[1] or maptbl + local ft_or_fun = type(maptbl) == 'table' and maptbl[1] or maptbl assert( - type(ft) == 'string' or type(ft) == 'function', + type(ft_or_fun) == 'string' or type(ft_or_fun) == 'function', 'Expected string or function for filetype' ) -- Parse pattern for common data and cache it once pattern_lookup[pat] = pattern_lookup[pat] or parse_pattern(pat) - local opts = (type(maptbl) == 'table' and type(maptbl[2]) == 'table') and maptbl[2] or {} - opts.parent = opts.parent or parent - opts.priority = opts.priority or 0 + --- @type vim.filetype.mapopts? + local opts = (type(maptbl) == 'table' and type(maptbl[2]) == 'table') and maptbl[2] or nil + local priority = opts and opts.priority or 0 - table.insert(opts.priority >= 0 and pos or neg, { [pat] = { ft, opts } }) + table.insert(priority >= 0 and pos or neg, { parent, pat, ft_or_fun, priority }) end end @@ -2625,7 +2650,8 @@ local function match_pattern(name, path, tail, pat, try_all_candidates) if some_env_missing then return nil end - pat, has_slash = expanded, expanded:find('/') ~= nil + pat = expanded + has_slash = has_slash or expanded:find('/') ~= nil end -- Try all possible candidates to make parent patterns not depend on slash presence @@ -2647,14 +2673,13 @@ end --- @param name string --- @param path string --- @param tail string ---- @param pattern_sorted vim.filetype.mapping[] +--- @param pattern_sorted vim.filetype.mapping.sorted[] --- @param parent_matches table<string,boolean> --- @param bufnr integer? local function match_pattern_sorted(name, path, tail, pattern_sorted, parent_matches, bufnr) - for i = 1, #pattern_sorted do - local pat, ft_data = next(pattern_sorted[i]) + for _, p in ipairs(pattern_sorted) do + local parent, pat, ft_or_fn = p[1], p[2], p[3] - local parent = ft_data[2].parent local parent_is_matched = parent_matches[parent] if parent_is_matched == nil then parent_matches[parent] = match_pattern(name, path, tail, parent, true) ~= nil @@ -2664,7 +2689,7 @@ local function match_pattern_sorted(name, path, tail, pattern_sorted, parent_mat if parent_is_matched then local matches = match_pattern(name, path, tail, pat, false) if matches then - local ft, on_detect = dispatch(ft_data[1], path, bufnr, matches) + local ft, on_detect = dispatch(ft_or_fn, path, bufnr, matches) if ft then return ft, on_detect end @@ -2729,9 +2754,7 @@ end --- filetype specific buffer variables). The function accepts a buffer number as --- its only argument. function M.match(args) - vim.validate({ - arg = { args, 't' }, - }) + vim.validate('arg', args, 'table') if not (args.buf or args.filename or args.contents) then error('At least one of "buf", "filename", or "contents" must be given') diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua index 1cc81b177f..98b001bd51 100644 --- a/runtime/lua/vim/filetype/detect.lua +++ b/runtime/lua/vim/filetype/detect.lua @@ -869,6 +869,16 @@ function M.log(path, _) end --- @type vim.filetype.mapfn +function M.ll(_, bufnr) + local first_line = getline(bufnr, 1) + if matchregex(first_line, [[;\|\<source_filename\>\|\<target\>]]) then + return 'llvm' + else + return 'lifelines' + end +end + +--- @type vim.filetype.mapfn function M.lpc(_, bufnr) if vim.g.lpc_syntax_for_c then for _, line in ipairs(getlines(bufnr, 1, 12)) do @@ -1908,7 +1918,7 @@ local function match_from_hashbang(contents, path, dispatch_extension) end for k, v in pairs(patterns_hashbang) do - local ft = type(v) == 'table' and v[1] or v + local ft = type(v) == 'table' and v[1] or v --[[@as string]] local opts = type(v) == 'table' and v[2] or {} if opts.vim_regex and matchregex(name, k) or name:find(k) then return ft @@ -2080,6 +2090,7 @@ local function match_from_text(contents, path) return ft end else + --- @cast k string local opts = type(v) == 'table' and v[2] or {} if opts.start_lnum and opts.end_lnum then assert( diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index ccddf826f7..d91eeaf02f 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -53,7 +53,7 @@ function M.dirname(file) if file == nil then return nil end - vim.validate({ file = { file, 's' } }) + vim.validate('file', file, 'string') if iswin then file = file:gsub(os_sep, '/') --[[@as string]] if file:match('^%w:/?$') then @@ -83,7 +83,7 @@ function M.basename(file) if file == nil then return nil end - vim.validate({ file = { file, 's' } }) + vim.validate('file', file, 'string') if iswin then file = file:gsub(os_sep, '/') --[[@as string]] if file:match('^%w:/?$') then @@ -123,11 +123,9 @@ end function M.dir(path, opts) opts = opts or {} - vim.validate({ - path = { path, { 'string' } }, - depth = { opts.depth, { 'number' }, true }, - skip = { opts.skip, { 'function' }, true }, - }) + vim.validate('path', path, 'string') + vim.validate('depth', opts.depth, 'number', true) + vim.validate('skip', opts.skip, 'function', true) path = M.normalize(path) if not opts.depth or opts.depth == 1 then @@ -231,14 +229,12 @@ end ---@return (string[]) # Normalized paths |vim.fs.normalize()| of all matching items function M.find(names, opts) opts = opts or {} - vim.validate({ - names = { names, { 's', 't', 'f' } }, - path = { opts.path, 's', true }, - upward = { opts.upward, 'b', true }, - stop = { opts.stop, 's', true }, - type = { opts.type, 's', true }, - limit = { opts.limit, 'n', true }, - }) + vim.validate('names', names, { 'string', 'table', 'function' }) + vim.validate('path', opts.path, 'string', true) + vim.validate('upward', opts.upward, 'boolean', true) + vim.validate('stop', opts.stop, 'string', true) + vim.validate('type', opts.type, 'string', true) + vim.validate('limit', opts.limit, 'number', true) if type(names) == 'string' then names = { names } @@ -547,11 +543,9 @@ function M.normalize(path, opts) opts = opts or {} if not opts._fast then - vim.validate({ - path = { path, { 'string' } }, - expand_env = { opts.expand_env, { 'boolean' }, true }, - win = { opts.win, { 'boolean' }, true }, - }) + vim.validate('path', path, 'string') + vim.validate('expand_env', opts.expand_env, 'boolean', true) + vim.validate('win', opts.win, 'boolean', true) end local win = opts.win == nil and iswin or not not opts.win diff --git a/runtime/lua/vim/func/_memoize.lua b/runtime/lua/vim/func/_memoize.lua index 65210351bf..6e557905a7 100644 --- a/runtime/lua/vim/func/_memoize.lua +++ b/runtime/lua/vim/func/_memoize.lua @@ -39,10 +39,8 @@ end --- @param strong? boolean --- @return F return function(hash, fn, strong) - vim.validate({ - hash = { hash, { 'number', 'string', 'function' } }, - fn = { fn, 'function' }, - }) + vim.validate('hash', hash, { 'number', 'string', 'function' }) + vim.validate('fn', fn, 'function') ---@type table<any,table<any,any>> local cache = {} diff --git a/runtime/lua/vim/glob.lua b/runtime/lua/vim/glob.lua index 22073b15c8..4f86d5e1ca 100644 --- a/runtime/lua/vim/glob.lua +++ b/runtime/lua/vim/glob.lua @@ -48,7 +48,7 @@ function M.to_lpeg(pattern) end -- luacheck: push ignore s - local function cut(s, idx, match) + local function cut(_s, idx, match) return idx, match end -- luacheck: pop diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/hl.lua index a8d88db372..099efa3c61 100644 --- a/runtime/lua/vim/highlight.lua +++ b/runtime/lua/vim/hl.lua @@ -17,7 +17,7 @@ M.priorities = { user = 200, } ---- @class vim.highlight.range.Opts +--- @class vim.hl.range.Opts --- @inlinedoc --- --- Type of range. See [getregtype()] @@ -28,8 +28,8 @@ M.priorities = { --- (default: `false`) --- @field inclusive? boolean --- ---- Indicates priority of highlight ---- (default: `vim.highlight.priorities.user`) +--- Highlight priority +--- (default: `vim.hl.priorities.user`) --- @field priority? integer --- Apply highlight group to range of text. @@ -39,7 +39,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? vim.highlight.range.Opts +---@param opts? vim.hl.range.Opts function M.range(bufnr, ns, higroup, start, finish, opts) opts = opts or {} local regtype = opts.regtype or 'v' @@ -124,7 +124,7 @@ local yank_cancel --- @type fun()? --- Add the following to your `init.vim`: --- --- ```vim ---- autocmd TextYankPost * silent! lua vim.highlight.on_yank {higroup='Visual', timeout=300} +--- autocmd TextYankPost * silent! lua vim.hl.on_yank {higroup='Visual', timeout=300} --- ``` --- --- @param opts table|nil Optional parameters @@ -133,21 +133,9 @@ local yank_cancel --- @type fun()? --- - on_macro highlight when executing macro (default false) --- - on_visual highlight when yanking visual selection (default true) --- - event event structure (default vim.v.event) ---- - priority integer priority (default |vim.highlight.priorities|`.user`) +--- - priority integer priority (default |vim.hl.priorities|`.user`) function M.on_yank(opts) - vim.validate({ - opts = { - opts, - function(t) - if t == nil then - return true - else - return type(t) == 'table' - end - end, - 'a table or nil to configure options (see `:h highlight.on_yank`)', - }, - }) + vim.validate('opts', opts, 'table', true) opts = opts or {} local event = opts.event or vim.v.event local on_macro = opts.on_macro or false diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua index 50ca0d2d0e..4c19435ef8 100644 --- a/runtime/lua/vim/keymap.lua +++ b/runtime/lua/vim/keymap.lua @@ -42,12 +42,10 @@ local keymap = {} ---@see |mapcheck()| ---@see |mapset()| function keymap.set(mode, lhs, rhs, opts) - vim.validate({ - mode = { mode, { 's', 't' } }, - lhs = { lhs, 's' }, - rhs = { rhs, { 's', 'f' } }, - opts = { opts, 't', true }, - }) + vim.validate('mode', mode, { 'string', 'table' }) + vim.validate('lhs', lhs, 'string') + vim.validate('rhs', rhs, { 'string', 'function' }) + vim.validate('opts', opts, 'table', true) opts = vim.deepcopy(opts or {}, true) @@ -107,11 +105,9 @@ end ---@param opts? vim.keymap.del.Opts ---@see |vim.keymap.set()| function keymap.del(modes, lhs, opts) - vim.validate({ - mode = { modes, { 's', 't' } }, - lhs = { lhs, 's' }, - opts = { opts, 't', true }, - }) + vim.validate('mode', modes, { 'string', 'table' }) + vim.validate('lhs', lhs, 'string') + vim.validate('opts', opts, 'table', true) opts = opts or {} modes = type(modes) == 'string' and { modes } or modes diff --git a/runtime/lua/vim/loader.lua b/runtime/lua/vim/loader.lua index e86d33bf53..0cce0ab21d 100644 --- a/runtime/lua/vim/loader.lua +++ b/runtime/lua/vim/loader.lua @@ -4,11 +4,14 @@ local uri_encode = vim.uri_encode --- @type function --- @type (fun(modename: string): fun()|string)[] local loaders = package.loaders +local _loadfile = loadfile + +local VERSION = 4 local M = {} ----@alias CacheHash {mtime: {nsec: integer, sec: integer}, size: integer, type?: string} ----@alias CacheEntry {hash:CacheHash, chunk:string} +--- @alias vim.loader.CacheHash {mtime: {nsec: integer, sec: integer}, size: integer, type?: string} +--- @alias vim.loader.CacheEntry {hash:vim.loader.CacheHash, chunk:string} --- @class vim.loader.find.Opts --- @inlinedoc @@ -40,107 +43,97 @@ local M = {} --- @field modname string --- --- The fs_stat of the module path. Won't be returned for `modname="*"` ---- @field stat? uv.uv_fs_t +--- @field stat? uv.fs_stat.result ----@alias LoaderStats table<string, {total:number, time:number, [string]:number?}?> +--- @alias vim.loader.Stats table<string, {total:number, time:number, [string]:number?}?> ----@nodoc +--- @private M.path = vim.fn.stdpath('cache') .. '/luac' ----@nodoc +--- @private M.enabled = false ----@class (private) Loader ----@field private _rtp string[] ----@field private _rtp_pure string[] ----@field private _rtp_key string ----@field private _hashes? table<string, CacheHash> -local Loader = { - VERSION = 4, - ---@type table<string, table<string,vim.loader.ModuleInfo>> - _indexed = {}, - ---@type table<string, string[]> - _topmods = {}, - _loadfile = loadfile, - ---@type LoaderStats - _stats = { - find = { total = 0, time = 0, not_found = 0 }, - }, -} +--- @type vim.loader.Stats +local stats = { find = { total = 0, time = 0, not_found = 0 } } + +--- @type table<string, uv.fs_stat.result>? +local fs_stat_cache + +--- @type table<string, table<string,vim.loader.ModuleInfo>> +local indexed = {} --- @param path string ---- @return CacheHash ---- @private -function Loader.get_hash(path) - if not Loader._hashes then - return uv.fs_stat(path) --[[@as CacheHash]] +--- @return uv.fs_stat.result? +local function fs_stat_cached(path) + if not fs_stat_cache then + return uv.fs_stat(path) end - if not Loader._hashes[path] then + if not fs_stat_cache[path] then -- Note we must never save a stat for a non-existent path. -- For non-existent paths fs_stat() will return nil. - Loader._hashes[path] = uv.fs_stat(path) + fs_stat_cache[path] = uv.fs_stat(path) end - return Loader._hashes[path] + return fs_stat_cache[path] end local function normalize(path) return fs.normalize(path, { expand_env = false, _fast = true }) end +local rtp_cached = {} --- @type string[] +local rtp_cache_key --- @type string? + --- Gets the rtp excluding after directories. --- The result is cached, and will be updated if the runtime path changes. --- When called from a fast event, the cached value will be returned. --- @return string[] rtp, boolean updated ----@private -function Loader.get_rtp() +local function get_rtp() if vim.in_fast_event() then - return (Loader._rtp or {}), false + return (rtp_cached or {}), false end local updated = false local key = vim.go.rtp - if key ~= Loader._rtp_key then - Loader._rtp = {} + if key ~= rtp_cache_key then + rtp_cached = {} for _, path in ipairs(vim.api.nvim_get_runtime_file('', true)) do path = normalize(path) -- skip after directories if path:sub(-6, -1) ~= '/after' - and not (Loader._indexed[path] and vim.tbl_isempty(Loader._indexed[path])) + and not (indexed[path] and vim.tbl_isempty(indexed[path])) then - Loader._rtp[#Loader._rtp + 1] = path + rtp_cached[#rtp_cached + 1] = path end end updated = true - Loader._rtp_key = key + rtp_cache_key = key end - return Loader._rtp, updated + return rtp_cached, updated end --- Returns the cache file name ----@param name string can be a module name, or a file name ----@return string file_name ----@private -function Loader.cache_file(name) +--- @param name string can be a module name, or a file name +--- @return string file_name +local function cache_filename(name) local ret = ('%s/%s'):format(M.path, uri_encode(name, 'rfc2396')) return ret:sub(-4) == '.lua' and (ret .. 'c') or (ret .. '.luac') end --- Saves the cache entry for a given module or file ----@param name string module name or filename ----@param entry CacheEntry ----@private -function Loader.write(name, entry) - local cname = Loader.cache_file(name) +--- @param cname string cache filename +--- @param hash vim.loader.CacheHash +--- @param chunk function +local function write_cachefile(cname, hash, chunk) local f = assert(uv.fs_open(cname, 'w', 438)) local header = { - Loader.VERSION, - entry.hash.size, - entry.hash.mtime.sec, - entry.hash.mtime.nsec, + VERSION, + hash.size, + hash.mtime.sec, + hash.mtime.nsec, } uv.fs_write(f, table.concat(header, ',') .. '\0') - uv.fs_write(f, entry.chunk) + uv.fs_write(f, string.dump(chunk)) uv.fs_close(f) end @@ -150,151 +143,159 @@ end local function readfile(path, mode) local f = uv.fs_open(path, 'r', mode) if f then - local hash = assert(uv.fs_fstat(f)) - local data = uv.fs_read(f, hash.size, 0) --[[@as string?]] + local size = assert(uv.fs_fstat(f)).size + local data = uv.fs_read(f, size, 0) uv.fs_close(f) return data end end --- Loads the cache entry for a given module or file ----@param name string module name or filename ----@return CacheEntry? ----@private -function Loader.read(name) - local cname = Loader.cache_file(name) +--- @param cname string cache filename +--- @return vim.loader.CacheHash? hash +--- @return string? chunk +local function read_cachefile(cname) local data = readfile(cname, 438) - if data then - local zero = data:find('\0', 1, true) - if not zero then - return - end + if not data then + return + end - ---@type integer[]|{[0]:integer} - local header = vim.split(data:sub(1, zero - 1), ',') - if tonumber(header[1]) ~= Loader.VERSION then - return - end - return { - hash = { - size = tonumber(header[2]), - mtime = { sec = tonumber(header[3]), nsec = tonumber(header[4]) }, - }, - chunk = data:sub(zero + 1), - } + local zero = data:find('\0', 1, true) + if not zero then + return + end + + --- @type integer[]|{[0]:integer} + local header = vim.split(data:sub(1, zero - 1), ',') + if tonumber(header[1]) ~= VERSION then + return end + + local hash = { + size = tonumber(header[2]), + mtime = { sec = tonumber(header[3]), nsec = tonumber(header[4]) }, + } + + local chunk = data:sub(zero + 1) + + return hash, chunk end --- The `package.loaders` loader for Lua files using the cache. ----@param modname string module name ----@return string|function ----@private -function Loader.loader(modname) - Loader._hashes = {} +--- @param modname string module name +--- @return string|function +local function loader_cached(modname) + fs_stat_cache = {} local ret = M.find(modname)[1] if ret then -- Make sure to call the global loadfile so we respect any augmentations done elsewhere. -- E.g. profiling local chunk, err = loadfile(ret.modpath) - Loader._hashes = nil + fs_stat_cache = nil return chunk or error(err) end - Loader._hashes = nil + fs_stat_cache = nil return ("\n\tcache_loader: module '%s' not found"):format(modname) end +local is_win = vim.fn.has('win32') == 1 + --- The `package.loaders` loader for libs ----@param modname string module name ----@return string|function ----@private -function Loader.loader_lib(modname) - local is_win = vim.fn.has('win32') == 1 - local ret = M.find(modname, { patterns = is_win and { '.dll' } or { '.so' } })[1] - if ret then - -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is - -- a) strip prefix up to and including the first dash, if any - -- b) replace all dots by underscores - -- c) prepend "luaopen_" - -- So "foo-bar.baz" should result in "luaopen_bar_baz" - local dash = modname:find('-', 1, true) - local funcname = dash and modname:sub(dash + 1) or modname - local chunk, err = package.loadlib(ret.modpath, 'luaopen_' .. funcname:gsub('%.', '_')) - return chunk or error(err) +--- @param modname string module name +--- @return string|function +local function loader_lib_cached(modname) + local ret = M.find(modname, { patterns = { is_win and '.dll' or '.so' } })[1] + if not ret then + return ("\n\tcache_loader_lib: module '%s' not found"):format(modname) end - return ("\n\tcache_loader_lib: module '%s' not found"):format(modname) -end ---- `loadfile` using the cache ---- Note this has the mode and env arguments which is supported by LuaJIT and is 5.1 compatible. ----@param filename? string ----@param _mode? "b"|"t"|"bt" ----@param env? table ----@return function?, string? error_message ----@private -function Loader.loadfile(filename, _mode, env) - -- ignore mode, since we byte-compile the Lua source files - return Loader.load(normalize(filename), { env = env }) + -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is + -- a) strip prefix up to and including the first dash, if any + -- b) replace all dots by underscores + -- c) prepend "luaopen_" + -- So "foo-bar.baz" should result in "luaopen_bar_baz" + local dash = modname:find('-', 1, true) + local funcname = dash and modname:sub(dash + 1) or modname + local chunk, err = package.loadlib(ret.modpath, 'luaopen_' .. funcname:gsub('%.', '_')) + return chunk or error(err) end --- Checks whether two cache hashes are the same based on: --- * file size --- * mtime in seconds --- * mtime in nanoseconds ----@param h1 CacheHash ----@param h2 CacheHash ----@private -function Loader.eq(h1, h2) - return h1 - and h2 - and h1.size == h2.size - and h1.mtime.sec == h2.mtime.sec - and h1.mtime.nsec == h2.mtime.nsec +--- @param a? vim.loader.CacheHash +--- @param b? vim.loader.CacheHash +local function hash_eq(a, b) + return a + and b + and a.size == b.size + and a.mtime.sec == b.mtime.sec + and a.mtime.nsec == b.mtime.nsec end ---- Loads the given module path using the cache ----@param modpath string ----@param opts? {mode?: "b"|"t"|"bt", env?:table} (table|nil) Options for loading the module: ---- - mode: (string) the mode to load the module with. "b"|"t"|"bt" (defaults to `nil`) ---- - env: (table) the environment to load the module in. (defaults to `nil`) ----@see |luaL_loadfile()| ----@return function?, string? error_message ----@private -function Loader.load(modpath, opts) - opts = opts or {} - local hash = Loader.get_hash(modpath) - ---@type function?, string? - local chunk, err - - if not hash then - -- trigger correct error - return Loader._loadfile(modpath, opts.mode, opts.env) - end - - local entry = Loader.read(modpath) - if entry and Loader.eq(entry.hash, hash) then - -- found in cache and up to date - chunk, err = load(entry.chunk --[[@as string]], '@' .. modpath, opts.mode, opts.env) - if not (err and err:find('cannot load incompatible bytecode', 1, true)) then - return chunk, err +--- `loadfile` using the cache +--- Note this has the mode and env arguments which is supported by LuaJIT and is 5.1 compatible. +--- @param filename? string +--- @param mode? "b"|"t"|"bt" +--- @param env? table +--- @return function?, string? error_message +local function loadfile_cached(filename, mode, env) + local modpath = normalize(filename) + local stat = fs_stat_cached(modpath) + local cname = cache_filename(modpath) + if stat then + local e_hash, e_chunk = read_cachefile(cname) + if hash_eq(e_hash, stat) and e_chunk then + -- found in cache and up to date + local chunk, err = load(e_chunk, '@' .. modpath, mode, env) + if not (err and err:find('cannot load incompatible bytecode', 1, true)) then + return chunk, err + end end end - entry = { hash = hash, modpath = modpath } - chunk, err = Loader._loadfile(modpath, opts.mode, opts.env) - if chunk then - entry.chunk = string.dump(chunk) - Loader.write(modpath, entry) + local chunk, err = _loadfile(modpath, mode, env) + if chunk and stat then + write_cachefile(cname, stat, chunk) end return chunk, err end +--- Return the top-level \`/lua/*` modules for this path +--- @param path string path to check for top-level Lua modules +local function lsmod(path) + if not indexed[path] then + indexed[path] = {} + for name, t in fs.dir(path .. '/lua') do + local modpath = path .. '/lua/' .. name + -- HACK: type is not always returned due to a bug in luv + t = t or fs_stat_cached(modpath).type + --- @type string + local topname + local ext = name:sub(-4) + if ext == '.lua' or ext == '.dll' then + topname = name:sub(1, -5) + elseif name:sub(-3) == '.so' then + topname = name:sub(1, -4) + elseif t == 'link' or t == 'directory' then + topname = name + end + if topname then + indexed[path][topname] = { modpath = modpath, modname = topname } + end + end + end + return indexed[path] +end + --- Finds Lua modules for the given module name. --- --- @since 0 --- ----@param modname string Module name, or `"*"` to find the top-level modules instead ----@param opts? vim.loader.find.Opts Options for finding a module: ----@return vim.loader.ModuleInfo[] +--- @param modname string Module name, or `"*"` to find the top-level modules instead +--- @param opts? vim.loader.find.Opts Options for finding a module: +--- @return vim.loader.ModuleInfo[] function M.find(modname, opts) opts = opts or {} @@ -320,7 +321,7 @@ function M.find(modname, opts) patterns[p] = '/lua/' .. basename .. pattern end - ---@type vim.loader.ModuleInfo[] + --- @type vim.loader.ModuleInfo[] local results = {} -- Only continue if we haven't found anything yet or we want to find all @@ -330,23 +331,23 @@ function M.find(modname, opts) -- Checks if the given paths contain the top-level module. -- If so, it tries to find the module path for the given module name. - ---@param paths string[] + --- @param paths string[] local function _find(paths) for _, path in ipairs(paths) do if topmod == '*' then - for _, r in pairs(Loader.lsmod(path)) do + for _, r in pairs(lsmod(path)) do results[#results + 1] = r if not continue() then return end end - elseif Loader.lsmod(path)[topmod] then + elseif lsmod(path)[topmod] then for _, pattern in ipairs(patterns) do local modpath = path .. pattern - Loader._stats.find.stat = (Loader._stats.find.stat or 0) + 1 - local hash = Loader.get_hash(modpath) - if hash then - results[#results + 1] = { modpath = modpath, stat = hash, modname = modname } + stats.find.stat = (stats.find.stat or 0) + 1 + local stat = fs_stat_cached(modpath) + if stat then + results[#results + 1] = { modpath = modpath, stat = stat, modname = modname } if not continue() then return end @@ -358,9 +359,9 @@ function M.find(modname, opts) -- always check the rtp first if opts.rtp ~= false then - _find(Loader._rtp or {}) + _find(rtp_cached or {}) if continue() then - local rtp, updated = Loader.get_rtp() + local rtp, updated = get_rtp() if updated then _find(rtp) end @@ -374,7 +375,7 @@ function M.find(modname, opts) if #results == 0 then -- module not found - Loader._stats.find.not_found = Loader._stats.find.not_found + 1 + stats.find.not_found = stats.find.not_found + 1 end return results @@ -384,17 +385,17 @@ end --- --- @since 0 --- ----@param path string? path to reset +--- @param path string? path to reset function M.reset(path) if path then - Loader._indexed[normalize(path)] = nil + indexed[normalize(path)] = nil else - Loader._indexed = {} + indexed = {} end -- Path could be a directory so just clear all the hashes. - if Loader._hashes then - Loader._hashes = {} + if fs_stat_cache then + fs_stat_cache = {} end end @@ -411,11 +412,11 @@ function M.enable() end M.enabled = true vim.fn.mkdir(vim.fn.fnamemodify(M.path, ':p'), 'p') - _G.loadfile = Loader.loadfile + _G.loadfile = loadfile_cached -- add Lua loader - table.insert(loaders, 2, Loader.loader) + table.insert(loaders, 2, loader_cached) -- add libs loader - table.insert(loaders, 3, Loader.loader_lib) + table.insert(loaders, 3, loader_lib_cached) -- remove Nvim loader for l, loader in ipairs(loaders) do if loader == vim._load_package then @@ -435,111 +436,75 @@ function M.disable() return end M.enabled = false - _G.loadfile = Loader._loadfile + _G.loadfile = _loadfile for l, loader in ipairs(loaders) do - if loader == Loader.loader or loader == Loader.loader_lib then + if loader == loader_cached or loader == loader_lib_cached then table.remove(loaders, l) end end table.insert(loaders, 2, vim._load_package) end ---- Return the top-level \`/lua/*` modules for this path ----@param path string path to check for top-level Lua modules ----@private -function Loader.lsmod(path) - if not Loader._indexed[path] then - Loader._indexed[path] = {} - for name, t in fs.dir(path .. '/lua') do - local modpath = path .. '/lua/' .. name - -- HACK: type is not always returned due to a bug in luv - t = t or Loader.get_hash(modpath).type - ---@type string - local topname - local ext = name:sub(-4) - if ext == '.lua' or ext == '.dll' then - topname = name:sub(1, -5) - elseif name:sub(-3) == '.so' then - topname = name:sub(1, -4) - elseif t == 'link' or t == 'directory' then - topname = name - end - if topname then - Loader._indexed[path][topname] = { modpath = modpath, modname = topname } - Loader._topmods[topname] = Loader._topmods[topname] or {} - if not vim.list_contains(Loader._topmods[topname], path) then - table.insert(Loader._topmods[topname], path) - end - end - end - end - return Loader._indexed[path] -end - --- Tracks the time spent in a function --- @generic F: function --- @param f F --- @return F ---- @private -function Loader.track(stat, f) +local function track(stat, f) return function(...) local start = vim.uv.hrtime() local r = { f(...) } - Loader._stats[stat] = Loader._stats[stat] or { total = 0, time = 0 } - Loader._stats[stat].total = Loader._stats[stat].total + 1 - Loader._stats[stat].time = Loader._stats[stat].time + uv.hrtime() - start + stats[stat] = stats[stat] or { total = 0, time = 0 } + stats[stat].total = stats[stat].total + 1 + stats[stat].time = stats[stat].time + uv.hrtime() - start return unpack(r, 1, table.maxn(r)) end end ----@class (private) vim.loader._profile.Opts ----@field loaders? boolean Add profiling to the loaders +--- @class (private) vim.loader._profile.Opts +--- @field loaders? boolean Add profiling to the loaders --- Debug function that wraps all loaders and tracks stats ----@private ----@param opts vim.loader._profile.Opts? +--- Must be called before vim.loader.enable() +--- @private +--- @param opts vim.loader._profile.Opts? function M._profile(opts) - Loader.get_rtp = Loader.track('get_rtp', Loader.get_rtp) - Loader.read = Loader.track('read', Loader.read) - Loader.loader = Loader.track('loader', Loader.loader) - Loader.loader_lib = Loader.track('loader_lib', Loader.loader_lib) - Loader.loadfile = Loader.track('loadfile', Loader.loadfile) - Loader.load = Loader.track('load', Loader.load) - M.find = Loader.track('find', M.find) - Loader.lsmod = Loader.track('lsmod', Loader.lsmod) + get_rtp = track('get_rtp', get_rtp) + read_cachefile = track('read', read_cachefile) + loader_cached = track('loader', loader_cached) + loader_lib_cached = track('loader_lib', loader_lib_cached) + loadfile_cached = track('loadfile', loadfile_cached) + M.find = track('find', M.find) + lsmod = track('lsmod', lsmod) if opts and opts.loaders then for l, loader in pairs(loaders) do local loc = debug.getinfo(loader, 'Sn').source:sub(2) - loaders[l] = Loader.track('loader ' .. l .. ': ' .. loc, loader) + loaders[l] = track('loader ' .. l .. ': ' .. loc, loader) end end end --- Prints all cache stats ----@param opts? {print?:boolean} ----@return LoaderStats ----@private +--- @param opts? {print?:boolean} +--- @return vim.loader.Stats +--- @private function M._inspect(opts) if opts and opts.print then local function ms(nsec) return math.floor(nsec / 1e6 * 1000 + 0.5) / 1000 .. 'ms' end - local chunks = {} ---@type string[][] - ---@type string[] - local stats = vim.tbl_keys(Loader._stats) - table.sort(stats) - for _, stat in ipairs(stats) do + local chunks = {} --- @type string[][] + for _, stat in vim.spairs(stats) do vim.list_extend(chunks, { { '\n' .. stat .. '\n', 'Title' }, { '* total: ' }, - { tostring(Loader._stats[stat].total) .. '\n', 'Number' }, + { tostring(stat.total) .. '\n', 'Number' }, { '* time: ' }, - { ms(Loader._stats[stat].time) .. '\n', 'Bold' }, + { ms(stat.time) .. '\n', 'Bold' }, { '* avg time: ' }, - { ms(Loader._stats[stat].time / Loader._stats[stat].total) .. '\n', 'Bold' }, + { ms(stat.time / stat.total) .. '\n', 'Bold' }, }) - for k, v in pairs(Loader._stats[stat]) do + for k, v in pairs(stat) do if not vim.list_contains({ 'time', 'total' }, k) then chunks[#chunks + 1] = { '* ' .. k .. ':' .. string.rep(' ', 9 - #k) } chunks[#chunks + 1] = { tostring(v) .. '\n', 'Number' } @@ -548,7 +513,7 @@ function M._inspect(opts) end vim.api.nvim_echo(chunks, true, {}) end - return Loader._stats + return stats end return M diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 60677554ce..0de3b4ee4d 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -3,7 +3,6 @@ local validate = vim.validate local lsp = vim._defer_require('vim.lsp', { _changetracking = ..., --- @module 'vim.lsp._changetracking' - _dynamic = ..., --- @module 'vim.lsp._dynamic' _snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar' _tagfunc = ..., --- @module 'vim.lsp._tagfunc' _watchfiles = ..., --- @module 'vim.lsp._watchfiles' @@ -31,6 +30,13 @@ local changetracking = lsp._changetracking ---@nodoc lsp.rpc_response_error = lsp.rpc.rpc_response_error +lsp._resolve_to_request = { + [ms.codeAction_resolve] = ms.textDocument_codeAction, + [ms.codeLens_resolve] = ms.textDocument_codeLens, + [ms.documentLink_resolve] = ms.textDocument_documentLink, + [ms.inlayHint_resolve] = ms.textDocument_inlayHint, +} + -- maps request name to the required server_capability in the client. lsp._request_name_to_capability = { [ms.callHierarchy_incomingCalls] = { 'callHierarchyProvider' }, @@ -86,7 +92,7 @@ lsp._request_name_to_capability = { ---@param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer ---@return integer bufnr local function resolve_bufnr(bufnr) - validate({ bufnr = { bufnr, 'n', true } }) + validate('bufnr', bufnr, 'number', true) if bufnr == nil or bufnr == 0 then return api.nvim_get_current_buf() end @@ -189,9 +195,10 @@ local function reuse_client_default(client, config) end if config.root_dir then + local root = vim.uri_from_fname(config.root_dir) for _, dir in ipairs(client.workspace_folders or {}) do -- note: do not need to check client.root_dir since that should be client.workspace_folders[1] - if config.root_dir == dir.name then + if root == dir.uri then return true end end @@ -235,9 +242,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.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. +--- should be re-used. The example above uses |vim.fs.root()| to detect the root by traversing +--- the file system upwards starting from the current directory until either a `pyproject.toml` +--- 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. @@ -630,10 +637,8 @@ end ---@param client_id (integer) Client id ---@return boolean success `true` if client was attached successfully; `false` otherwise function lsp.buf_attach_client(bufnr, client_id) - validate({ - bufnr = { bufnr, 'n', true }, - client_id = { client_id, 'n' }, - }) + validate('bufnr', bufnr, 'number', true) + validate('client_id', client_id, 'number') bufnr = resolve_bufnr(bufnr) if not api.nvim_buf_is_loaded(bufnr) then log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr)) @@ -669,10 +674,8 @@ end ---@param bufnr integer Buffer handle, or 0 for current ---@param client_id integer Client id function lsp.buf_detach_client(bufnr, client_id) - validate({ - bufnr = { bufnr, 'n', true }, - client_id = { client_id, 'n' }, - }) + validate('bufnr', bufnr, 'number', true) + validate('client_id', client_id, 'number') bufnr = resolve_bufnr(bufnr) local client = all_clients[client_id] @@ -773,7 +776,7 @@ end ---@param filter? vim.lsp.get_clients.Filter ---@return vim.lsp.Client[]: List of |vim.lsp.Client| objects function lsp.get_clients(filter) - validate({ filter = { filter, 't', true } }) + validate('filter', filter, 'table', true) filter = filter or {} @@ -858,7 +861,7 @@ api.nvim_create_autocmd('VimLeavePre', { --- ---@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 params? table|(fun(client: vim.lsp.Client, bufnr: integer): table?) Parameters to send to the server ---@param handler? lsp.Handler See |lsp-handler| --- If nil, follows resolution strategy defined in |lsp-handler-configuration| ---@param on_unsupported? fun() @@ -870,12 +873,10 @@ api.nvim_create_autocmd('VimLeavePre', { ---cancel all the requests. You could instead ---iterate all clients and call their `cancel_request()` methods. function lsp.buf_request(bufnr, method, params, handler, on_unsupported) - validate({ - bufnr = { bufnr, 'n', true }, - method = { method, 's' }, - handler = { handler, 'f', true }, - on_unsupported = { on_unsupported, 'f', true }, - }) + validate('bufnr', bufnr, 'number', true) + validate('method', method, 'string') + validate('handler', handler, 'function', true) + validate('on_unsupported', on_unsupported, 'function', true) bufnr = resolve_bufnr(bufnr) local method_supported = false @@ -885,7 +886,8 @@ function lsp.buf_request(bufnr, method, params, handler, on_unsupported) if client.supports_method(method, { bufnr = bufnr }) then method_supported = true - local request_success, request_id = client.request(method, params, handler, bufnr) + local cparams = type(params) == 'function' and params(client, bufnr) or params --[[@as table?]] + local request_success, request_id = client.request(method, cparams, handler, bufnr) -- This could only fail if the client shut down in the time since we looked -- it up and we did the request, which should be rare. if request_success then @@ -920,35 +922,31 @@ 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 params? table|(fun(client: vim.lsp.Client, bufnr: integer): table?) Parameters to send to the server. +--- Can also be passed as a function that returns the params table for cases where +--- parameters are specific to the client. +---@param handler lsp.MultiHandler (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: lsp.ResponseError?, result: any}> - local result_count = 0 - local expected_result_count = 0 - - local set_expected_result_count = once(function() - for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do - if client.supports_method(method, { bufnr = bufnr }) then - expected_result_count = expected_result_count + 1 - end + local results = {} --- @type table<integer,{err: lsp.ResponseError?, result: any}> + local remaining --- @type integer? + + local _, cancel = lsp.buf_request(bufnr, method, params, function(err, result, ctx, config) + if not remaining then + -- Calculate as late as possible in case a client is removed during the request + remaining = #lsp.get_clients({ bufnr = bufnr, method = method }) end - end) - local function _sync_handler(err, result, ctx) - results[ctx.client_id] = { error = err, result = result } - result_count = result_count + 1 - set_expected_result_count() + -- The error key is deprecated and will be removed in 0.13 + results[ctx.client_id] = { err = err, error = err, result = result } + remaining = remaining - 1 - if result_count >= expected_result_count then - handler(results) + if remaining == 0 then + handler(results, ctx, config) end - end - - local _, cancel = lsp.buf_request(bufnr, method, params, _sync_handler) + end) return cancel end @@ -992,10 +990,8 @@ end --- ---@return boolean success true if any client returns true; false otherwise function lsp.buf_notify(bufnr, method, params) - validate({ - bufnr = { bufnr, 'n', true }, - method = { method, 's' }, - }) + validate('bufnr', bufnr, 'number', true) + validate('method', method, 'string') local resp = false for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do if client.rpc.notify(method, params) then @@ -1056,7 +1052,7 @@ function lsp.formatexpr(opts) if client.supports_method(ms.textDocument_rangeFormatting) then local params = util.make_formatting_params() local end_line = vim.fn.getline(end_lnum) --[[@as string]] - local end_col = util._str_utfindex_enc(end_line, nil, client.offset_encoding) + local end_col = vim.str_utfindex(end_line, client.offset_encoding) --- @cast params +lsp.DocumentRangeFormattingParams params.range = { start = { @@ -1175,6 +1171,7 @@ function lsp.for_each_buffer_client(bufnr, fn) end end +--- @deprecated --- Function to manage overriding defaults for LSP handlers. ---@param handler (lsp.Handler) See |lsp-handler| ---@param override_config (table) Table containing the keys to override behavior of the {handler} diff --git a/runtime/lua/vim/lsp/_dynamic.lua b/runtime/lua/vim/lsp/_dynamic.lua deleted file mode 100644 index 27113c0e74..0000000000 --- a/runtime/lua/vim/lsp/_dynamic.lua +++ /dev/null @@ -1,110 +0,0 @@ -local glob = vim.glob - ---- @class lsp.DynamicCapabilities ---- @field capabilities table<string, lsp.Registration[]> ---- @field client_id number -local M = {} - ---- @param client_id number ---- @return lsp.DynamicCapabilities -function M.new(client_id) - return setmetatable({ - capabilities = {}, - client_id = client_id, - }, { __index = M }) -end - -function M:supports_registration(method) - local client = vim.lsp.get_client_by_id(self.client_id) - if not client then - return false - end - local capability = vim.tbl_get(client.capabilities, unpack(vim.split(method, '/'))) - return type(capability) == 'table' and capability.dynamicRegistration -end - ---- @param registrations lsp.Registration[] -function M:register(registrations) - -- remove duplicates - self:unregister(registrations) - for _, reg in ipairs(registrations) do - local method = reg.method - if not self.capabilities[method] then - self.capabilities[method] = {} - end - table.insert(self.capabilities[method], reg) - end -end - ---- @param unregisterations lsp.Unregistration[] -function M:unregister(unregisterations) - for _, unreg in ipairs(unregisterations) do - local method = unreg.method - if not self.capabilities[method] then - return - end - local id = unreg.id - for i, reg in ipairs(self.capabilities[method]) do - if reg.id == id then - table.remove(self.capabilities[method], i) - break - end - end - end -end - ---- @param method string ---- @param opts? {bufnr: integer?} ---- @return lsp.Registration? (table|nil) the registration if found -function M:get(method, opts) - opts = opts or {} - opts.bufnr = opts.bufnr or vim.api.nvim_get_current_buf() - for _, reg in ipairs(self.capabilities[method] or {}) do - if not reg.registerOptions then - return reg - end - local documentSelector = reg.registerOptions.documentSelector - if not documentSelector then - return reg - end - if self:match(opts.bufnr, documentSelector) then - return reg - end - end -end - ---- @param method string ---- @param opts? {bufnr: integer?} -function M:supports(method, opts) - return self:get(method, opts) ~= nil -end - ---- @param bufnr number ---- @param documentSelector lsp.DocumentSelector ---- @private -function M:match(bufnr, documentSelector) - local client = vim.lsp.get_client_by_id(self.client_id) - if not client then - return false - end - local language = client.get_language_id(bufnr, vim.bo[bufnr].filetype) - local uri = vim.uri_from_bufnr(bufnr) - local fname = vim.uri_to_fname(uri) - for _, filter in ipairs(documentSelector) do - local matches = true - if filter.language and language ~= filter.language then - matches = false - end - if matches and filter.scheme and not vim.startswith(uri, filter.scheme .. ':') then - matches = false - end - if matches and filter.pattern and not glob.to_lpeg(filter.pattern):match(fname) then - matches = false - end - if matches then - return true - end - end -end - -return M diff --git a/runtime/lua/vim/lsp/_meta.lua b/runtime/lua/vim/lsp/_meta.lua index be3222828d..bf693ccc57 100644 --- a/runtime/lua/vim/lsp/_meta.lua +++ b/runtime/lua/vim/lsp/_meta.lua @@ -1,7 +1,8 @@ ---@meta error('Cannot require a meta file') ----@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext, config?: table): ...any +---@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext): ...any +---@alias lsp.MultiHandler fun(results: table<integer,{err: lsp.ResponseError?, result: any}>, context: lsp.HandlerContext): ...any ---@class lsp.HandlerContext ---@field method string diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua index 4ad50e4a58..f75d43f373 100644 --- a/runtime/lua/vim/lsp/_tagfunc.lua +++ b/runtime/lua/vim/lsp/_tagfunc.lua @@ -1,4 +1,5 @@ local lsp = vim.lsp +local api = vim.api local util = lsp.util local ms = lsp.protocol.Methods @@ -21,32 +22,48 @@ end ---@param pattern string ---@return table[] local function query_definition(pattern) - local params = util.make_position_params() - local results_by_client, err = lsp.buf_request_sync(0, ms.textDocument_definition, params, 1000) - if err then + local bufnr = api.nvim_get_current_buf() + local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_definition }) + if not next(clients) then return {} end + local win = api.nvim_get_current_win() local results = {} + + --- @param range lsp.Range + --- @param uri string + --- @param offset_encoding string local add = function(range, uri, offset_encoding) table.insert(results, mk_tag_item(pattern, range, uri, offset_encoding)) end - for client_id, lsp_results in pairs(assert(results_by_client)) do - local client = lsp.get_client_by_id(client_id) - local offset_encoding = client and client.offset_encoding or 'utf-16' - local result = lsp_results.result or {} - if result.range then -- Location - add(result.range, result.uri) - else - result = result --[[@as (lsp.Location[]|lsp.LocationLink[])]] - for _, item in pairs(result) do - if item.range then -- Location - add(item.range, item.uri, offset_encoding) - else -- LocationLink - add(item.targetSelectionRange, item.targetUri, offset_encoding) + + local remaining = #clients + for _, client in ipairs(clients) do + ---@param result nil|lsp.Location|lsp.Location[]|lsp.LocationLink[] + local function on_response(_, result) + if result then + local encoding = client.offset_encoding + -- single Location + if result.range then + add(result.range, result.uri, encoding) + else + for _, location in ipairs(result) do + if location.range then -- Location + add(location.range, location.uri, encoding) + else -- LocationLink + add(location.targetSelectionRange, location.targetUri, encoding) + end + end end end + remaining = remaining - 1 end + local params = util.make_position_params(win, client.offset_encoding) + client.request(ms.textDocument_definition, params, on_response, bufnr) end + vim.wait(1000, function() + return remaining == 0 + end) return results end diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 98e9818bcd..c4cdb5aea8 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -44,9 +44,8 @@ M._poll_exclude_pattern = glob.to_lpeg('**/.git/{objects,subtree-cache}/**') --- Registers the workspace/didChangeWatchedFiles capability dynamically. --- ---@param reg lsp.Registration LSP Registration object. ----@param ctx lsp.HandlerContext Context from the |lsp-handler|. -function M.register(reg, ctx) - local client_id = ctx.client_id +---@param client_id integer Client ID. +function M.register(reg, client_id) local client = assert(vim.lsp.get_client_by_id(client_id), 'Client must be running') -- Ill-behaved servers may not honor the client capability and try to register -- anyway, so ignore requests when the user has opted out of the feature. @@ -155,9 +154,8 @@ end --- Unregisters the workspace/didChangeWatchedFiles capability dynamically. --- ---@param unreg lsp.Unregistration LSP Unregistration object. ----@param ctx lsp.HandlerContext Context from the |lsp-handler|. -function M.unregister(unreg, ctx) - local client_id = ctx.client_id +---@param client_id integer Client ID. +function M.unregister(unreg, client_id) local client_cancels = cancels[client_id] local reg_cancels = client_cancels[unreg.id] while #reg_cancels > 0 do diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 301c1f0cb6..6383855a30 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1,4 +1,5 @@ local api = vim.api +local lsp = vim.lsp local validate = vim.validate local util = require('vim.lsp.util') local npcall = vim.F.npcall @@ -6,28 +7,24 @@ local ms = require('vim.lsp.protocol').Methods local M = {} ---- Sends an async request to all active clients attached to the current ---- buffer. ---- ----@param method (string) LSP method name ----@param params (table|nil) Parameters to send to the server ----@param handler lsp.Handler? See |lsp-handler|. Follows |lsp-handler-resolution| ---- ----@return table<integer, integer> client_request_ids Map of client-id:request-id pairs ----for all successful requests. ----@return function _cancel_all_requests Function which can be used to ----cancel all the requests. You could instead ----iterate all clients and call their `cancel_request()` methods. ---- ----@see |vim.lsp.buf_request()| -local function request(method, params, handler) - validate({ - method = { method, 's' }, - handler = { handler, 'f', true }, - }) - return vim.lsp.buf_request(0, method, params, handler) +--- @param params? table +--- @return fun(client: vim.lsp.Client): lsp.TextDocumentPositionParams +local function client_positional_params(params) + local win = api.nvim_get_current_win() + return function(client) + local ret = util.make_position_params(win, client.offset_encoding) + if params then + ret = vim.tbl_extend('force', ret, params) + end + return ret + end end +local hover_ns = api.nvim_create_namespace('vim_lsp_hover_range') + +--- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts +--- @field silent? boolean + --- Displays hover information about the symbol under the cursor in a floating --- window. The window will be dismissed on cursor move. --- Calling the function twice will jump into the floating window @@ -35,21 +32,210 @@ end --- In the floating window, all commands and mappings are available as usual, --- except that "q" dismisses the window. --- You can scroll the contents the same as you would any other buffer. -function M.hover() - local params = util.make_position_params() - request(ms.textDocument_hover, params) +--- +--- Note: to disable hover highlights, add the following to your config: +--- +--- ```lua +--- vim.api.nvim_create_autocmd('ColorScheme', { +--- callback = function() +--- vim.api.nvim_set_hl(0, 'LspReferenceTarget', {}) +--- end, +--- }) +--- ``` +--- @param config? vim.lsp.buf.hover.Opts +function M.hover(config) + config = config or {} + config.focus_id = ms.textDocument_hover + + lsp.buf_request_all(0, ms.textDocument_hover, client_positional_params(), function(results, ctx) + local bufnr = assert(ctx.bufnr) + if api.nvim_get_current_buf() ~= bufnr then + -- Ignore result since buffer changed. This happens for slow language servers. + return + end + + -- Filter errors from results + local results1 = {} --- @type table<integer,lsp.Hover> + + for client_id, resp in pairs(results) do + local err, result = resp.err, resp.result + if err then + lsp.log.error(err.code, err.message) + elseif result then + results1[client_id] = result + end + end + + if vim.tbl_isempty(results1) then + if config.silent ~= true then + vim.notify('No information available') + end + return + end + + local contents = {} --- @type string[] + + local nresults = #vim.tbl_keys(results1) + + local format = 'markdown' + + for client_id, result in pairs(results1) do + local client = assert(lsp.get_client_by_id(client_id)) + if nresults > 1 then + -- Show client name if there are multiple clients + contents[#contents + 1] = string.format('# %s', client.name) + end + if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then + if #results1 == 1 then + format = 'plaintext' + contents = vim.split(result.contents.value or '', '\n', { trimempty = true }) + else + -- Surround plaintext with ``` to get correct formatting + contents[#contents + 1] = '```' + vim.list_extend( + contents, + vim.split(result.contents.value or '', '\n', { trimempty = true }) + ) + contents[#contents + 1] = '```' + end + else + vim.list_extend(contents, util.convert_input_to_markdown_lines(result.contents)) + end + local range = result.range + if range then + local start = range.start + local end_ = range['end'] + local start_idx = util._get_line_byte_from_position(bufnr, start, client.offset_encoding) + local end_idx = util._get_line_byte_from_position(bufnr, end_, client.offset_encoding) + + vim.hl.range( + bufnr, + hover_ns, + 'LspReferenceTarget', + { start.line, start_idx }, + { end_.line, end_idx }, + { priority = vim.hl.priorities.user } + ) + end + contents[#contents + 1] = '---' + end + + -- Remove last linebreak ('---') + contents[#contents] = nil + + if vim.tbl_isempty(contents) then + if config.silent ~= true then + vim.notify('No information available') + end + return + end + + local _, winid = lsp.util.open_floating_preview(contents, format, config) + + api.nvim_create_autocmd('WinClosed', { + pattern = tostring(winid), + once = true, + callback = function() + api.nvim_buf_clear_namespace(bufnr, hover_ns, 0, -1) + return true + end, + }) + end) end local function request_with_opts(name, params, opts) local req_handler --- @type function? 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] + local client = assert(lsp.get_client_by_id(ctx.client_id)) + local handler = client.handlers[name] or lsp.handlers[name] handler(err, result, ctx, vim.tbl_extend('force', config or {}, opts)) end end - request(name, params, req_handler) + lsp.buf_request(0, name, params, req_handler) +end + +---@param method string +---@param opts? vim.lsp.LocationOpts +local function get_locations(method, opts) + opts = opts or {} + local bufnr = api.nvim_get_current_buf() + local clients = lsp.get_clients({ method = method, bufnr = bufnr }) + if not next(clients) then + vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) + return + end + local win = api.nvim_get_current_win() + local from = vim.fn.getpos('.') + from[1] = bufnr + local tagname = vim.fn.expand('<cword>') + local remaining = #clients + + ---@type vim.quickfix.entry[] + local all_items = {} + + ---@param result nil|lsp.Location|lsp.Location[] + ---@param client vim.lsp.Client + local function on_response(_, result, client) + local locations = {} + if result then + locations = vim.islist(result) and result or { result } + end + local items = util.locations_to_items(locations, client.offset_encoding) + vim.list_extend(all_items, items) + remaining = remaining - 1 + if remaining == 0 then + if vim.tbl_isempty(all_items) then + vim.notify('No locations found', vim.log.levels.INFO) + return + end + + local title = 'LSP locations' + if opts.on_list then + assert(vim.is_callable(opts.on_list), 'on_list is not a function') + opts.on_list({ + title = title, + items = all_items, + context = { bufnr = bufnr, method = method }, + }) + return + end + + if #all_items == 1 then + local item = all_items[1] + local b = item.bufnr or vim.fn.bufadd(item.filename) + + -- Save position in jumplist + vim.cmd("normal! m'") + -- Push a new item into tagstack + local tagstack = { { tagname = tagname, from = from } } + vim.fn.settagstack(vim.fn.win_getid(win), { items = tagstack }, 't') + + vim.bo[b].buflisted = true + local w = opts.reuse_win and vim.fn.win_findbuf(b)[1] or win + api.nvim_win_set_buf(w, b) + api.nvim_win_set_cursor(w, { item.lnum, item.col - 1 }) + vim._with({ win = w }, function() + -- Open folds under the cursor + vim.cmd('normal! zv') + end) + return + end + if opts.loclist then + vim.fn.setloclist(0, {}, ' ', { title = title, items = all_items }) + vim.cmd.lopen() + else + vim.fn.setqflist({}, ' ', { title = title, items = all_items }) + vim.cmd('botright copen') + end + end + end + for _, client in ipairs(clients) do + local params = util.make_position_params(win, client.offset_encoding) + client.request(method, params, function(_, result) + on_response(_, result, client) + end) + end end --- @class vim.lsp.ListOpts @@ -89,39 +275,145 @@ end --- @note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead. --- @param opts? vim.lsp.LocationOpts function M.declaration(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_declaration, params, opts) + get_locations(ms.textDocument_declaration, opts) end --- Jumps to the definition of the symbol under the cursor. --- @param opts? vim.lsp.LocationOpts function M.definition(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_definition, params, opts) + get_locations(ms.textDocument_definition, opts) end --- Jumps to the definition of the type of the symbol under the cursor. --- @param opts? vim.lsp.LocationOpts function M.type_definition(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_typeDefinition, params, opts) + get_locations(ms.textDocument_typeDefinition, opts) end --- Lists all the implementations for the symbol under the cursor in the --- quickfix window. --- @param opts? vim.lsp.LocationOpts function M.implementation(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_implementation, params, opts) + get_locations(ms.textDocument_implementation, opts) end +--- @param results table<integer,{err: lsp.ResponseError?, result: lsp.SignatureHelp?}> +local function process_signature_help_results(results) + local signatures = {} --- @type [vim.lsp.Client,lsp.SignatureInformation][] + + -- Pre-process results + for client_id, r in pairs(results) do + local err = r.err + local client = assert(lsp.get_client_by_id(client_id)) + if err then + vim.notify( + client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message, + vim.log.levels.ERROR + ) + api.nvim_command('redraw') + else + local result = r.result --- @type lsp.SignatureHelp + if result and result.signatures and result.signatures[1] then + for _, sig in ipairs(result.signatures) do + signatures[#signatures + 1] = { client, sig } + end + end + end + end + + return signatures +end + +local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') + +--- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts +--- @field silent? boolean + +-- TODO(lewis6991): support multiple clients --- Displays signature information about the symbol under the cursor in a --- floating window. -function M.signature_help() - local params = util.make_position_params() - request(ms.textDocument_signatureHelp, params) +--- @param config? vim.lsp.buf.signature_help.Opts +function M.signature_help(config) + local method = ms.textDocument_signatureHelp + + config = config and vim.deepcopy(config) or {} + config.focus_id = method + + lsp.buf_request_all(0, method, client_positional_params(), function(results, ctx) + if api.nvim_get_current_buf() ~= ctx.bufnr then + -- Ignore result since buffer changed. This happens for slow language servers. + return + end + + local signatures = process_signature_help_results(results) + + if not next(signatures) then + if config.silent ~= true then + print('No signature help available') + end + return + end + + local ft = vim.bo[ctx.bufnr].filetype + local total = #signatures + local idx = 0 + + --- @param update_win? integer + local function show_signature(update_win) + idx = (idx % total) + 1 + local client, result = signatures[idx][1], signatures[idx][2] + --- @type string[]? + local triggers = + vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') + local lines, hl = + util.convert_signature_help_to_markdown_lines({ signatures = { result } }, ft, triggers) + if not lines then + return + end + + local sfx = total > 1 and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or '' + local title = string.format('Signature Help: %s%s', client.name, sfx) + if config.border then + config.title = title + else + table.insert(lines, 1, '# ' .. title) + if hl then + hl[1] = hl[1] + 1 + hl[3] = hl[3] + 1 + end + end + + config._update_win = update_win + + local buf, win = util.open_floating_preview(lines, 'markdown', config) + + if hl then + vim.api.nvim_buf_clear_namespace(buf, sig_help_ns, 0, -1) + vim.hl.range( + buf, + sig_help_ns, + 'LspSignatureActiveParameter', + { hl[1], hl[2] }, + { hl[3], hl[4] } + ) + end + return buf, win + end + + local fbuf, fwin = show_signature() + + if total > 1 then + vim.keymap.set('n', '<C-s>', function() + show_signature(fwin) + end, { + buffer = fbuf, + desc = 'Cycle next signature', + }) + end + end) end +--- @deprecated --- Retrieves the completion items at the current cursor position. Can only be --- called in Insert mode. --- @@ -131,9 +423,14 @@ end --- ---@see vim.lsp.protocol.CompletionTriggerKind function M.completion(context) - local params = util.make_position_params() - params.context = context - return request(ms.textDocument_completion, params) + vim.depends('vim.lsp.buf.completion', 'vim.lsp.commpletion.trigger', '0.12') + return lsp.buf_request( + 0, + ms.textDocument_completion, + client_positional_params({ + context = context, + }) + ) end ---@param bufnr integer @@ -240,7 +537,7 @@ function M.format(opts) method = ms.textDocument_formatting end - local clients = vim.lsp.get_clients({ + local clients = lsp.get_clients({ id = opts.id, bufnr = bufnr, name = opts.name, @@ -277,7 +574,7 @@ function M.format(opts) end 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] + local handler = client.handlers[method] or lsp.handlers[method] handler(...) do_format(next(clients, idx)) end, bufnr) @@ -319,7 +616,7 @@ end 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({ + local clients = lsp.get_clients({ bufnr = bufnr, name = opts.name, -- Clients must at least support rename, prepareRename is optional @@ -338,6 +635,8 @@ function M.rename(new_name, opts) -- Compute early to account for cursor movements after going async local cword = vim.fn.expand('<cword>') + --- @param range lsp.Range + --- @param offset_encoding string local function get_text_at_range(range, offset_encoding) return api.nvim_buf_get_text( bufnr, @@ -359,7 +658,7 @@ function M.rename(new_name, opts) local params = util.make_position_params(win, client.offset_encoding) params.newName = name local handler = client.handlers[ms.textDocument_rename] - or vim.lsp.handlers[ms.textDocument_rename] + or lsp.handlers[ms.textDocument_rename] client.request(ms.textDocument_rename, params, function(...) handler(...) try_use_client(next(clients, idx)) @@ -437,12 +736,60 @@ end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references ---@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_opts(ms.textDocument_references, params, opts) + validate('context', context, 'table', true) + local bufnr = api.nvim_get_current_buf() + local clients = lsp.get_clients({ method = ms.textDocument_references, bufnr = bufnr }) + if not next(clients) then + return + end + local win = api.nvim_get_current_win() + opts = opts or {} + + local all_items = {} + local title = 'References' + + local function on_done() + if not next(all_items) then + vim.notify('No references found') + else + local list = { + title = title, + items = all_items, + context = { + method = ms.textDocument_references, + bufnr = bufnr, + }, + } + if opts.loclist then + vim.fn.setloclist(0, {}, ' ', list) + vim.cmd.lopen() + elseif opts.on_list then + assert(vim.is_callable(opts.on_list), 'on_list is not a function') + opts.on_list(list) + else + vim.fn.setqflist({}, ' ', list) + vim.cmd('botright copen') + end + end + end + + local remaining = #clients + for _, client in ipairs(clients) do + local params = util.make_position_params(win, client.offset_encoding) + + ---@diagnostic disable-next-line: inject-field + params.context = context or { + includeDeclaration = true, + } + client.request(ms.textDocument_references, params, function(_, result) + local items = util.locations_to_items(result or {}, client.offset_encoding) + vim.list_extend(all_items, items) + remaining = remaining - 1 + if remaining == 0 then + on_done() + end + end) + end end --- Lists all symbols in the current buffer in the quickfix window. @@ -452,65 +799,116 @@ function M.document_symbol(opts) request_with_opts(ms.textDocument_documentSymbol, params, opts) end ---- @param call_hierarchy_items lsp.CallHierarchyItem[] ---- @return lsp.CallHierarchyItem? -local function pick_call_hierarchy_item(call_hierarchy_items) - if #call_hierarchy_items == 1 then - return call_hierarchy_items[1] - end - local items = {} - for i, item in pairs(call_hierarchy_items) do - local entry = item.detail or item.name - table.insert(items, string.format('%d. %s', i, entry)) - end - local choice = vim.fn.inputlist(items) - if choice < 1 or choice > #items then +--- @param client_id integer +--- @param method string +--- @param params table +--- @param handler? lsp.Handler +--- @param bufnr? integer +local function request_with_id(client_id, method, params, handler, bufnr) + local client = lsp.get_client_by_id(client_id) + if not client then + vim.notify( + string.format('Client with id=%d disappeared during hierarchy request', client_id), + vim.log.levels.WARN + ) return end - return call_hierarchy_items[choice] + client.request(method, params, handler, bufnr) +end + +--- @param item lsp.TypeHierarchyItem|lsp.CallHierarchyItem +local function format_hierarchy_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 hierarchy_methods = { + [ms.typeHierarchy_subtypes] = 'type', + [ms.typeHierarchy_supertypes] = 'type', + [ms.callHierarchy_incomingCalls] = 'call', + [ms.callHierarchy_outgoingCalls] = 'call', +} + --- @param method string -local function call_hierarchy(method) - local params = util.make_position_params() - --- @param result lsp.CallHierarchyItem[]? - request(ms.textDocument_prepareCallHierarchy, params, function(err, result, ctx) - if err then - vim.notify(err.message, vim.log.levels.WARN) - return - end - if not result or vim.tbl_isempty(result) then +local function hierarchy(method) + local kind = hierarchy_methods[method] + if not kind then + error('unsupported method ' .. method) + end + + local prepare_method = kind == 'type' and ms.textDocument_prepareTypeHierarchy + or ms.textDocument_prepareCallHierarchy + + local bufnr = api.nvim_get_current_buf() + local clients = lsp.get_clients({ bufnr = bufnr, method = prepare_method }) + if not next(clients) then + vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) + return + end + + local win = api.nvim_get_current_win() + + --- @param results [integer, lsp.TypeHierarchyItem|lsp.CallHierarchyItem][] + local function on_response(results) + if #results == 0 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) + elseif #results == 1 then + local client_id, item = results[1][1], results[1][2] + request_with_id(client_id, method, { item = item }, nil, bufnr) else - vim.notify( - string.format('Client with id=%d disappeared during call hierarchy request', ctx.client_id), - vim.log.levels.WARN - ) + vim.ui.select(results, { + prompt = string.format('Select a %s hierarchy item:', kind), + kind = kind .. 'hierarchy', + format_item = function(x) + return format_hierarchy_item(x[2]) + end, + }, function(x) + if x then + local client_id, item = x[1], x[2] + request_with_id(client_id, method, { item = item }, nil, bufnr) + end + end) end - end) + end + + local results = {} --- @type [integer, lsp.TypeHierarchyItem|lsp.CallHierarchyItem][] + + local remaining = #clients + + for _, client in ipairs(clients) do + local params = util.make_position_params(win, client.offset_encoding) + --- @param result lsp.CallHierarchyItem[]|lsp.TypeHierarchyItem[]? + client.request(prepare_method, params, function(err, result, ctx) + if err then + vim.notify(err.message, vim.log.levels.WARN) + elseif result then + for _, item in ipairs(result) do + results[#results + 1] = { ctx.client_id, item } + end + end + + remaining = remaining - 1 + if remaining == 0 then + on_response(results) + end + end, bufnr) + end end --- Lists all the call sites of the symbol under the cursor in the --- |quickfix| window. If the symbol can resolve to multiple --- items, the user can pick one in the |inputlist()|. function M.incoming_calls() - call_hierarchy(ms.callHierarchy_incomingCalls) + hierarchy(ms.callHierarchy_incomingCalls) end --- Lists all the items that are called by the symbol under the --- cursor in the |quickfix| window. If the symbol can resolve to --- multiple items, the user can pick one in the |inputlist()|. function M.outgoing_calls() - call_hierarchy(ms.callHierarchy_outgoingCalls) + hierarchy(ms.callHierarchy_outgoingCalls) end --- Lists all the subtypes or supertypes of the symbol under the @@ -519,79 +917,14 @@ end ---@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) + hierarchy(method) end --- List workspace folders. --- function M.list_workspace_folders() local workspace_folders = {} - for _, client in pairs(vim.lsp.get_clients({ bufnr = 0 })) do + for _, client in pairs(lsp.get_clients({ bufnr = 0 })) do for _, folder in pairs(client.workspace_folders or {}) do table.insert(workspace_folders, folder.name) end @@ -614,7 +947,7 @@ function M.add_workspace_folder(workspace_folder) return end local bufnr = api.nvim_get_current_buf() - for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do + for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do client:_add_workspace_folder(workspace_folder) end end @@ -631,7 +964,7 @@ function M.remove_workspace_folder(workspace_folder) return end local bufnr = api.nvim_get_current_buf() - for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do + for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do client:_remove_workspace_folder(workspace_folder) end print(workspace_folder, 'is not currently part of the workspace') @@ -670,8 +1003,7 @@ end --- |hl-LspReferenceRead| --- |hl-LspReferenceWrite| function M.document_highlight() - local params = util.make_position_params() - request(ms.textDocument_documentHighlight, params) + lsp.buf_request(0, ms.textDocument_documentHighlight, client_positional_params()) end --- Removes document highlights from current buffer. @@ -773,7 +1105,8 @@ local function on_code_action_results(results, opts) local a_cmd = action.command if a_cmd then local command = type(a_cmd) == 'table' and a_cmd or action - client:_exec_cmd(command, ctx) + --- @cast command lsp.Command + client:exec_cmd(command, ctx) end end @@ -794,16 +1127,11 @@ local function on_code_action_results(results, opts) -- command: string -- arguments?: any[] -- - local client = assert(vim.lsp.get_client_by_id(choice.ctx.client_id)) + local client = assert(lsp.get_client_by_id(choice.ctx.client_id)) local action = choice.action local bufnr = assert(choice.ctx.bufnr, 'Must have buffer number') - local reg = client.dynamic_capabilities:get(ms.textDocument_codeAction, { bufnr = bufnr }) - - local supports_resolve = vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider') - or client.supports_method(ms.codeAction_resolve) - - if not action.edit and client and supports_resolve then + if not action.edit and client.supports_method(ms.codeAction_resolve) then client.request(ms.codeAction_resolve, action, function(err, resolved_action) if err then if action.command then @@ -827,11 +1155,19 @@ local function on_code_action_results(results, opts) return end - ---@param item {action: lsp.Command|lsp.CodeAction} + ---@param item {action: lsp.Command|lsp.CodeAction, ctx: lsp.HandlerContext} local function format_item(item) - local title = item.action.title:gsub('\r\n', '\\r\\n') - return title:gsub('\n', '\\n') + local clients = lsp.get_clients({ bufnr = item.ctx.bufnr }) + local title = item.action.title:gsub('\r\n', '\\r\\n'):gsub('\n', '\\n') + + if #clients == 1 then + return title + end + + local source = lsp.get_client_by_id(item.ctx.client_id).name + return ('%s [%s]'):format(title, source) end + local select_opts = { prompt = 'Code actions:', kind = 'codeaction', @@ -847,7 +1183,7 @@ end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction ---@see vim.lsp.protocol.CodeActionTriggerKind function M.code_action(opts) - validate({ options = { opts, 't', true } }) + validate('options', opts, 'table', true) opts = opts or {} -- Detect old API call code_action(context) which should now be -- code_action({ context = context} ) @@ -857,16 +1193,16 @@ function M.code_action(opts) end local context = opts.context and vim.deepcopy(opts.context) or {} if not context.triggerKind then - context.triggerKind = vim.lsp.protocol.CodeActionTriggerKind.Invoked + context.triggerKind = lsp.protocol.CodeActionTriggerKind.Invoked end local mode = api.nvim_get_mode().mode local bufnr = api.nvim_get_current_buf() local win = api.nvim_get_current_win() - local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_codeAction }) + local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_codeAction }) local remaining = #clients if remaining == 0 then - if next(vim.lsp.get_clients({ bufnr = bufnr })) then - vim.notify(vim.lsp._unsupported_method(ms.textDocument_codeAction), vim.log.levels.WARN) + if next(lsp.get_clients({ bufnr = bufnr })) then + vim.notify(lsp._unsupported_method(ms.textDocument_codeAction), vim.log.levels.WARN) end return end @@ -903,8 +1239,8 @@ function M.code_action(opts) if context.diagnostics then params.context = context else - local ns_push = vim.lsp.diagnostic.get_namespace(client.id, false) - local ns_pull = vim.lsp.diagnostic.get_namespace(client.id, true) + local ns_push = lsp.diagnostic.get_namespace(client.id, false) + local ns_pull = lsp.diagnostic.get_namespace(client.id, true) local diagnostics = {} local lnum = api.nvim_win_get_cursor(0)[1] - 1 vim.list_extend(diagnostics, vim.diagnostic.get(bufnr, { namespace = ns_pull, lnum = lnum })) @@ -921,20 +1257,20 @@ function M.code_action(opts) end end +--- @deprecated --- Executes an LSP server command. --- @param command_params lsp.ExecuteCommandParams --- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand function M.execute_command(command_params) - validate({ - command = { command_params.command, 's' }, - arguments = { command_params.arguments, 't', true }, - }) + validate('command', command_params.command, 'string') + validate('arguments', command_params.arguments, 'table', true) + vim.deprecate('execute_command', 'client:exec_cmd', '0.12') command_params = { command = command_params.command, arguments = command_params.arguments, workDoneToken = command_params.workDoneToken, } - request(ms.workspace_executeCommand, command_params) + lsp.buf_request(0, ms.workspace_executeCommand, command_params) end return M diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index e3c82f4169..11ecb87507 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -91,7 +91,7 @@ local validate = vim.validate --- (default: client-id) --- @field name? string --- ---- Language ID as string. Defaults to the filetype. +--- Language ID as string. Defaults to the buffer filetype. --- @field get_language_id? fun(bufnr: integer, filetype: string): string --- --- The encoding that the LSP server expects. Client does not verify this is correct. @@ -216,6 +216,7 @@ local validate = vim.validate --- --- The capabilities provided by the client (editor or tool) --- @field capabilities lsp.ClientCapabilities +--- @field private registrations table<string,lsp.Registration[]> --- @field dynamic_capabilities lsp.DynamicCapabilities --- --- Sends a request to the server. @@ -291,7 +292,7 @@ local client_index = 0 --- @param filename (string) path to check --- @return boolean # true if {filename} exists and is a directory, false otherwise local function is_dir(filename) - validate({ filename = { filename, 's' } }) + validate('filename', filename, 'string') local stat = uv.fs_stat(filename) return stat and stat.type == 'directory' or false end @@ -312,9 +313,7 @@ local valid_encodings = { --- @param encoding string? Encoding to normalize --- @return string # normalized encoding name local function validate_encoding(encoding) - validate({ - encoding = { encoding, 's', true }, - }) + validate('encoding', encoding, 'string', true) if not encoding then return valid_encodings.UTF16 end @@ -350,27 +349,23 @@ end --- Validates a client configuration as given to |vim.lsp.start_client()|. --- @param config vim.lsp.ClientConfig local function validate_config(config) - validate({ - config = { config, 't' }, - }) - validate({ - handlers = { config.handlers, 't', true }, - capabilities = { config.capabilities, 't', true }, - cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' }, - cmd_env = { config.cmd_env, 't', true }, - detached = { config.detached, 'b', true }, - name = { config.name, 's', true }, - on_error = { config.on_error, 'f', true }, - on_exit = { config.on_exit, { 'f', 't' }, true }, - on_init = { config.on_init, { 'f', 't' }, true }, - on_attach = { config.on_attach, { 'f', 't' }, true }, - settings = { config.settings, 't', true }, - commands = { config.commands, 't', true }, - before_init = { config.before_init, { 'f', 't' }, true }, - offset_encoding = { config.offset_encoding, 's', true }, - flags = { config.flags, 't', true }, - get_language_id = { config.get_language_id, 'f', true }, - }) + validate('config', config, 'table') + validate('handlers', config.handlers, 'table', true) + validate('capabilities', config.capabilities, 'table', true) + validate('cmd_cwd', config.cmd_cwd, optional_validator(is_dir), 'directory') + validate('cmd_env', config.cmd_env, 'table', true) + validate('detached', config.detached, 'boolean', true) + validate('name', config.name, 'string', true) + validate('on_error', config.on_error, 'function', true) + validate('on_exit', config.on_exit, { 'function', 'table' }, true) + validate('on_init', config.on_init, { 'function', 'table' }, true) + validate('on_attach', config.on_attach, { 'function', 'table' }, true) + validate('settings', config.settings, 'table', true) + validate('commands', config.commands, 'table', true) + validate('before_init', config.before_init, { 'function', 'table' }, true) + validate('offset_encoding', config.offset_encoding, 'string', true) + validate('flags', config.flags, 'table', true) + validate('get_language_id', config.get_language_id, 'function', true) assert( ( @@ -409,18 +404,16 @@ local function get_name(id, config) return tostring(id) end ---- @param workspace_folders lsp.WorkspaceFolder[]? ---- @param root_dir string? +--- @param workspace_folders string|lsp.WorkspaceFolder[]? --- @return lsp.WorkspaceFolder[]? -local function get_workspace_folders(workspace_folders, root_dir) - if workspace_folders then +local function get_workspace_folders(workspace_folders) + if type(workspace_folders) == 'table' then return workspace_folders - end - if root_dir then + elseif type(workspace_folders) == 'string' then return { { - uri = vim.uri_from_fname(root_dir), - name = root_dir, + uri = vim.uri_from_fname(workspace_folders), + name = workspace_folders, }, } end @@ -457,13 +450,13 @@ function Client.create(config) requests = {}, attached_buffers = {}, server_capabilities = {}, - dynamic_capabilities = lsp._dynamic.new(id), + registrations = {}, commands = config.commands or {}, settings = config.settings or {}, flags = config.flags or {}, get_language_id = config.get_language_id or default_get_language_id, capabilities = config.capabilities or lsp.protocol.make_client_capabilities(), - workspace_folders = get_workspace_folders(config.workspace_folders, config.root_dir), + workspace_folders = get_workspace_folders(config.workspace_folders or config.root_dir), root_dir = config.root_dir, _before_init_cb = config.before_init, _on_init_cbs = ensure_list(config.on_init), @@ -484,6 +477,28 @@ function Client.create(config) messages = { name = name, messages = {}, progress = {}, status = {} }, } + --- @class lsp.DynamicCapabilities + --- @nodoc + self.dynamic_capabilities = { + capabilities = self.registrations, + client_id = id, + register = function(_, registrations) + return self:_register_dynamic(registrations) + end, + unregister = function(_, unregistrations) + return self:_unregister_dynamic(unregistrations) + end, + get = function(_, method, opts) + return self:_get_registration(method, opts and opts.bufnr) + end, + supports_registration = function(_, method) + return self:_supports_registration(method) + end, + supports = function(_, method, opts) + return self:_get_registration(method, opts and opts.bufnr) ~= nil + end, + } + self.request = method_wrapper(self, Client._request) self.request_sync = method_wrapper(self, Client._request_sync) self.notify = method_wrapper(self, Client._notify) @@ -640,7 +655,7 @@ end --- @param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer --- @return integer bufnr local function resolve_bufnr(bufnr) - validate({ bufnr = { bufnr, 'n', true } }) + validate('bufnr', bufnr, 'number', true) if bufnr == nil or bufnr == 0 then return api.nvim_get_current_buf() end @@ -806,7 +821,7 @@ end --- @return boolean status true if notification was successful. false otherwise --- @see |vim.lsp.client.notify()| function Client:_cancel_request(id) - validate({ id = { id, 'n' } }) + validate('id', id, 'number') local request = self.requests[id] if request and request.type == 'pending' then request.type = 'cancel' @@ -852,6 +867,105 @@ function Client:_stop(force) end) end +--- Get options for a method that is registered dynamically. +--- @param method string +function Client:_supports_registration(method) + local capability = vim.tbl_get(self.capabilities, unpack(vim.split(method, '/'))) + return type(capability) == 'table' and capability.dynamicRegistration +end + +--- @private +--- @param registrations lsp.Registration[] +function Client:_register_dynamic(registrations) + -- remove duplicates + self:_unregister_dynamic(registrations) + for _, reg in ipairs(registrations) do + local method = reg.method + if not self.registrations[method] then + self.registrations[method] = {} + end + table.insert(self.registrations[method], reg) + end +end + +--- @param registrations lsp.Registration[] +function Client:_register(registrations) + self:_register_dynamic(registrations) + + local unsupported = {} --- @type string[] + + for _, reg in ipairs(registrations) do + local method = reg.method + if method == ms.workspace_didChangeWatchedFiles then + vim.lsp._watchfiles.register(reg, self.id) + elseif not self:_supports_registration(method) then + unsupported[#unsupported + 1] = method + end + end + + if #unsupported > 0 then + local warning_tpl = 'The language server %s triggers a registerCapability ' + .. 'handler for %s despite dynamicRegistration set to false. ' + .. 'Report upstream, this warning is harmless' + log.warn(string.format(warning_tpl, self.name, table.concat(unsupported, ', '))) + end +end + +--- @private +--- @param unregistrations lsp.Unregistration[] +function Client:_unregister_dynamic(unregistrations) + for _, unreg in ipairs(unregistrations) do + local sreg = self.registrations[unreg.method] + -- Unegister dynamic capability + for i, reg in ipairs(sreg or {}) do + if reg.id == unreg.id then + table.remove(sreg, i) + break + end + end + end +end + +--- @param unregistrations lsp.Unregistration[] +function Client:_unregister(unregistrations) + self:_unregister_dynamic(unregistrations) + for _, unreg in ipairs(unregistrations) do + if unreg.method == ms.workspace_didChangeWatchedFiles then + vim.lsp._watchfiles.unregister(unreg, self.id) + end + end +end + +--- @private +function Client:_get_language_id(bufnr) + return self.get_language_id(bufnr, vim.bo[bufnr].filetype) +end + +--- @param method string +--- @param bufnr? integer +--- @return lsp.Registration? +function Client:_get_registration(method, bufnr) + bufnr = bufnr or vim.api.nvim_get_current_buf() + for _, reg in ipairs(self.registrations[method] or {}) do + if not reg.registerOptions or not reg.registerOptions.documentSelector then + return reg + end + local documentSelector = reg.registerOptions.documentSelector + local language = self:_get_language_id(bufnr) + local uri = vim.uri_from_bufnr(bufnr) + local fname = vim.uri_to_fname(uri) + for _, filter in ipairs(documentSelector) do + if + not (filter.language and language ~= filter.language) + and not (filter.scheme and not vim.startswith(uri, filter.scheme .. ':')) + and not (filter.pattern and not vim.glob.to_lpeg(filter.pattern):match(fname)) + then + return reg + end + end + end +end + --- @private --- Checks whether a client is stopped. --- @@ -865,10 +979,9 @@ end --- or via workspace/executeCommand (if supported by the server) --- --- @param command lsp.Command ---- @param context? {bufnr: integer} +--- @param context? {bufnr?: integer} --- @param handler? lsp.Handler only called if a server command ---- @param on_unsupported? function handler invoked when the command is not supported by the client. -function Client:_exec_cmd(command, context, handler, on_unsupported) +function Client:exec_cmd(command, context, handler) context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]] context.bufnr = context.bufnr or api.nvim_get_current_buf() context.client_id = self.id @@ -881,25 +994,23 @@ function Client:_exec_cmd(command, context, handler, on_unsupported) local command_provider = self.server_capabilities.executeCommandProvider local commands = type(command_provider) == 'table' and command_provider.commands or {} + if not vim.list_contains(commands, cmdname) then - if on_unsupported then - on_unsupported() - else - vim.notify_once( - string.format( - 'Language server `%s` does not support command `%s`. This command may require a client extension.', - self.name, - cmdname - ), - vim.log.levels.WARN - ) - end + vim.notify_once( + string.format( + 'Language server `%s` does not support command `%s`. This command may require a client extension.', + self.name, + cmdname + ), + vim.log.levels.WARN + ) return end -- Not using command directly to exclude extra properties, -- see https://github.com/python-lsp/python-lsp-server/issues/146 + --- @type lsp.ExecuteCommandParams local params = { - command = command.command, + command = cmdname, arguments = command.arguments, } self.request(ms.workspace_executeCommand, params, handler, context.bufnr) @@ -917,12 +1028,11 @@ function Client:_text_document_did_open_handler(bufnr) return end - local filetype = vim.bo[bufnr].filetype self.notify(ms.textDocument_didOpen, { textDocument = { version = lsp.util.buf_versions[bufnr], uri = vim.uri_from_bufnr(bufnr), - languageId = self.get_language_id(bufnr, filetype), + languageId = self:_get_language_id(bufnr), text = lsp._buf_get_full_text(bufnr), }, }) @@ -987,12 +1097,37 @@ function Client:_supports_method(method, opts) if vim.tbl_get(self.server_capabilities, unpack(required_capability)) then return true end - if self.dynamic_capabilities:supports_registration(method) then - return self.dynamic_capabilities:supports(method, opts) + + local rmethod = lsp._resolve_to_request[method] + if rmethod then + if self:_supports_registration(rmethod) then + local reg = self:_get_registration(rmethod, opts and opts.bufnr) + return vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider') or false + end + else + if self:_supports_registration(method) then + return self:_get_registration(method, opts and opts.bufnr) ~= nil + end end return false end +--- Get options for a method that is registered dynamically. +--- @param method string +--- @param bufnr? integer +--- @return lsp.LSPAny? +function Client:_get_registration_options(method, bufnr) + if not self:_supports_registration(method) then + return + end + + local reg = self:_get_registration(method, bufnr) + + if reg then + return reg.registerOptions + end +end + --- @private --- Handles a notification sent by an LSP server by invoking the --- corresponding handler. @@ -1070,7 +1205,7 @@ function Client:_add_workspace_folder(dir) end end - local wf = assert(get_workspace_folders(nil, dir)) + local wf = assert(get_workspace_folders(dir)) self:_notify(ms.workspace_didChangeWorkspaceFolders, { event = { added = wf, removed = {} }, @@ -1085,7 +1220,7 @@ end --- Remove a directory to the workspace folders. --- @param dir string? function Client:_remove_workspace_folder(dir) - local wf = assert(get_workspace_folders(nil, dir)) + local wf = assert(get_workspace_folders(dir)) self:_notify(ms.workspace_didChangeWorkspaceFolders, { event = { added = {}, removed = wf }, diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index c1b6bfb28c..fdbdda695a 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -48,7 +48,7 @@ local function execute_lens(lens, bufnr, client_id) local client = vim.lsp.get_client_by_id(client_id) assert(client, 'Client is required to execute lens, client_id=' .. client_id) - client:_exec_cmd(lens.command, { bufnr = bufnr }, function(...) + client:exec_cmd(lens.command, { bufnr = bufnr }, function(...) vim.lsp.handlers[ms.workspace_executeCommand](...) M.refresh() end) @@ -261,7 +261,7 @@ end ---@param err lsp.ResponseError? ---@param result lsp.CodeLens[] ---@param ctx lsp.HandlerContext -function M.on_codelens(err, result, ctx, _) +function M.on_codelens(err, result, ctx) if err then active_refreshes[assert(ctx.bufnr)] = nil log.error('codelens', err) diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 71ea2df100..92bc110a97 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -113,12 +113,11 @@ local function parse_snippet(input) end --- @param item lsp.CompletionItem ---- @param suffix? string -local function apply_snippet(item, suffix) +local function apply_snippet(item) if item.textEdit then - vim.snippet.expand(item.textEdit.newText .. suffix) + vim.snippet.expand(item.textEdit.newText) elseif item.insertText then - vim.snippet.expand(item.insertText .. suffix) + vim.snippet.expand(item.insertText) end end @@ -221,6 +220,20 @@ local function get_doc(item) return '' end +---@param value string +---@param prefix string +---@return boolean +local function match_item_by_value(value, prefix) + if vim.o.completeopt:find('fuzzy') ~= nil then + return next(vim.fn.matchfuzzy({ value }, prefix)) ~= nil + end + + if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then + return vim.startswith(value:lower(), prefix:lower()) + end + return vim.startswith(value, prefix) +end + --- Turns the result of a `textDocument/completion` request into vim-compatible --- |complete-items|. --- @@ -245,8 +258,16 @@ function M._lsp_to_complete_items(result, prefix, client_id) else ---@param item lsp.CompletionItem matches = function(item) - local text = item.filterText or item.label - return next(vim.fn.matchfuzzy({ text }, prefix)) ~= nil + if item.filterText then + return match_item_by_value(item.filterText, prefix) + end + + if item.textEdit then + -- server took care of filtering + return true + end + + return match_item_by_value(item.label, prefix) end end @@ -272,7 +293,7 @@ function M._lsp_to_complete_items(result, prefix, client_id) icase = 1, dup = 1, empty = 1, - hl_group = hl_group, + abbr_hlgroup = hl_group, user_data = { nvim = { lsp = { @@ -316,7 +337,7 @@ local function adjust_start_col(lnum, line, items, encoding) end end if min_start_char then - return lsp.util._str_byteindex_enc(line, min_start_char, encoding) + return vim.str_byteindex(line, encoding, min_start_char, false) else return nil end @@ -539,35 +560,24 @@ local function on_complete_done() -- Remove the already inserted word. local start_char = cursor_col - #completed_item.word - local line = api.nvim_buf_get_lines(bufnr, cursor_row, cursor_row + 1, true)[1] - api.nvim_buf_set_text(bufnr, cursor_row, start_char, cursor_row, #line, { '' }) - return line:sub(cursor_col + 1) + api.nvim_buf_set_text(bufnr, cursor_row, start_char, cursor_row, cursor_col, { '' }) end - --- @param suffix? string - local function apply_snippet_and_command(suffix) + local function apply_snippet_and_command() if expand_snippet then - apply_snippet(completion_item, suffix) + apply_snippet(completion_item) end local command = completion_item.command if command then - client:_exec_cmd(command, { bufnr = bufnr }, nil, function() - vim.lsp.log.warn( - string.format( - 'Language server `%s` does not support command `%s`. This command may require a client extension.', - client.name, - command.command - ) - ) - end) + client:exec_cmd(command, { bufnr = bufnr }) end end if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then - local suffix = clear_word() + clear_word() lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, offset_encoding) - apply_snippet_and_command(suffix) + apply_snippet_and_command() elseif resolve_provider and type(completion_item) == 'table' then local changedtick = vim.b[bufnr].changedtick @@ -577,7 +587,7 @@ local function on_complete_done() return end - local suffix = clear_word() + clear_word() if err then vim.notify_once(err.message, vim.log.levels.WARN) elseif result and result.additionalTextEdits then @@ -587,16 +597,16 @@ local function on_complete_done() end end - apply_snippet_and_command(suffix) + apply_snippet_and_command() end, bufnr) else - local suffix = clear_word() - apply_snippet_and_command(suffix) + clear_word() + apply_snippet_and_command() end end --- @class vim.lsp.completion.BufferOpts ---- @field autotrigger? boolean Whether to trigger completion automatically. Default: false +--- @field autotrigger? boolean Default: false When true, completion triggers automatically based on the server's `triggerCharacters`. --- @field convert? fun(item: lsp.CompletionItem): table Transforms an LSP CompletionItem to |complete-items|. ---@param client_id integer diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index c10312484b..8fd30c7668 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -9,14 +9,6 @@ local augroup = api.nvim_create_augroup('vim_lsp_diagnostic', {}) local DEFAULT_CLIENT_ID = -1 -local function get_client_id(client_id) - if client_id == nil then - client_id = DEFAULT_CLIENT_ID - end - - return client_id -end - ---@param severity lsp.DiagnosticSeverity local function severity_lsp_to_vim(severity) if type(severity) == 'string' then @@ -33,25 +25,6 @@ local function severity_vim_to_lsp(severity) return severity end ----@param lines string[]? ----@param lnum integer ----@param col integer ----@param offset_encoding string ----@return integer -local function line_byte_from_position(lines, lnum, col, offset_encoding) - if not lines or offset_encoding == 'utf-8' then - return col - end - - local line = lines[lnum + 1] - local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == 'utf-16') - if ok then - return result --- @type integer - end - - return col -end - ---@param bufnr integer ---@return string[]? local function get_buf_lines(bufnr) @@ -118,12 +91,13 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id) ) message = diagnostic.message.value end + local line = buf_lines and buf_lines[start.line + 1] or '' --- @type vim.Diagnostic return { lnum = start.line, - col = line_byte_from_position(buf_lines, start.line, start.character, offset_encoding), + col = vim.str_byteindex(line, offset_encoding, start.character, false), end_lnum = _end.line, - end_col = line_byte_from_position(buf_lines, _end.line, _end.character, offset_encoding), + end_col = vim.str_byteindex(line, offset_encoding, _end.character, false), severity = severity_lsp_to_vim(diagnostic.severity), message = message, source = diagnostic.source, @@ -195,7 +169,7 @@ local _client_pull_namespaces = {} ---@param client_id integer The id of the LSP client ---@param is_pull boolean? Whether the namespace is for a pull or push client. Defaults to push function M.get_namespace(client_id, is_pull) - vim.validate({ client_id = { client_id, 'n' } }) + vim.validate('client_id', client_id, 'number') local client = vim.lsp.get_client_by_id(client_id) if is_pull then @@ -236,8 +210,7 @@ end --- @param client_id? integer --- @param diagnostics vim.Diagnostic[] --- @param is_pull boolean ---- @param config? vim.diagnostic.Opts -local function handle_diagnostics(uri, client_id, diagnostics, is_pull, config) +local function handle_diagnostics(uri, client_id, diagnostics, is_pull) local fname = vim.uri_to_fname(uri) if #diagnostics == 0 and vim.fn.bufexists(fname) == 0 then @@ -249,91 +222,39 @@ local function handle_diagnostics(uri, client_id, diagnostics, is_pull, config) return end - client_id = get_client_id(client_id) - local namespace = M.get_namespace(client_id, is_pull) - - if config then - --- @cast config table<string, table> - for _, opt in pairs(config) do - convert_severity(opt) - end - -- Persist configuration to ensure buffer reloads use the same - -- configuration. To make lsp.with configuration work (See :help - -- lsp-handler-configuration) - vim.diagnostic.config(config, namespace) + if client_id == nil then + client_id = DEFAULT_CLIENT_ID end + local namespace = M.get_namespace(client_id, is_pull) + vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)) end --- |lsp-handler| for the method "textDocument/publishDiagnostics" --- ---- See |vim.diagnostic.config()| for configuration options. Handler-specific ---- configuration can be set using |vim.lsp.with()|: ---- ---- ```lua ---- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( ---- vim.lsp.diagnostic.on_publish_diagnostics, { ---- -- Enable underline, use default values ---- underline = true, ---- -- Enable virtual text, override spacing to 4 ---- virtual_text = { ---- spacing = 4, ---- }, ---- -- Use a function to dynamically turn signs off ---- -- and on, using buffer local variables ---- signs = function(namespace, bufnr) ---- return vim.b[bufnr].show_signs == true ---- end, ---- -- Disable a feature ---- update_in_insert = false, ---- } ---- ) ---- ``` +--- See |vim.diagnostic.config()| for configuration options. --- ---@param _ lsp.ResponseError? ---@param result lsp.PublishDiagnosticsParams ---@param ctx lsp.HandlerContext ----@param config? vim.diagnostic.Opts Configuration table (see |vim.diagnostic.config()|). -function M.on_publish_diagnostics(_, result, ctx, config) - handle_diagnostics(result.uri, ctx.client_id, result.diagnostics, false, config) +function M.on_publish_diagnostics(_, result, ctx) + handle_diagnostics(result.uri, ctx.client_id, result.diagnostics, false) end --- |lsp-handler| for the method "textDocument/diagnostic" --- ---- See |vim.diagnostic.config()| for configuration options. Handler-specific ---- configuration can be set using |vim.lsp.with()|: ---- ---- ```lua ---- vim.lsp.handlers["textDocument/diagnostic"] = vim.lsp.with( ---- vim.lsp.diagnostic.on_diagnostic, { ---- -- Enable underline, use default values ---- underline = true, ---- -- Enable virtual text, override spacing to 4 ---- virtual_text = { ---- spacing = 4, ---- }, ---- -- Use a function to dynamically turn signs off ---- -- and on, using buffer local variables ---- signs = function(namespace, bufnr) ---- return vim.b[bufnr].show_signs == true ---- end, ---- -- Disable a feature ---- update_in_insert = false, ---- } ---- ) ---- ``` +--- See |vim.diagnostic.config()| for configuration options. --- ---@param _ lsp.ResponseError? ---@param result lsp.DocumentDiagnosticReport ---@param ctx lsp.HandlerContext ----@param config vim.diagnostic.Opts Configuration table (see |vim.diagnostic.config()|). -function M.on_diagnostic(_, result, ctx, config) +function M.on_diagnostic(_, result, ctx) if result == nil or result.kind == 'unchanged' then return end - handle_diagnostics(ctx.params.textDocument.uri, ctx.client_id, result.items, true, config) + handle_diagnostics(ctx.params.textDocument.uri, ctx.client_id, result.items, true) end --- Clear push diagnostics and diagnostic cache. diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 44548fec92..5c28d88b38 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -5,10 +5,21 @@ local util = require('vim.lsp.util') local api = vim.api local completion = require('vim.lsp.completion') ---- @type table<string,lsp.Handler> +--- @type table<string, lsp.Handler> local M = {} --- FIXME: DOC: Expose in vimdocs +--- @deprecated +--- Client to server response handlers. +--- @type table<vim.lsp.protocol.Method.ClientToServer, lsp.Handler> +local RCS = {} + +--- Server to client request handlers. +--- @type table<vim.lsp.protocol.Method.ServerToClient, lsp.Handler> +local RSC = {} + +--- Server to client notification handlers. +--- @type table<vim.lsp.protocol.Method.ServerToClient, lsp.Handler> +local NSC = {} --- Writes to error buffer. ---@param ... string Will be concatenated before being written @@ -18,14 +29,15 @@ local function err_message(...) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand -M[ms.workspace_executeCommand] = function(_, _, _, _) +RCS[ms.workspace_executeCommand] = function(_, _, _) -- Error handling is done implicitly by wrapping all handlers; see end of this file end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress ---@param params lsp.ProgressParams ---@param ctx lsp.HandlerContext -M[ms.dollar_progress] = function(_, params, ctx) +---@diagnostic disable-next-line:no-unknown +RSC[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') @@ -59,26 +71,26 @@ M[ms.dollar_progress] = function(_, params, ctx) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create ----@param result lsp.WorkDoneProgressCreateParams +---@param params lsp.WorkDoneProgressCreateParams ---@param ctx lsp.HandlerContext -M[ms.window_workDoneProgress_create] = function(_, result, ctx) +RSC[ms.window_workDoneProgress_create] = 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 - client.progress:push(result) + client.progress:push(params) return vim.NIL end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest ----@param result lsp.ShowMessageRequestParams -M[ms.window_showMessageRequest] = function(_, result) - local actions = result.actions or {} +---@param params lsp.ShowMessageRequestParams +RSC[ms.window_showMessageRequest] = function(_, params) + local actions = params.actions or {} local co, is_main = coroutine.running() if co and not is_main then local opts = { - prompt = result.message .. ': ', + prompt = params.message .. ': ', format_item = function(action) return (action.title:gsub('\r\n', '\\r\\n')):gsub('\n', '\\n') end, @@ -92,7 +104,7 @@ M[ms.window_showMessageRequest] = function(_, result) end) return coroutine.yield() else - local option_strings = { result.message, '\nRequest Actions:' } + local option_strings = { params.message, '\nRequest Actions:' } for i, action in ipairs(actions) do local title = action.title:gsub('\r\n', '\\r\\n') title = title:gsub('\n', '\\n') @@ -108,65 +120,37 @@ M[ms.window_showMessageRequest] = function(_, result) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability ---- @param result lsp.RegistrationParams -M[ms.client_registerCapability] = function(_, result, ctx) - local client_id = ctx.client_id - local client = assert(vim.lsp.get_client_by_id(client_id)) - - client.dynamic_capabilities:register(result.registrations) - for bufnr, _ in pairs(client.attached_buffers) do +--- @param params lsp.RegistrationParams +RSC[ms.client_registerCapability] = function(_, params, ctx) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + client:_register(params.registrations) + for bufnr in pairs(client.attached_buffers) do vim.lsp._set_defaults(client, bufnr) end - - ---@type string[] - local unsupported = {} - for _, reg in ipairs(result.registrations) do - if reg.method == ms.workspace_didChangeWatchedFiles then - vim.lsp._watchfiles.register(reg, ctx) - elseif not client.dynamic_capabilities:supports_registration(reg.method) then - unsupported[#unsupported + 1] = reg.method - end - end - if #unsupported > 0 then - local warning_tpl = 'The language server %s triggers a registerCapability ' - .. 'handler for %s despite dynamicRegistration set to false. ' - .. 'Report upstream, this warning is harmless' - local client_name = client and client.name or string.format('id=%d', client_id) - local warning = string.format(warning_tpl, client_name, table.concat(unsupported, ', ')) - log.warn(warning) - end return vim.NIL end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_unregisterCapability ---- @param result lsp.UnregistrationParams -M[ms.client_unregisterCapability] = function(_, result, ctx) - local client_id = ctx.client_id - local client = assert(vim.lsp.get_client_by_id(client_id)) - client.dynamic_capabilities:unregister(result.unregisterations) - - for _, unreg in ipairs(result.unregisterations) do - if unreg.method == ms.workspace_didChangeWatchedFiles then - vim.lsp._watchfiles.unregister(unreg, ctx) - end - end +--- @param params lsp.UnregistrationParams +RSC[ms.client_unregisterCapability] = function(_, params, ctx) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + client:_unregister(params.unregisterations) return vim.NIL end +-- TODO(lewis6991): Do we need to notify other servers? --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit -M[ms.workspace_applyEdit] = function(_, workspace_edit, ctx) +RSC[ms.workspace_applyEdit] = function(_, params, ctx) assert( - workspace_edit, + params, 'workspace/applyEdit must be called with `ApplyWorkspaceEditParams`. Server is violating the specification' ) -- TODO(ashkan) Do something more with label? - local client_id = ctx.client_id - local client = assert(vim.lsp.get_client_by_id(client_id)) - if workspace_edit.label then - print('Workspace edit', workspace_edit.label) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + if params.label then + print('Workspace edit', params.label) end - local status, result = - pcall(util.apply_workspace_edit, workspace_edit.edit, client.offset_encoding) + local status, result = pcall(util.apply_workspace_edit, params.edit, client.offset_encoding) return { applied = status, failureReason = result, @@ -182,24 +166,23 @@ local function lookup_section(table, section) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration ---- @param result lsp.ConfigurationParams -M[ms.workspace_configuration] = function(_, result, ctx) - local client_id = ctx.client_id - local client = vim.lsp.get_client_by_id(client_id) +--- @param params lsp.ConfigurationParams +RSC[ms.workspace_configuration] = function(_, params, ctx) + local client = vim.lsp.get_client_by_id(ctx.client_id) if not client then err_message( 'LSP[', - client_id, + ctx.client_id, '] client has shut down after sending a workspace/configuration request' ) return end - if not result.items then + if not params.items then return {} end local response = {} - for _, item in ipairs(result.items) do + for _, item in ipairs(params.items) do if item.section then local value = lookup_section(client.settings, item.section) -- For empty sections with no explicit '' key, return settings as is @@ -216,57 +199,34 @@ M[ms.workspace_configuration] = function(_, result, ctx) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders -M[ms.workspace_workspaceFolders] = function(_, _, ctx) - local client_id = ctx.client_id - local client = vim.lsp.get_client_by_id(client_id) +RSC[ms.workspace_workspaceFolders] = function(_, _, ctx) + local client = vim.lsp.get_client_by_id(ctx.client_id) if not client then - err_message('LSP[id=', client_id, '] client has shut down after sending the message') + err_message('LSP[id=', ctx.client_id, '] client has shut down after sending the message') return end return client.workspace_folders or vim.NIL end -M[ms.textDocument_publishDiagnostics] = function(...) +NSC[ms.textDocument_publishDiagnostics] = function(...) return vim.lsp.diagnostic.on_publish_diagnostics(...) end -M[ms.textDocument_diagnostic] = function(...) +--- @private +RCS[ms.textDocument_diagnostic] = function(...) return vim.lsp.diagnostic.on_diagnostic(...) end -M[ms.textDocument_codeLens] = function(...) +--- @private +RCS[ms.textDocument_codeLens] = function(...) return vim.lsp.codelens.on_codelens(...) end -M[ms.textDocument_inlayHint] = function(...) +--- @private +RCS[ms.textDocument_inlayHint] = function(...) return vim.lsp.inlay_hint.on_inlayhint(...) end ---- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references -M[ms.textDocument_references] = function(_, result, ctx, config) - if not result or vim.tbl_isempty(result) then - vim.notify('No references found') - return - end - - local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) - config = config or {} - 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, {}, ' ', list) - vim.cmd.lopen() - elseif config.on_list then - assert(vim.is_callable(config.on_list), 'on_list is not a function') - config.on_list(list) - else - 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 |vim.lsp.ListOpts| @@ -276,6 +236,7 @@ end ---@param title_fn fun(ctx: lsp.HandlerContext): string Function to call to generate list title ---@return lsp.Handler local function response_to_list(map_result, entity, title_fn) + --- @diagnostic disable-next-line:redundant-parameter return function(_, result, ctx, config) if not result or vim.tbl_isempty(result) then vim.notify('No ' .. entity .. ' found') @@ -299,8 +260,9 @@ local function response_to_list(map_result, entity, title_fn) end end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol -M[ms.textDocument_documentSymbol] = response_to_list( +RCS[ms.textDocument_documentSymbol] = response_to_list( util.symbols_to_items, 'document symbols', function(ctx) @@ -309,13 +271,15 @@ M[ms.textDocument_documentSymbol] = response_to_list( end ) +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol -M[ms.workspace_symbol] = response_to_list(util.symbols_to_items, 'symbols', function(ctx) +RCS[ms.workspace_symbol] = response_to_list(util.symbols_to_items, 'symbols', function(ctx) return string.format("Symbols matching '%s'", ctx.params.query) end) +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename -M[ms.textDocument_rename] = function(_, result, ctx, _) +RCS[ms.textDocument_rename] = function(_, result, ctx) if not result then vim.notify("Language server couldn't provide rename result", vim.log.levels.INFO) return @@ -324,8 +288,9 @@ M[ms.textDocument_rename] = function(_, result, ctx, _) util.apply_workspace_edit(result, client.offset_encoding) end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting -M[ms.textDocument_rangeFormatting] = function(_, result, ctx, _) +RCS[ms.textDocument_rangeFormatting] = function(_, result, ctx) if not result then return end @@ -333,8 +298,9 @@ M[ms.textDocument_rangeFormatting] = function(_, result, ctx, _) util.apply_text_edits(result, ctx.bufnr, client.offset_encoding) end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting -M[ms.textDocument_formatting] = function(_, result, ctx, _) +RCS[ms.textDocument_formatting] = function(_, result, ctx) if not result then return end @@ -342,8 +308,9 @@ M[ms.textDocument_formatting] = function(_, result, ctx, _) util.apply_text_edits(result, ctx.bufnr, client.offset_encoding) end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion -M[ms.textDocument_completion] = function(_, result, _, _) +RCS[ms.textDocument_completion] = function(_, result, _) if vim.tbl_isempty(result or {}) then return end @@ -358,6 +325,7 @@ M[ms.textDocument_completion] = function(_, result, _, _) vim.fn.complete(textMatch + 1, matches) end +--- @deprecated --- |lsp-handler| for the method "textDocument/hover" --- --- ```lua @@ -378,6 +346,7 @@ end --- - border: (default=nil) --- - Add borders to the floating window --- - See |vim.lsp.util.open_floating_preview()| for more options. +--- @diagnostic disable-next-line:redundant-parameter function M.hover(_, result, ctx, config) config = config or {} config.focus_id = ctx.method @@ -408,60 +377,14 @@ function M.hover(_, result, ctx, config) return util.open_floating_preview(contents, format, config) end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover -M[ms.textDocument_hover] = M.hover - ---- Jumps to a location. Used as a handler for multiple LSP methods. ----@param _ nil not used ----@param result (table) result of LSP method; a location or a list of locations. ----@param ctx (lsp.HandlerContext) table containing the context of the request, including the method ----@param config? vim.lsp.LocationOpts ----(`textDocument/definition` can return `Location` or `Location[]` -local function location_handler(_, result, ctx, config) - if result == nil or vim.tbl_isempty(result) then - log.info(ctx.method, 'No location found') - return nil - end - local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) - - config = config or {} - - -- textDocument/definition can return Location or Location[] - -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition - if not vim.islist(result) then - result = { result } - end +--- @diagnostic disable-next-line: deprecated +RCS[ms.textDocument_hover] = M.hover - local title = 'LSP locations' - local items = util.locations_to_items(result, client.offset_encoding) - - if config.on_list then - assert(vim.is_callable(config.on_list), 'on_list is not a function') - config.on_list({ title = title, items = items }) - return - end - if #result == 1 then - util.jump_to_location(result[1], client.offset_encoding, config.reuse_win) - return - end - 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 -M[ms.textDocument_declaration] = location_handler ---- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition -M[ms.textDocument_definition] = location_handler ---- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition -M[ms.textDocument_typeDefinition] = location_handler ---- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation -M[ms.textDocument_implementation] = location_handler +local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') +--- @deprecated remove in 0.13 --- |lsp-handler| for the method "textDocument/signatureHelp". --- --- The active parameter is highlighted with |hl-LspSignatureActiveParameter|. @@ -476,12 +399,13 @@ M[ms.textDocument_implementation] = location_handler --- ``` --- ---@param _ lsp.ResponseError? ----@param result lsp.SignatureHelp Response from the language server +---@param result lsp.SignatureHelp? Response from the language server ---@param ctx lsp.HandlerContext Client context ---@param config table Configuration table. --- - border: (default=nil) --- - Add borders to the floating window --- - See |vim.lsp.util.open_floating_preview()| for more options +--- @diagnostic disable-next-line:redundant-parameter function M.signature_help(_, result, ctx, config) config = config or {} config.focus_id = ctx.method @@ -509,19 +433,27 @@ function M.signature_help(_, result, ctx, config) return end local fbuf, fwin = util.open_floating_preview(lines, 'markdown', config) + -- Highlight the active parameter. if hl then - -- Highlight the second line if the signature is wrapped in a Markdown code block. - local line = vim.startswith(lines[1], '```') and 1 or 0 - api.nvim_buf_add_highlight(fbuf, -1, 'LspSignatureActiveParameter', line, unpack(hl)) + vim.hl.range( + fbuf, + sig_help_ns, + 'LspSignatureActiveParameter', + { hl[1], hl[2] }, + { hl[3], hl[4] } + ) end return fbuf, fwin end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp -M[ms.textDocument_signatureHelp] = M.signature_help +--- @diagnostic disable-next-line:deprecated +RCS[ms.textDocument_signatureHelp] = M.signature_help +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight -M[ms.textDocument_documentHighlight] = function(_, result, ctx, _) +RCS[ms.textDocument_documentHighlight] = function(_, result, ctx) if not result then return end @@ -564,11 +496,13 @@ local function make_call_hierarchy_handler(direction) end end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_incomingCalls -M[ms.callHierarchy_incomingCalls] = make_call_hierarchy_handler('from') +RCS[ms.callHierarchy_incomingCalls] = make_call_hierarchy_handler('from') +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_outgoingCalls -M[ms.callHierarchy_outgoingCalls] = make_call_hierarchy_handler('to') +RCS[ms.callHierarchy_outgoingCalls] = make_call_hierarchy_handler('to') --- Displays type hierarchy in the quickfix window. local function make_type_hierarchy_handler() @@ -603,17 +537,19 @@ local function make_type_hierarchy_handler() end end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#typeHierarchy_incomingCalls -M[ms.typeHierarchy_subtypes] = make_type_hierarchy_handler() +RCS[ms.typeHierarchy_subtypes] = make_type_hierarchy_handler() +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#typeHierarchy_outgoingCalls -M[ms.typeHierarchy_supertypes] = make_type_hierarchy_handler() +RCS[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, _) - local message_type = result.type - local message = result.message +--- @param params lsp.LogMessageParams +NSC['window/logMessage'] = function(_, params, ctx) + local message_type = params.type + local message = params.message local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) local client_name = client and client.name or string.format('id=%d', client_id) @@ -629,14 +565,14 @@ M[ms.window_logMessage] = function(_, result, ctx, _) else log.debug(message) end - return result + return params end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessage ---- @param result lsp.ShowMessageParams -M[ms.window_showMessage] = function(_, result, ctx, _) - local message_type = result.type - local message = result.message +--- @param params lsp.ShowMessageParams +NSC['window/showMessage'] = function(_, params, ctx) + local message_type = params.type + local message = params.message local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) local client_name = client and client.name or string.format('id=%d', client_id) @@ -650,15 +586,16 @@ M[ms.window_showMessage] = function(_, result, ctx, _) local message_type_name = protocol.MessageType[message_type] api.nvim_out_write(string.format('LSP[%s][%s] %s\n', client_name, message_type_name, message)) end - return result + return params end +--- @private --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showDocument ---- @param result lsp.ShowDocumentParams -M[ms.window_showDocument] = function(_, result, ctx, _) - local uri = result.uri +--- @param params lsp.ShowDocumentParams +RSC[ms.window_showDocument] = function(_, params, ctx) + local uri = params.uri - if result.external then + if params.external then -- TODO(lvimuser): ask the user for confirmation local cmd, err = vim.ui.open(uri) local ret = cmd and cmd:wait(2000) or nil @@ -686,35 +623,39 @@ M[ms.window_showDocument] = function(_, result, ctx, _) local location = { uri = uri, - range = result.selection, + range = params.selection, } local success = util.show_document(location, client.offset_encoding, { reuse_win = true, - focus = result.takeFocus, + focus = params.takeFocus, }) return { success = success or false } end ---@see https://microsoft.github.io/language-server-protocol/specification/#workspace_inlayHint_refresh -M[ms.workspace_inlayHint_refresh] = function(err, result, ctx, config) - return vim.lsp.inlay_hint.on_refresh(err, result, ctx, config) +RSC[ms.workspace_inlayHint_refresh] = function(err, result, ctx) + return vim.lsp.inlay_hint.on_refresh(err, result, ctx) end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#semanticTokens_refreshRequest -M[ms.workspace_semanticTokens_refresh] = function(err, result, ctx, _config) +RSC[ms.workspace_semanticTokens_refresh] = function(err, result, ctx) return vim.lsp.semantic_tokens._refresh(err, result, ctx) end +--- @nodoc +--- @type table<string, lsp.Handler> +M = vim.tbl_extend('force', M, RSC, NSC, RCS) + -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do + --- @diagnostic disable-next-line:redundant-parameter M[k] = function(err, result, ctx, config) if log.trace() then log.trace('default_handler', ctx.method, { err = err, result = result, ctx = vim.inspect(ctx), - config = config, }) end @@ -735,6 +676,7 @@ for k, fn in pairs(M) do return end + --- @diagnostic disable-next-line:redundant-parameter return fn(err, result, ctx, config) end end diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua index 18066a84db..0d314108fe 100644 --- a/runtime/lua/vim/lsp/health.lua +++ b/runtime/lua/vim/lsp/health.lua @@ -39,12 +39,27 @@ local function check_active_clients() elseif type(client.config.cmd) == 'function' then cmd = tostring(client.config.cmd) end + local dirs_info ---@type string + if client.workspace_folders and #client.workspace_folders > 1 then + dirs_info = string.format( + ' Workspace folders:\n %s', + vim + .iter(client.workspace_folders) + ---@param folder lsp.WorkspaceFolder + :map(function(folder) + return folder.name + end) + :join('\n ') + ) + else + dirs_info = string.format( + ' Root directory: %s', + client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~') + ) or nil + end report_info(table.concat({ string.format('%s (id: %d)', client.name, client.id), - string.format( - ' Root directory: %s', - client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~') or nil - ), + dirs_info, string.format(' Command: %s', cmd), string.format(' Settings: %s', vim.inspect(client.settings, { newline = '\n ' })), string.format( diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua index 61059180fe..f1ae9a8e9e 100644 --- a/runtime/lua/vim/lsp/inlay_hint.lua +++ b/runtime/lua/vim/lsp/inlay_hint.lua @@ -37,7 +37,7 @@ local augroup = api.nvim_create_augroup('vim_lsp_inlayhint', {}) ---@param result lsp.InlayHint[]? ---@param ctx lsp.HandlerContext ---@private -function M.on_inlayhint(err, result, ctx, _) +function M.on_inlayhint(err, result, ctx) if err then log.error('inlayhint', err) return @@ -65,37 +65,29 @@ function M.on_inlayhint(err, result, ctx, _) if num_unprocessed == 0 then client_hints[client_id] = {} bufstate.version = ctx.version - api.nvim__redraw({ buf = bufnr, valid = true }) + api.nvim__redraw({ buf = bufnr, valid = true, flush = false }) return end local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) - ---@param position lsp.Position - ---@return integer - local function pos_to_byte(position) - local col = position.character - if col > 0 then - local line = lines[position.line + 1] or '' - return util._str_byteindex_enc(line, col, client.offset_encoding) - end - return col - end for _, hint in ipairs(result) do local lnum = hint.position.line - hint.position.character = pos_to_byte(hint.position) + local line = lines and lines[lnum + 1] or '' + hint.position.character = + vim.str_byteindex(line, client.offset_encoding, hint.position.character, false) table.insert(new_lnum_hints[lnum], hint) end client_hints[client_id] = new_lnum_hints bufstate.version = ctx.version - api.nvim__redraw({ buf = bufnr, valid = true }) + api.nvim__redraw({ buf = bufnr, valid = true, flush = false }) end --- |lsp-handler| for the method `workspace/inlayHint/refresh` ---@param ctx lsp.HandlerContext ---@private -function M.on_refresh(err, _, ctx, _) +function M.on_refresh(err, _, ctx) if err then return vim.NIL end @@ -145,7 +137,7 @@ end --- @return vim.lsp.inlay_hint.get.ret[] --- @since 12 function M.get(filter) - vim.validate({ filter = { filter, 'table', true } }) + vim.validate('filter', filter, 'table', true) filter = filter or {} local bufnr = filter.bufnr @@ -223,7 +215,7 @@ local function clear(bufnr) end end api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) - api.nvim__redraw({ buf = bufnr, valid = true }) + api.nvim__redraw({ buf = bufnr, valid = true, flush = false }) end --- Disable inlay hints for a buffer @@ -375,11 +367,11 @@ api.nvim_set_decoration_provider(namespace, { --- @return boolean --- @since 12 function M.is_enabled(filter) - vim.validate({ filter = { filter, 'table', true } }) + vim.validate('filter', filter, 'table', true) filter = filter or {} local bufnr = filter.bufnr - vim.validate({ bufnr = { bufnr, 'number', true } }) + vim.validate('bufnr', bufnr, 'number', true) if bufnr == nil then return globalstate.enabled elseif bufnr == 0 then @@ -406,7 +398,8 @@ end --- @param filter vim.lsp.inlay_hint.enable.Filter? --- @since 12 function M.enable(enable, filter) - vim.validate({ enable = { enable, 'boolean', true }, filter = { filter, 'table', true } }) + vim.validate('enable', enable, 'boolean', true) + vim.validate('filter', filter, 'table', true) enable = enable == nil or enable filter = filter or {} diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index 4f177b47fd..ec78dd3dc5 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -32,12 +32,12 @@ local function notify(msg, level) end end -local logfilename = vim.fs.joinpath(vim.fn.stdpath('log'), 'lsp.log') +local logfilename = vim.fs.joinpath(vim.fn.stdpath('log') --[[@as string]], 'lsp.log') -- TODO: Ideally the directory should be created in open_logfile(), right -- before opening the log file, but open_logfile() can be called from libuv -- callbacks, where using fn.mkdir() is not allowed. -vim.fn.mkdir(vim.fn.stdpath('log'), 'p') +vim.fn.mkdir(vim.fn.stdpath('log') --[[@as string]], 'p') --- Returns the log filename. ---@return string log filename @@ -82,6 +82,7 @@ end for level, levelnr in pairs(log_levels) do -- Also export the log level on the root object. + ---@diagnostic disable-next-line: no-unknown log[level] = levelnr -- Add a reverse lookup. @@ -93,7 +94,7 @@ end --- @return fun(...:any): boolean? local function create_logger(level, levelnr) return function(...) - if levelnr < current_log_level then + if not log.should_log(levelnr) then return false end local argc = select('#', ...) @@ -169,7 +170,7 @@ end --- Checks whether the level is sufficient for logging. ---@param level integer log level ----@return bool : true if would log, false if not +---@return boolean : true if would log, false if not function log.should_log(level) return level >= current_log_level end diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 1699fff0c1..7db48b0c06 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -12,6 +12,8 @@ end local sysname = vim.uv.os_uname().sysname +--- @class vim.lsp.protocol.constants +--- @nodoc local constants = { --- @enum lsp.DiagnosticSeverity DiagnosticSeverity = { @@ -314,7 +316,9 @@ local constants = { }, } --- Protocol for the Microsoft Language Server Protocol (mslsp) +--- Protocol for the Microsoft Language Server Protocol (mslsp) +--- @class vim.lsp.protocol : vim.lsp.protocol.constants +--- @nodoc local protocol = {} --- @diagnostic disable:no-unknown @@ -334,7 +338,9 @@ function protocol.make_client_capabilities() return { general = { positionEncodings = { + 'utf-8', 'utf-16', + 'utf-32', }, }, textDocument = { @@ -615,9 +621,109 @@ function protocol.resolve_capabilities(server_capabilities) end -- Generated by gen_lsp.lua, keep at end of file. +--- @alias vim.lsp.protocol.Method.ClientToServer +--- | 'callHierarchy/incomingCalls', +--- | 'callHierarchy/outgoingCalls', +--- | 'codeAction/resolve', +--- | 'codeLens/resolve', +--- | 'completionItem/resolve', +--- | 'documentLink/resolve', +--- | '$/setTrace', +--- | 'exit', +--- | 'initialize', +--- | 'initialized', +--- | 'inlayHint/resolve', +--- | 'notebookDocument/didChange', +--- | 'notebookDocument/didClose', +--- | 'notebookDocument/didOpen', +--- | 'notebookDocument/didSave', +--- | 'shutdown', +--- | 'textDocument/codeAction', +--- | 'textDocument/codeLens', +--- | 'textDocument/colorPresentation', +--- | 'textDocument/completion', +--- | 'textDocument/declaration', +--- | 'textDocument/definition', +--- | 'textDocument/diagnostic', +--- | 'textDocument/didChange', +--- | 'textDocument/didClose', +--- | 'textDocument/didOpen', +--- | 'textDocument/didSave', +--- | 'textDocument/documentColor', +--- | 'textDocument/documentHighlight', +--- | 'textDocument/documentLink', +--- | 'textDocument/documentSymbol', +--- | 'textDocument/foldingRange', +--- | 'textDocument/formatting', +--- | 'textDocument/hover', +--- | 'textDocument/implementation', +--- | 'textDocument/inlayHint', +--- | 'textDocument/inlineCompletion', +--- | 'textDocument/inlineValue', +--- | 'textDocument/linkedEditingRange', +--- | 'textDocument/moniker', +--- | 'textDocument/onTypeFormatting', +--- | 'textDocument/prepareCallHierarchy', +--- | 'textDocument/prepareRename', +--- | 'textDocument/prepareTypeHierarchy', +--- | 'textDocument/rangeFormatting', +--- | 'textDocument/rangesFormatting', +--- | 'textDocument/references', +--- | 'textDocument/rename', +--- | 'textDocument/selectionRange', +--- | 'textDocument/semanticTokens/full', +--- | 'textDocument/semanticTokens/full/delta', +--- | 'textDocument/semanticTokens/range', +--- | 'textDocument/signatureHelp', +--- | 'textDocument/typeDefinition', +--- | 'textDocument/willSave', +--- | 'textDocument/willSaveWaitUntil', +--- | 'typeHierarchy/subtypes', +--- | 'typeHierarchy/supertypes', +--- | 'window/workDoneProgress/cancel', +--- | 'workspaceSymbol/resolve', +--- | 'workspace/diagnostic', +--- | 'workspace/didChangeConfiguration', +--- | 'workspace/didChangeWatchedFiles', +--- | 'workspace/didChangeWorkspaceFolders', +--- | 'workspace/didCreateFiles', +--- | 'workspace/didDeleteFiles', +--- | 'workspace/didRenameFiles', +--- | 'workspace/executeCommand', +--- | 'workspace/symbol', +--- | 'workspace/willCreateFiles', +--- | 'workspace/willDeleteFiles', +--- | 'workspace/willRenameFiles', + +--- @alias vim.lsp.protocol.Method.ServerToClient +--- | 'client/registerCapability', +--- | 'client/unregisterCapability', +--- | '$/logTrace', +--- | 'telemetry/event', +--- | 'textDocument/publishDiagnostics', +--- | 'window/logMessage', +--- | 'window/showDocument', +--- | 'window/showMessage', +--- | 'window/showMessageRequest', +--- | 'window/workDoneProgress/create', +--- | 'workspace/applyEdit', +--- | 'workspace/codeLens/refresh', +--- | 'workspace/configuration', +--- | 'workspace/diagnostic/refresh', +--- | 'workspace/foldingRange/refresh', +--- | 'workspace/inlayHint/refresh', +--- | 'workspace/inlineValue/refresh', +--- | 'workspace/semanticTokens/refresh', +--- | 'workspace/workspaceFolders', + +--- @alias vim.lsp.protocol.Method +--- | vim.lsp.protocol.Method.ClientToServer +--- | vim.lsp.protocol.Method.ServerToClient + +-- Generated by gen_lsp.lua, keep at end of file. --- ----@enum vim.lsp.protocol.Methods ----@see https://microsoft.github.io/language-server-protocol/specification/#metaModel +--- @enum vim.lsp.protocol.Methods +--- @see https://microsoft.github.io/language-server-protocol/specification/#metaModel --- LSP method names. protocol.Methods = { --- A request to resolve the incoming calls for a given `CallHierarchyItem`. diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index e79dbd2db3..6c8564845f 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -1,7 +1,7 @@ local uv = vim.uv local log = require('vim.lsp.log') local protocol = require('vim.lsp.protocol') -local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap +local validate, schedule_wrap = vim.validate, vim.schedule_wrap local is_win = vim.fn.has('win32') == 1 @@ -152,9 +152,7 @@ end ---@param err table The error object ---@return string error_message The formatted error message function M.format_rpc_error(err) - validate({ - err = { err, 't' }, - }) + validate('err', err, 'table') -- There is ErrorCodes in the LSP specification, -- but in ResponseError.code it is not used and the actual type is number. @@ -329,10 +327,8 @@ end ---@return boolean success `true` if request could be sent, `false` if not ---@return integer? message_id if request could be sent, `nil` if not function Client:request(method, params, callback, notify_reply_callback) - validate({ - callback = { callback, 'f' }, - notify_reply_callback = { notify_reply_callback, 'f', true }, - }) + validate('callback', callback, 'function') + validate('notify_reply_callback', notify_reply_callback, 'function', true) self.message_index = self.message_index + 1 local message_id = self.message_index local result = self:encode_and_send({ @@ -413,49 +409,44 @@ function Client:handle_body(body) local err --- @type lsp.ResponseError|nil -- Schedule here so that the users functions don't trigger an error and -- we can still use the result. - schedule(function() - coroutine.wrap(function() - local status, result - status, result, err = self:try_call( - M.client_errors.SERVER_REQUEST_HANDLER_ERROR, - self.dispatchers.server_request, - decoded.method, - decoded.params - ) - log.debug( - 'server_request: callback result', - { status = status, result = result, err = err } - ) - if status then - if result == nil and err == nil then - error( - string.format( - 'method %q: either a result or an error must be sent to the server in response', - decoded.method - ) - ) - end - if err then - ---@cast err lsp.ResponseError - assert( - type(err) == 'table', - 'err must be a table. Use rpc_response_error to help format errors.' - ) - ---@type string - local code_name = assert( - protocol.ErrorCodes[err.code], - 'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.' + vim.schedule(coroutine.wrap(function() + local status, result + status, result, err = self:try_call( + M.client_errors.SERVER_REQUEST_HANDLER_ERROR, + self.dispatchers.server_request, + decoded.method, + decoded.params + ) + log.debug('server_request: callback result', { status = status, result = result, err = err }) + if status then + if result == nil and err == nil then + error( + string.format( + 'method %q: either a result or an error must be sent to the server in response', + decoded.method ) - err.message = err.message or code_name - end - else - -- On an exception, result will contain the error message. - err = M.rpc_response_error(protocol.ErrorCodes.InternalError, result) - result = nil + ) + end + if err then + ---@cast err lsp.ResponseError + assert( + type(err) == 'table', + 'err must be a table. Use rpc_response_error to help format errors.' + ) + ---@type string + local code_name = assert( + protocol.ErrorCodes[err.code], + 'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.' + ) + err.message = err.message or code_name end - self:send_response(decoded.id, err, result) - end)() - end) + else + -- On an exception, result will contain the error message. + err = M.rpc_response_error(protocol.ErrorCodes.InternalError, result) + result = nil + end + self:send_response(decoded.id, err, result) + end)) -- This works because we are expecting vim.NIL here elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then -- We sent a number, so we expect a number. @@ -465,9 +456,7 @@ function Client:handle_body(body) local notify_reply_callbacks = self.notify_reply_callbacks local notify_reply_callback = notify_reply_callbacks and notify_reply_callbacks[result_id] if notify_reply_callback then - validate({ - notify_reply_callback = { notify_reply_callback, 'f' }, - }) + validate('notify_reply_callback', notify_reply_callback, 'function') notify_reply_callback(result_id) notify_reply_callbacks[result_id] = nil end @@ -498,9 +487,7 @@ function Client:handle_body(body) local callback = message_callbacks and message_callbacks[result_id] if callback then message_callbacks[result_id] = nil - validate({ - callback = { callback, 'f' }, - }) + validate('callback', callback, 'function') if decoded.error then decoded.error = setmetatable(decoded.error, { __tostring = M.format_rpc_error, @@ -734,10 +721,8 @@ end function M.start(cmd, dispatchers, extra_spawn_params) log.info('Starting RPC client', { cmd = cmd, extra = extra_spawn_params }) - validate({ - cmd = { cmd, 't' }, - dispatchers = { dispatchers, 't', true }, - }) + validate('cmd', cmd, 'table') + validate('dispatchers', dispatchers, 'table', true) extra_spawn_params = extra_spawn_params or {} diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 8182457dd0..215e5f41aa 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -99,11 +99,12 @@ local function tokens_to_ranges(data, bufnr, client, request) local legend = client.server_capabilities.semanticTokensProvider.legend local token_types = legend.tokenTypes local token_modifiers = legend.tokenModifiers + local encoding = client.offset_encoding local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) local ranges = {} ---@type STTokenRange[] local start = uv.hrtime() - local ms_to_ns = 1000 * 1000 + local ms_to_ns = 1e6 local yield_interval_ns = 5 * ms_to_ns local co, is_main = coroutine.running() @@ -135,20 +136,13 @@ local function tokens_to_ranges(data, bufnr, client, request) -- data[i+3] +1 because Lua tables are 1-indexed local token_type = token_types[data[i + 3] + 1] - local modifiers = modifiers_from_number(data[i + 4], token_modifiers) - - local function _get_byte_pos(col) - if col > 0 then - local buf_line = lines[line + 1] or '' - return util._str_byteindex_enc(buf_line, col, client.offset_encoding) - end - return col - end - - local start_col = _get_byte_pos(start_char) - local end_col = _get_byte_pos(start_char + data[i + 2]) if token_type then + local modifiers = modifiers_from_number(data[i + 4], token_modifiers) + local end_char = start_char + data[i + 2] + local buf_line = lines and lines[line + 1] or '' + local start_col = vim.str_byteindex(buf_line, encoding, start_char, false) + local end_col = vim.str_byteindex(buf_line, encoding, end_char, false) ranges[#ranges + 1] = { line = line, start_col = start_col, @@ -386,6 +380,37 @@ function STHighlighter:process_response(response, client, version) api.nvim__redraw({ buf = self.bufnr, valid = true }) end +--- @param bufnr integer +--- @param ns integer +--- @param token STTokenRange +--- @param hl_group string +--- @param priority integer +local function set_mark(bufnr, ns, token, hl_group, priority) + vim.api.nvim_buf_set_extmark(bufnr, ns, token.line, token.start_col, { + hl_group = hl_group, + end_col = token.end_col, + priority = priority, + strict = false, + }) +end + +--- @param lnum integer +--- @param foldend integer? +--- @return boolean, integer? +local function check_fold(lnum, foldend) + if foldend and lnum <= foldend then + return true, foldend + end + + local folded = vim.fn.foldclosed(lnum) + + if folded == -1 then + return false, nil + end + + return folded ~= lnum, vim.fn.foldclosedend(lnum) +end + --- on_win handler for the decoration provider (see |nvim_set_decoration_provider|) --- --- If there is a current result for the buffer and the version matches the @@ -439,13 +464,14 @@ function STHighlighter:on_win(topline, botline) -- finishes, clangd sends a refresh request which lets the client -- re-synchronize the tokens. - local set_mark = function(token, hl_group, delta) - vim.api.nvim_buf_set_extmark(self.bufnr, state.namespace, token.line, token.start_col, { - hl_group = hl_group, - end_col = token.end_col, - priority = vim.highlight.priorities.semantic_tokens + delta, - strict = false, - }) + local function set_mark0(token, hl_group, delta) + set_mark( + self.bufnr, + state.namespace, + token, + hl_group, + vim.hl.priorities.semantic_tokens + delta + ) end local ft = vim.bo[self.bufnr].filetype @@ -453,13 +479,19 @@ function STHighlighter:on_win(topline, botline) local first = lower_bound(highlights, topline, 1, #highlights + 1) local last = upper_bound(highlights, botline, first, #highlights + 1) - 1 + --- @type boolean?, integer? + local is_folded, foldend + for i = first, last do local token = highlights[i] - if not token.marked then - set_mark(token, string.format('@lsp.type.%s.%s', token.type, ft), 0) - for modifier, _ in pairs(token.modifiers) do - set_mark(token, string.format('@lsp.mod.%s.%s', modifier, ft), 1) - set_mark(token, string.format('@lsp.typemod.%s.%s.%s', token.type, modifier, ft), 2) + + is_folded, foldend = check_fold(token.line + 1, foldend) + + if not is_folded and not token.marked then + set_mark0(token, string.format('@lsp.type.%s.%s', token.type, ft), 0) + for modifier in pairs(token.modifiers) do + set_mark0(token, string.format('@lsp.mod.%s.%s', modifier, ft), 1) + set_mark0(token, string.format('@lsp.typemod.%s.%s.%s', token.type, modifier, ft), 2) end token.marked = true @@ -565,10 +597,8 @@ local M = {} --- - debounce (integer, default: 200): Debounce token requests --- to the server by the given number in milliseconds function M.start(bufnr, client_id, opts) - vim.validate({ - bufnr = { bufnr, 'n', false }, - client_id = { client_id, 'n', false }, - }) + vim.validate('bufnr', bufnr, 'number') + vim.validate('client_id', client_id, 'number') if bufnr == 0 then bufnr = api.nvim_get_current_buf() @@ -622,10 +652,8 @@ end ---@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 }, - }) + vim.validate('bufnr', bufnr, 'number') + vim.validate('client_id', client_id, 'number') if bufnr == 0 then bufnr = api.nvim_get_current_buf() @@ -708,9 +736,7 @@ end ---@param bufnr (integer|nil) filter by buffer. All buffers if nil, current --- buffer if 0 function M.force_refresh(bufnr) - vim.validate({ - bufnr = { bufnr, 'n', true }, - }) + vim.validate('bufnr', bufnr, 'number', true) local buffers = bufnr == nil and vim.tbl_keys(STHighlighter.active) or bufnr == 0 and { api.nvim_get_current_buf() } @@ -729,7 +755,7 @@ end --- @inlinedoc --- --- Priority for the applied extmark. ---- (Default: `vim.highlight.priorities.semantic_tokens + 3`) +--- (Default: `vim.hl.priorities.semantic_tokens + 3`) --- @field priority? integer --- Highlight a semantic token. @@ -757,15 +783,9 @@ function M.highlight_token(token, bufnr, client_id, hl_group, opts) return end - opts = opts or {} - local priority = opts.priority or vim.highlight.priorities.semantic_tokens + 3 + local priority = opts and opts.priority or vim.hl.priorities.semantic_tokens + 3 - vim.api.nvim_buf_set_extmark(bufnr, state.namespace, token.line, token.start_col, { - hl_group = hl_group, - end_col = token.end_col, - priority = priority, - strict = false, - }) + set_mark(bufnr, state.namespace, token, hl_group, priority) end --- |lsp-handler| for the method `workspace/semanticTokens/refresh` diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua index bdfe8d51b8..3df45ebff0 100644 --- a/runtime/lua/vim/lsp/sync.lua +++ b/runtime/lua/vim/lsp/sync.lua @@ -48,45 +48,6 @@ local str_utfindex = vim.str_utfindex local str_utf_start = vim.str_utf_start local str_utf_end = vim.str_utf_end --- Given a line, byte idx, and offset_encoding convert to the --- utf-8, utf-16, or utf-32 index. ----@param line string the line to index into ----@param byte integer the byte idx ----@param offset_encoding string utf-8|utf-16|utf-32|nil (default: utf-8) ----@return integer utf_idx for the given encoding -local function byte_to_utf(line, byte, offset_encoding) - -- convert to 0 based indexing for str_utfindex - byte = byte - 1 - - local utf_idx, _ --- @type integer, integer - -- Convert the byte range to utf-{8,16,32} and convert 1-based (lua) indexing to 0-based - if offset_encoding == 'utf-16' then - _, utf_idx = str_utfindex(line, byte) - elseif offset_encoding == 'utf-32' then - utf_idx, _ = str_utfindex(line, byte) - else - utf_idx = byte - end - - -- convert to 1 based indexing - return utf_idx + 1 -end - ----@param line string ----@param offset_encoding string ----@return integer -local function compute_line_length(line, offset_encoding) - local length, _ --- @type integer, integer - if offset_encoding == 'utf-16' then - _, length = str_utfindex(line) - elseif offset_encoding == 'utf-32' then - length, _ = str_utfindex(line) - else - length = #line - end - return length -end - -- Given a line, byte idx, alignment, and offset_encoding convert to the aligned -- utf-8 index and either the utf-16, or utf-32 index. ---@param line string the line to index into @@ -101,7 +62,7 @@ local function align_end_position(line, byte, offset_encoding) char = byte -- Called in the case of extending an empty line "" -> "a" elseif byte == #line + 1 then - char = compute_line_length(line, offset_encoding) + 1 + char = str_utfindex(line, offset_encoding) + 1 else -- Modifying line, find the nearest utf codepoint local offset = str_utf_start(line, byte) @@ -111,9 +72,10 @@ local function align_end_position(line, byte, offset_encoding) byte = byte + str_utf_end(line, byte) + 1 end if byte <= #line then - char = byte_to_utf(line, byte, offset_encoding) + --- Convert to 0 based for input, and from 0 based for output + char = str_utfindex(line, offset_encoding, byte - 1) + 1 else - char = compute_line_length(line, offset_encoding) + 1 + char = str_utfindex(line, offset_encoding) + 1 end -- Extending line, find the nearest utf codepoint for the last valid character end @@ -153,7 +115,7 @@ local function compute_start_range( if line then line_idx = firstline - 1 byte_idx = #line + 1 - char_idx = compute_line_length(line, offset_encoding) + 1 + char_idx = str_utfindex(line, offset_encoding) + 1 else line_idx = firstline byte_idx = 1 @@ -190,10 +152,11 @@ local function compute_start_range( char_idx = 1 elseif start_byte_idx == #prev_line + 1 then byte_idx = start_byte_idx - char_idx = compute_line_length(prev_line, offset_encoding) + 1 + char_idx = str_utfindex(prev_line, offset_encoding) + 1 else byte_idx = start_byte_idx + str_utf_start(prev_line, start_byte_idx) - char_idx = byte_to_utf(prev_line, byte_idx, offset_encoding) + --- Convert to 0 based for input, and from 0 based for output + char_idx = vim.str_utfindex(prev_line, offset_encoding, byte_idx - 1) + 1 end -- Return the start difference (shared for new and prev lines) @@ -230,7 +193,7 @@ local function compute_end_range( return { line_idx = lastline - 1, byte_idx = #prev_line + 1, - char_idx = compute_line_length(prev_line, offset_encoding) + 1, + char_idx = str_utfindex(prev_line, offset_encoding) + 1, }, { line_idx = 1, byte_idx = 1, char_idx = 1 } end -- If firstline == new_lastline, the first change occurred on a line that was deleted. @@ -376,7 +339,7 @@ local function compute_range_length(lines, start_range, end_range, offset_encodi local start_line = lines[start_range.line_idx] local range_length --- @type integer if start_line and #start_line > 0 then - range_length = compute_line_length(start_line, offset_encoding) + range_length = str_utfindex(start_line, offset_encoding) - start_range.char_idx + 1 + line_ending_length @@ -389,7 +352,7 @@ local function compute_range_length(lines, start_range, end_range, offset_encodi for idx = start_range.line_idx + 1, end_range.line_idx - 1 do -- Length full line plus newline character if #lines[idx] > 0 then - range_length = range_length + compute_line_length(lines[idx], offset_encoding) + #line_ending + range_length = range_length + str_utfindex(lines[idx], offset_encoding) + #line_ending else range_length = range_length + line_ending_length end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 882ec22ca6..6eab0f3da4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2,12 +2,8 @@ local protocol = require('vim.lsp.protocol') local validate = vim.validate local api = vim.api local list_extend = vim.list_extend -local highlight = vim.highlight local uv = vim.uv -local npcall = vim.F.npcall -local split = vim.split - local M = {} local default_border = { @@ -21,82 +17,73 @@ local default_border = { { ' ', 'NormalFloat' }, } +--- @param border string|(string|[string,string])[] +local function border_error(border) + error( + string.format( + 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', + vim.inspect(border) + ), + 2 + ) +end + +local border_size = { + none = { 0, 0 }, + single = { 2, 2 }, + double = { 2, 2 }, + rounded = { 2, 2 }, + solid = { 2, 2 }, + shadow = { 1, 1 }, +} + --- Check the border given by opts or the default border for the additional --- size it adds to a float. ----@param opts table optional options for the floating window ---- - border (string or table) the border ----@return table size of border in the form of { height = height, width = width } +--- @param opts? {border:string|(string|[string,string])[]} +--- @return integer height +--- @return integer width local function get_border_size(opts) local border = opts and opts.border or default_border - local height = 0 - local width = 0 if type(border) == 'string' then - local border_size = { - none = { 0, 0 }, - single = { 2, 2 }, - double = { 2, 2 }, - rounded = { 2, 2 }, - solid = { 2, 2 }, - shadow = { 1, 1 }, - } - if border_size[border] == nil then - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) + if not border_size[border] then + border_error(border) end - height, width = unpack(border_size[border]) - else - if 8 % #border ~= 0 then - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) - end - local function border_width(id) - id = (id - 1) % #border + 1 - if type(border[id]) == 'table' then - -- border specified as a table of <character, highlight group> - return vim.fn.strdisplaywidth(border[id][1]) - elseif type(border[id]) == 'string' then - -- border specified as a list of border characters - return vim.fn.strdisplaywidth(border[id]) - end - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) - end - local function border_height(id) - id = (id - 1) % #border + 1 - if type(border[id]) == 'table' then - -- border specified as a table of <character, highlight group> - return #border[id][1] > 0 and 1 or 0 - elseif type(border[id]) == 'string' then - -- border specified as a list of border characters - return #border[id] > 0 and 1 or 0 - end - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) + return unpack(border_size[border]) + end + + if 8 % #border ~= 0 then + border_error(border) + end + + --- @param id integer + --- @return string + local function elem(id) + id = (id - 1) % #border + 1 + local e = border[id] + if type(e) == 'table' then + -- border specified as a table of <character, highlight group> + return e[1] + elseif type(e) == 'string' then + -- border specified as a list of border characters + return e end - height = height + border_height(2) -- top - height = height + border_height(6) -- bottom - width = width + border_width(4) -- right - width = width + border_width(8) -- left + --- @diagnostic disable-next-line:missing-return + border_error(border) + end + + --- @param e string + local function border_height(e) + return #e > 0 and 1 or 0 end - return { height = height, width = width } + local top, bottom = elem(2), elem(6) + local height = border_height(top) + border_height(bottom) + + local right, left = elem(4), elem(8) + local width = vim.fn.strdisplaywidth(right) + vim.fn.strdisplaywidth(left) + + return height, width end --- Splits string at newlines, optionally removing unwanted blank lines. @@ -122,79 +109,13 @@ local function split_lines(s, no_blank) end local function create_window_without_focus() - local prev = vim.api.nvim_get_current_win() + local prev = api.nvim_get_current_win() vim.cmd.new() - local new = vim.api.nvim_get_current_win() - vim.api.nvim_set_current_win(prev) + local new = api.nvim_get_current_win() + api.nvim_set_current_win(prev) return new end ---- Convert byte index to `encoding` index. ---- Convenience wrapper around vim.str_utfindex ----@param line string line to be indexed ----@param index integer|nil byte index (utf-8), or `nil` for length ----@param encoding 'utf-8'|'utf-16'|'utf-32'|nil defaults to utf-16 ----@return integer `encoding` index of `index` in `line` -function M._str_utfindex_enc(line, index, encoding) - local len32, len16 = vim.str_utfindex(line) - if not encoding then - encoding = 'utf-16' - end - if encoding == 'utf-8' then - if index then - return index - else - return #line - end - elseif encoding == 'utf-16' then - if not index or index > len16 then - return len16 - end - local _, col16 = vim.str_utfindex(line, index) - return col16 - elseif encoding == 'utf-32' then - if not index or index > len32 then - return len32 - end - local col32, _ = vim.str_utfindex(line, index) - return col32 - else - error('Invalid encoding: ' .. vim.inspect(encoding)) - end -end - ---- Convert UTF index to `encoding` index. ---- Convenience wrapper around vim.str_byteindex ----Alternative to vim.str_byteindex that takes an encoding. ----@param line string line to be indexed ----@param index integer UTF index ----@param encoding string utf-8|utf-16|utf-32| defaults to utf-16 ----@return integer byte (utf-8) index of `encoding` index `index` in `line` -function M._str_byteindex_enc(line, index, encoding) - local len = #line - if index > len then - -- LSP spec: if character > line length, default to the line length. - -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position - return len - end - if not encoding then - encoding = 'utf-16' - end - if encoding == 'utf-8' then - if index then - return index - else - return len - end - elseif encoding == 'utf-16' then - return vim.str_byteindex(line, index, true) - elseif encoding == 'utf-32' then - return vim.str_byteindex(line, index) - else - error('Invalid encoding: ' .. vim.inspect(encoding)) - end -end - --- Replaces text in a range with new text. --- --- CAUTION: Changes in-place! @@ -243,6 +164,8 @@ function M.set_lines(lines, A, B, new_lines) return lines end +--- @param fn fun(x:any):any[] +--- @return function local function sort_by_key(fn) return function(a, b) local ka, kb = fn(a), fn(b) @@ -354,7 +277,7 @@ end --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position ---@param position lsp.Position ----@param offset_encoding? string utf-8|utf-16|utf-32 +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' ---@return integer local function get_line_byte_from_position(bufnr, position, offset_encoding) -- LSP's line and characters are 0-indexed @@ -364,7 +287,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding) -- character if col > 0 then local line = get_line(bufnr, position.line) or '' - return M._str_byteindex_enc(line, col, offset_encoding or 'utf-16') + return vim.str_byteindex(line, offset_encoding, col, false) end return col end @@ -372,14 +295,13 @@ end --- Applies a list of text edits to a buffer. ---@param text_edits lsp.TextEdit[] ---@param bufnr integer Buffer id ----@param offset_encoding string utf-8|utf-16|utf-32 +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit function M.apply_text_edits(text_edits, bufnr, offset_encoding) - validate({ - text_edits = { text_edits, 't', false }, - bufnr = { bufnr, 'number', false }, - offset_encoding = { offset_encoding, 'string', false }, - }) + validate('text_edits', text_edits, 'table', false) + validate('bufnr', bufnr, 'number', false) + validate('offset_encoding', offset_encoding, 'string', false) + if not next(text_edits) then return end @@ -392,10 +314,8 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) vim.bo[bufnr].buflisted = true -- Fix reversed range and indexing each text_edits - local index = 0 - --- @param text_edit lsp.TextEdit - text_edits = vim.tbl_map(function(text_edit) - index = index + 1 + for index, text_edit in ipairs(text_edits) do + --- @cast text_edit lsp.TextEdit|{_index: integer} text_edit._index = index if @@ -407,8 +327,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) text_edit.range.start = text_edit.range['end'] text_edit.range['end'] = start end - return text_edit - end, text_edits) + end -- Sort text_edits ---@param a lsp.TextEdit | { _index: integer } @@ -439,47 +358,45 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n') -- Convert from LSP style ranges to Neovim style ranges. - local e = { - start_row = text_edit.range.start.line, - start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding), - end_row = text_edit.range['end'].line, - end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding), - text = split(text_edit.newText, '\n', { plain = true }), - } + local start_row = text_edit.range.start.line + local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding) + local end_row = text_edit.range['end'].line + local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding) + local text = vim.split(text_edit.newText, '\n', { plain = true }) local max = api.nvim_buf_line_count(bufnr) -- If the whole edit is after the lines in the buffer we can simply add the new text to the end -- of the buffer. - if max <= e.start_row then - api.nvim_buf_set_lines(bufnr, max, max, false, e.text) + if max <= start_row then + api.nvim_buf_set_lines(bufnr, max, max, false, text) else - local last_line_len = #(get_line(bufnr, math.min(e.end_row, max - 1)) or '') + local last_line_len = #(get_line(bufnr, math.min(end_row, max - 1)) or '') -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't -- accept it so we should fix it here. - if max <= e.end_row then - e.end_row = max - 1 - e.end_col = last_line_len + if max <= end_row then + end_row = max - 1 + end_col = last_line_len has_eol_text_edit = true else - -- If the replacement is over the end of a line (i.e. e.end_col is equal to the line length and the + -- If the replacement is over the end of a line (i.e. end_col is equal to the line length and the -- replacement text ends with a newline We can likely assume that the replacement is assumed -- to be meant to replace the newline with another newline and we need to make sure this -- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r' -- in the file some servers (clangd on windows) will include that character in the line -- while nvim_buf_set_text doesn't count it as part of the line. if - e.end_col >= last_line_len - and text_edit.range['end'].character > e.end_col + end_col >= last_line_len + and text_edit.range['end'].character > end_col and #text_edit.newText > 0 and string.sub(text_edit.newText, -1) == '\n' then - table.remove(e.text, #e.text) + table.remove(text, #text) end end - -- Make sure we don't go out of bounds for e.end_col - e.end_col = math.min(last_line_len, e.end_col) + -- Make sure we don't go out of bounds for end_col + end_col = math.min(last_line_len, end_col) - api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text) + api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, text) end end @@ -495,7 +412,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) -- make sure we don't go out of bounds pos[1] = math.min(pos[1], max) pos[2] = math.min(pos[2], #(get_line(bufnr, pos[1] - 1) or '')) - vim.api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {}) + api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {}) end end @@ -513,7 +430,7 @@ end --- ---@param text_document_edit lsp.TextDocumentEdit ---@param index? integer: Optional index of the edit, if from a list of edits (or nil, if not from a list) ----@param offset_encoding? string +---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32' ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit function M.apply_text_document_edit(text_document_edit, index, offset_encoding) local text_document = text_document_edit.textDocument @@ -523,19 +440,15 @@ function M.apply_text_document_edit(text_document_edit, index, offset_encoding) 'apply_text_document_edit must be called with valid offset encoding', vim.log.levels.WARN ) - end - - -- For lists of text document edits, - -- do not check the version after the first edit. - local should_check_version = true - if index and index > 1 then - should_check_version = false + return end -- `VersionedTextDocumentIdentifier`s version may be null -- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier if - should_check_version + -- For lists of text document edits, + -- do not check the version after the first edit. + not (index and index > 1) and ( text_document.version and text_document.version > 0 @@ -553,6 +466,9 @@ local function path_components(path) return vim.split(path, '/', { plain = true }) end +--- @param path string[] +--- @param prefix string[] +--- @return boolean local function path_under_prefix(path, prefix) for i, c in ipairs(prefix) do if c ~= path[i] then @@ -562,17 +478,24 @@ local function path_under_prefix(path, prefix) return true end ---- Get list of buffers whose filename matches the given path prefix (normalized full path) +--- Get list of loaded writable buffers whose filename matches the given path +--- prefix (normalized full path). ---@param prefix string ---@return integer[] -local function get_bufs_with_prefix(prefix) - prefix = path_components(prefix) - local buffers = {} - for _, v in ipairs(vim.api.nvim_list_bufs()) do - local bname = vim.api.nvim_buf_get_name(v) - local path = path_components(vim.fs.normalize(bname, { expand_env = false })) - if path_under_prefix(path, prefix) then - table.insert(buffers, v) +local function get_writable_bufs(prefix) + local prefix_parts = path_components(prefix) + local buffers = {} --- @type integer[] + for _, buf in ipairs(api.nvim_list_bufs()) do + -- No need to care about unloaded or nofile buffers. Also :saveas won't work for them. + if + api.nvim_buf_is_loaded(buf) + and not vim.list_contains({ 'nofile', 'nowrite' }, vim.bo[buf].buftype) + then + local bname = api.nvim_buf_get_name(buf) + local path = path_components(vim.fs.normalize(bname, { expand_env = false })) + if path_under_prefix(path, prefix_parts) then + buffers[#buffers + 1] = buf + end end end return buffers @@ -616,19 +539,13 @@ function M.rename(old_fname, new_fname, opts) local buf_rename = {} ---@type table<integer, {from: string, to: string}> local old_fname_pat = '^' .. vim.pesc(old_fname_full) - for b in - vim.iter(get_bufs_with_prefix(old_fname_full)):filter(function(b) - -- No need to care about unloaded or nofile buffers. Also :saveas won't work for them. - return api.nvim_buf_is_loaded(b) - and not vim.list_contains({ 'nofile', 'nowrite' }, vim.bo[b].buftype) - end) - do + for _, b in ipairs(get_writable_bufs(old_fname_full)) do -- Renaming a buffer may conflict with another buffer that happens to have the same name. In -- most cases, this would have been already detected by the file conflict check above, but the -- conflicting buffer may not be associated with a file. For example, 'buftype' can be "nofile" -- or "nowrite", or the buffer can be a normal buffer but has not been written to the file yet. -- Renaming should fail in such cases to avoid losing the contents of the conflicting buffer. - local old_bname = vim.api.nvim_buf_get_name(b) + local old_bname = api.nvim_buf_get_name(b) local new_bname = old_bname:gsub(old_fname_pat, escape_gsub_repl(new_fname)) if vim.fn.bufexists(new_bname) == 1 then local existing_buf = vim.fn.bufnr(new_bname) @@ -702,7 +619,7 @@ end --- Applies a `WorkspaceEdit`. --- ---@param workspace_edit lsp.WorkspaceEdit ----@param offset_encoding string utf-8|utf-16|utf-32 (required) +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' (required) ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit function M.apply_workspace_edit(workspace_edit, offset_encoding) if offset_encoding == nil then @@ -710,16 +627,18 @@ function M.apply_workspace_edit(workspace_edit, offset_encoding) 'apply_workspace_edit must be called with valid offset encoding', vim.log.levels.WARN ) + return end if workspace_edit.documentChanges then for idx, change in ipairs(workspace_edit.documentChanges) do if change.kind == 'rename' then - M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), change.options) + local options = change.options --[[@as vim.lsp.util.rename.Opts]] + M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), options) elseif change.kind == 'create' then create_file(change) elseif change.kind == 'delete' then delete_file(change) - elseif change.kind then + elseif change.kind then --- @diagnostic disable-line:undefined-field error(string.format('Unsupported change: %q', vim.inspect(change))) else M.apply_text_document_edit(change, idx, offset_encoding) @@ -748,7 +667,7 @@ end --- then the corresponding value is returned without further modifications. --- ---@param input lsp.MarkedString|lsp.MarkedString[]|lsp.MarkupContent ----@param contents string[]|nil List of strings to extend with converted lines. Defaults to {}. +---@param contents string[]? List of strings to extend with converted lines. Defaults to {}. ---@return string[] extended with lines of converted markdown. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover function M.convert_input_to_markdown_lines(input, contents) @@ -781,111 +700,117 @@ function M.convert_input_to_markdown_lines(input, contents) return contents end +--- Returns the line/column-based position in `contents` at the given offset. +--- +---@param offset integer +---@param contents string[] +---@return { [1]: integer, [2]: integer }? +local function get_pos_from_offset(offset, contents) + local i = 0 + for l, line in ipairs(contents) do + if offset >= i and offset < i + #line then + return { l - 1, offset - i + 1 } + else + i = i + #line + 1 + end + end +end + --- Converts `textDocument/signatureHelp` response to markdown lines. --- ---@param signature_help lsp.SignatureHelp Response of `textDocument/SignatureHelp` ----@param ft string|nil filetype that will be use as the `lang` for the label markdown code block ----@param triggers table|nil list of trigger characters from the lsp server. used to better determine parameter offsets ----@return string[]|nil table list of lines of converted markdown. ----@return number[]|nil table of active hl +---@param ft string? filetype that will be use as the `lang` for the label markdown code block +---@param triggers string[]? list of trigger characters from the lsp server. used to better determine parameter offsets +---@return string[]? # lines of converted markdown. +---@return Range4? # highlight range for the active parameter ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers) - if not signature_help.signatures then - return - end --The active signature. If omitted or the value lies outside the range of --`signatures` the value defaults to zero or is ignored if `signatures.length == 0`. --Whenever possible implementors should make an active decision about --the active signature and shouldn't rely on a default value. - local contents = {} - local active_hl + local contents = {} --- @type string[] + local active_offset ---@type [integer, integer]? local active_signature = signature_help.activeSignature or 0 -- If the activeSignature is not inside the valid range, then clip it. -- In 3.15 of the protocol, activeSignature was allowed to be negative if active_signature >= #signature_help.signatures or active_signature < 0 then active_signature = 0 end - local signature = signature_help.signatures[active_signature + 1] - if not signature then - return - end + local signature = vim.deepcopy(signature_help.signatures[active_signature + 1]) local label = signature.label if ft then -- wrap inside a code block for proper rendering label = ('```%s\n%s\n```'):format(ft, label) end - list_extend(contents, split(label, '\n', { plain = true, trimempty = true })) - if signature.documentation then + list_extend(contents, vim.split(label, '\n', { plain = true, trimempty = true })) + local doc = signature.documentation + if doc then -- if LSP returns plain string, we treat it as plaintext. This avoids -- special characters like underscore or similar from being interpreted -- as markdown font modifiers - if type(signature.documentation) == 'string' then - signature.documentation = { kind = 'plaintext', value = signature.documentation } + if type(doc) == 'string' then + signature.documentation = { kind = 'plaintext', value = doc } end M.convert_input_to_markdown_lines(signature.documentation, contents) end if signature.parameters and #signature.parameters > 0 then - local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0) - if active_parameter < 0 then - active_parameter = 0 - end + -- First check if the signature has an activeParameter. If it doesn't check if the response + -- had that property instead. Else just default to 0. + local active_parameter = + math.max(signature.activeParameter or signature_help.activeParameter or 0, 0) -- If the activeParameter is > #parameters, then set it to the last -- NOTE: this is not fully according to the spec, but a client-side interpretation - if active_parameter >= #signature.parameters then - active_parameter = #signature.parameters - 1 - end + active_parameter = math.min(active_parameter, #signature.parameters - 1) local parameter = signature.parameters[active_parameter + 1] - if parameter then - --[=[ - --Represents a parameter of a callable-signature. A parameter can - --have a label and a doc-comment. - interface ParameterInformation { - --The label of this parameter information. - -- - --Either a string or an inclusive start and exclusive end offsets within its containing - --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 - --string representation as `Position` and `Range` does. - -- - --*Note*: a label of type string should be a substring of its containing signature label. - --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. - label: string | [number, number]; - --The human-readable doc-comment of this parameter. Will be shown - --in the UI but can be omitted. - documentation?: string | MarkupContent; - } - --]=] - if parameter.label then - if type(parameter.label) == 'table' then - active_hl = parameter.label - else - local offset = 1 - -- try to set the initial offset to the first found trigger character - for _, t in ipairs(triggers or {}) do - local trigger_offset = signature.label:find(t, 1, true) - if trigger_offset and (offset == 1 or trigger_offset < offset) then - offset = trigger_offset - end - end - for p, param in pairs(signature.parameters) do - offset = signature.label:find(param.label, offset, true) - if not offset then - break - end - if p == active_parameter + 1 then - active_hl = { offset - 1, offset + #parameter.label - 1 } - break - end - offset = offset + #param.label + 1 - end + local parameter_label = parameter.label + if type(parameter_label) == 'table' then + active_offset = parameter_label + else + local offset = 1 ---@type integer? + -- try to set the initial offset to the first found trigger character + for _, t in ipairs(triggers or {}) do + local trigger_offset = signature.label:find(t, 1, true) + if trigger_offset and (offset == 1 or trigger_offset < offset) then + offset = trigger_offset end end - if parameter.documentation then - M.convert_input_to_markdown_lines(parameter.documentation, contents) + for p, param in pairs(signature.parameters) do + local plabel = param.label + assert(type(plabel) == 'string', 'Expected label to be a string') + offset = signature.label:find(plabel, offset, true) + if not offset then + break + end + if p == active_parameter + 1 then + active_offset = { offset - 1, offset + #parameter_label - 1 } + break + end + offset = offset + #param.label + 1 end end + if parameter.documentation then + M.convert_input_to_markdown_lines(parameter.documentation, contents) + end end + + local active_hl = nil + if active_offset then + -- Account for the start of the markdown block. + if ft then + active_offset[1] = active_offset[1] + #contents[1] + active_offset[2] = active_offset[2] + #contents[1] + end + + local a_start = get_pos_from_offset(active_offset[1], contents) + local a_end = get_pos_from_offset(active_offset[2], contents) + if a_start and a_end then + active_hl = { a_start[1], a_start[2], a_end[1], a_end[2] } + end + end + return contents, active_hl end @@ -894,32 +819,15 @@ end --- ---@param width integer window width (in character cells) ---@param height integer window height (in character cells) ----@param opts table optional ---- - offset_x (integer) offset to add to `col` ---- - offset_y (integer) offset to add to `row` ---- - border (string or table) override `border` ---- - focusable (string or table) override `focusable` ---- - zindex (string or table) override `zindex`, defaults to 50 ---- - relative ("mouse"|"cursor") defaults to "cursor" ---- - anchor_bias ("auto"|"above"|"below") defaults to "auto" ---- - "auto": place window based on which side of the cursor has more lines ---- - "above": place the window above the cursor unless there are not enough lines ---- to display the full window height. ---- - "below": place the window below the cursor unless there are not enough lines ---- to display the full window height. ----@return table Options +---@param opts? vim.lsp.util.open_floating_preview.Opts +---@return vim.api.keyset.win_config function M.make_floating_popup_options(width, height, opts) - validate({ - opts = { opts, 't', true }, - }) + validate('opts', opts, 'table', true) opts = opts or {} - validate({ - ['opts.offset_x'] = { opts.offset_x, 'n', true }, - ['opts.offset_y'] = { opts.offset_y, 'n', true }, - }) + validate('opts.offset_x', opts.offset_x, 'number', true) + validate('opts.offset_y', opts.offset_y, 'number', true) local anchor = '' - local row, col local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1 or vim.fn.winline() - 1 @@ -927,7 +835,7 @@ function M.make_floating_popup_options(width, height, opts) local anchor_bias = opts.anchor_bias or 'auto' - local anchor_below + local anchor_below --- @type boolean? if anchor_bias == 'below' then anchor_below = (lines_below > lines_above) or (height <= lines_below) @@ -938,7 +846,8 @@ function M.make_floating_popup_options(width, height, opts) anchor_below = lines_below > lines_above end - local border_height = get_border_size(opts).height + local border_height = get_border_size(opts) + local row, col --- @type integer?, integer? if anchor_below then anchor = anchor .. 'N' height = math.max(math.min(lines_below - border_height, height), 0) @@ -960,7 +869,7 @@ function M.make_floating_popup_options(width, height, opts) end local title = (opts.border and opts.title) and opts.title or nil - local title_pos + local title_pos --- @type 'left'|'center'|'right'? if title then title_pos = opts.title_pos or 'center' @@ -982,13 +891,21 @@ function M.make_floating_popup_options(width, height, opts) } end +--- @class vim.lsp.util.show_document.Opts +--- @inlinedoc +--- +--- Jump to existing window if buffer is already open. +--- @field reuse_win? boolean +--- +--- Whether to focus/jump to location if possible. +--- (defaults: true) +--- @field focus? boolean + --- Shows document and optionally jumps to the location. --- ---@param location lsp.Location|lsp.LocationLink ----@param offset_encoding string|nil utf-8|utf-16|utf-32 ----@param opts table|nil options ---- - reuse_win (boolean) Jump to existing window if buffer is already open. ---- - focus (boolean) Whether to focus/jump to location if possible. Defaults to true. +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? +---@param opts? vim.lsp.util.show_document.Opts ---@return boolean `true` if succeeded function M.show_document(location, offset_encoding, opts) -- location may be Location or LocationLink @@ -998,6 +915,7 @@ function M.show_document(location, offset_encoding, opts) end if offset_encoding == nil then vim.notify_once('show_document must be called with valid offset encoding', vim.log.levels.WARN) + return false end local bufnr = vim.uri_to_bufnr(uri) @@ -1041,18 +959,13 @@ end --- Jumps to a location. --- +---@deprecated use `vim.lsp.util.show_document` with `{focus=true}` instead ---@param location lsp.Location|lsp.LocationLink ----@param offset_encoding string|nil utf-8|utf-16|utf-32 ----@param reuse_win boolean|nil Jump to existing window if buffer is already open. +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? +---@param reuse_win boolean? Jump to existing window if buffer is already open. ---@return boolean `true` if the jump succeeded function M.jump_to_location(location, offset_encoding, reuse_win) - if offset_encoding == nil then - vim.notify_once( - 'jump_to_location must be called with valid offset encoding', - vim.log.levels.WARN - ) - end - + vim.deprecate('vim.lsp.util.jump_to_location', nil, '0.12') return M.show_document(location, offset_encoding, { reuse_win = reuse_win, focus = true }) end @@ -1063,9 +976,9 @@ end --- - for LocationLink, targetRange is shown (e.g., body of function definition) --- ---@param location lsp.Location|lsp.LocationLink ----@param opts table ----@return integer|nil buffer id of float window ----@return integer|nil window id of float window +---@param opts? vim.lsp.util.open_floating_preview.Opts +---@return integer? buffer id of float window +---@return integer? window id of float window function M.preview_location(location, opts) -- location may be LocationLink or Location (more useful for the former) local uri = location.targetUri or location.uri @@ -1092,7 +1005,7 @@ end local function find_window_by_var(name, value) for _, win in ipairs(api.nvim_list_wins()) do - if npcall(api.nvim_win_get_var, win, name) == value then + if vim.w[win][name] == value then return win end end @@ -1158,8 +1071,10 @@ local function collapse_blank_lines(contents) end local function get_markdown_fences() - local fences = {} - for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do + local fences = {} --- @type table<string,string> + for _, fence in + pairs(vim.g.markdown_fenced_languages or {} --[[@as string[] ]]) + do local lang, syntax = fence:match('^(.*)=(.*)$') if lang then fences[lang] = syntax @@ -1179,7 +1094,7 @@ end --- ---@param bufnr integer ---@param contents string[] of lines to show in window ----@param opts table with optional fields +---@param opts? table with optional fields --- - height of floating window --- - width of floating window --- - wrap_at character to wrap at for computing height @@ -1188,10 +1103,8 @@ end --- - separator insert separator after code block ---@return table stripped content function M.stylize_markdown(bufnr, contents, opts) - validate({ - contents = { contents, 't' }, - opts = { opts, 't', true }, - }) + validate('contents', contents, 'table') + validate('opts', opts, 'table', true) opts = opts or {} -- table of fence types to {ft, begin, end} @@ -1203,8 +1116,11 @@ function M.stylize_markdown(bufnr, contents, opts) text = { 'text', '<text>', '</text>' }, } - local match_begin = function(line) + --- @param line string + --- @return {type:string,ft:string}? + local function match_begin(line) for type, pattern in pairs(matchers) do + --- @type string? local ret = line:match(string.format('^%%s*%s%%s*$', pattern[2])) if ret then return { @@ -1215,7 +1131,10 @@ function M.stylize_markdown(bufnr, contents, opts) end end - local match_end = function(line, match) + --- @param line string + --- @param match {type:string,ft:string} + --- @return string + local function match_end(line, match) local pattern = matchers[match.type] return line:match(string.format('^%%s*%s%%s*$', pattern[3])) end @@ -1224,76 +1143,80 @@ function M.stylize_markdown(bufnr, contents, opts) contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) local stripped = {} - local highlights = {} + local highlights = {} --- @type {ft:string,start:integer,finish:integer}[] -- keep track of lnums that contain markdown - local markdown_lines = {} - do - local i = 1 - while i <= #contents do - local line = contents[i] - local match = match_begin(line) - if match then - local start = #stripped - i = i + 1 - while i <= #contents do - line = contents[i] - if match_end(line, match) then - i = i + 1 - break - end - table.insert(stripped, line) + local markdown_lines = {} --- @type table<integer,boolean> + + local i = 1 + while i <= #contents do + local line = contents[i] + local match = match_begin(line) + if match then + local start = #stripped + i = i + 1 + while i <= #contents do + line = contents[i] + if match_end(line, match) then i = i + 1 + break end - table.insert(highlights, { - ft = match.ft, - start = start + 1, - finish = #stripped, - }) - -- add a separator, but not on the last line - if opts.separator and i < #contents then - table.insert(stripped, '---') - markdown_lines[#stripped] = true - end - else - -- strip any empty lines or separators prior to this separator in actual markdown - if line:match('^---+$') then - while - markdown_lines[#stripped] - and (stripped[#stripped]:match('^%s*$') or stripped[#stripped]:match('^---+$')) - do - markdown_lines[#stripped] = false - table.remove(stripped, #stripped) - end - end - -- add the line if its not an empty line following a separator - if - not ( - line:match('^%s*$') - and markdown_lines[#stripped] - and stripped[#stripped]:match('^---+$') - ) - then - table.insert(stripped, line) - markdown_lines[#stripped] = true - end + table.insert(stripped, line) i = i + 1 end + table.insert(highlights, { + ft = match.ft, + start = start + 1, + finish = #stripped, + }) + -- add a separator, but not on the last line + if opts.separator and i < #contents then + table.insert(stripped, '---') + markdown_lines[#stripped] = true + end + else + -- strip any empty lines or separators prior to this separator in actual markdown + if line:match('^---+$') then + while + markdown_lines[#stripped] + and (stripped[#stripped]:match('^%s*$') or stripped[#stripped]:match('^---+$')) + do + markdown_lines[#stripped] = false + table.remove(stripped, #stripped) + end + end + -- add the line if its not an empty line following a separator + if + not ( + line:match('^%s*$') + and markdown_lines[#stripped] + and stripped[#stripped]:match('^---+$') + ) + then + table.insert(stripped, line) + markdown_lines[#stripped] = true + end + i = i + 1 end end -- Handle some common html escape sequences - stripped = vim.tbl_map(function(line) - local escapes = { - ['>'] = '>', - ['<'] = '<', - ['"'] = '"', - ['''] = "'", - [' '] = ' ', - [' '] = ' ', - ['&'] = '&', - } - return (string.gsub(line, '&[^ ;]+;', escapes)) - end, stripped) + --- @type string[] + stripped = vim.tbl_map( + --- @param line string + function(line) + local escapes = { + ['>'] = '>', + ['<'] = '<', + ['"'] = '"', + ['''] = "'", + [' '] = ' ', + [' '] = ' ', + ['&'] = '&', + } + return (line:gsub('&[^ ;]+;', escapes)) + end, + stripped + ) -- Compute size of float needed to show (wrapped) lines opts.wrap_at = opts.wrap_at or (vim.wo['wrap'] and api.nvim_win_get_width(0)) @@ -1312,7 +1235,7 @@ function M.stylize_markdown(bufnr, contents, opts) local idx = 1 -- keep track of syntaxes we already included. -- no need to include the same syntax more than once - local langs = {} + local langs = {} --- @type table<string,boolean> local fences = get_markdown_fences() local function apply_syntax_to_region(ft, start, finish) if ft == '' then @@ -1335,6 +1258,7 @@ function M.stylize_markdown(bufnr, contents, opts) if #api.nvim_get_runtime_file(('syntax/%s.vim'):format(ft), true) == 0 then return end + --- @diagnostic disable-next-line:param-type-mismatch pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft)) langs[lang] = true end @@ -1390,10 +1314,8 @@ end ---@return string[] table of lines containing normalized Markdown ---@see https://github.github.com/gfm function M._normalize_markdown(contents, opts) - validate({ - contents = { contents, 't' }, - opts = { opts, 't', true }, - }) + validate('contents', contents, 'table') + validate('opts', opts, 'table', true) opts = opts or {} -- 1. Carriage returns are removed @@ -1412,7 +1334,7 @@ end --- Closes the preview window --- ---@param winnr integer window id of preview window ----@param bufnrs table|nil optional list of ignored buffers +---@param bufnrs table? optional list of ignored buffers local function close_preview_window(winnr, bufnrs) vim.schedule(function() -- exit if we are in one of ignored buffers @@ -1460,20 +1382,13 @@ end ---@private --- Computes size of float needed to show contents (with optional wrapping) --- ----@param contents table of lines to show in window ----@param opts? table with optional fields ---- - height of floating window ---- - width of floating window ---- - wrap_at character to wrap at for computing height ---- - max_width maximal width of floating window ---- - max_height maximal height of floating window +---@param contents string[] of lines to show in window +---@param opts? vim.lsp.util.open_floating_preview.Opts ---@return integer width size of float ---@return integer height size of float function M._make_floating_popup_size(contents, opts) - validate({ - contents = { contents, 't' }, - opts = { opts, 't', true }, - }) + validate('contents', contents, 'table') + validate('opts', opts, 'table', true) opts = opts or {} local width = opts.width @@ -1481,7 +1396,7 @@ function M._make_floating_popup_size(contents, opts) local wrap_at = opts.wrap_at local max_width = opts.max_width local max_height = opts.max_height - local line_widths = {} + local line_widths = {} --- @type table<integer,integer> if not width then width = 0 @@ -1492,17 +1407,15 @@ function M._make_floating_popup_size(contents, opts) end end - local border_width = get_border_size(opts).width + local _, border_width = get_border_size(opts) local screen_width = api.nvim_win_get_width(0) width = math.min(width, screen_width) -- make sure borders are always inside the screen - if width + border_width > screen_width then - width = width - (width + border_width - screen_width) - end + width = math.min(width, screen_width - border_width) - if wrap_at and wrap_at > width then - wrap_at = width + if wrap_at then + wrap_at = math.min(wrap_at, width) end if max_width then @@ -1534,7 +1447,6 @@ function M._make_floating_popup_size(contents, opts) end --- @class vim.lsp.util.open_floating_preview.Opts ---- @inlinedoc --- --- Height of floating window --- @field height? integer @@ -1569,6 +1481,29 @@ end --- window with the same {focus_id} --- (default: `true`) --- @field focus? boolean +--- +--- offset to add to `col` +--- @field offset_x? integer +--- +--- offset to add to `row` +--- @field offset_y? integer +--- @field border? string|(string|[string,string])[] override `border` +--- @field zindex? integer override `zindex`, defaults to 50 +--- @field title? string +--- @field title_pos? 'left'|'center'|'right' +--- +--- (default: `'cursor'`) +--- @field relative? 'mouse'|'cursor' +--- +--- - "auto": place window based on which side of the cursor has more lines +--- - "above": place the window above the cursor unless there are not enough lines +--- to display the full window height. +--- - "below": place the window below the cursor unless there are not enough lines +--- to display the full window height. +--- (default: `'auto'`) +--- @field anchor_bias? 'auto'|'above'|'below' +--- +--- @field _update_win? integer --- Shows contents in a floating window. --- @@ -1580,11 +1515,9 @@ end ---@return integer bufnr of newly created float window ---@return integer winid of newly created float window preview window function M.open_floating_preview(contents, syntax, opts) - validate({ - contents = { contents, 't' }, - syntax = { syntax, 's', true }, - opts = { opts, 't', true }, - }) + validate('contents', contents, 'table') + validate('syntax', syntax, 'string', true) + validate('opts', opts, 'table', true) opts = opts or {} opts.wrap = opts.wrap ~= false -- wrapping by default opts.focus = opts.focus ~= false @@ -1592,43 +1525,49 @@ function M.open_floating_preview(contents, syntax, opts) local bufnr = api.nvim_get_current_buf() - -- check if this popup is focusable and we need to focus - if opts.focus_id and opts.focusable ~= false and opts.focus then - -- Go back to previous window if we are in a focusable one - local current_winnr = api.nvim_get_current_win() - if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then - api.nvim_command('wincmd p') - return bufnr, current_winnr - end - do - local win = find_window_by_var(opts.focus_id, bufnr) - if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then - -- focus and return the existing buf, win - api.nvim_set_current_win(win) - api.nvim_command('stopinsert') - return api.nvim_win_get_buf(win), win + local floating_winnr = opts._update_win + + -- Create/get the buffer + local floating_bufnr --- @type integer + if floating_winnr then + floating_bufnr = api.nvim_win_get_buf(floating_winnr) + else + -- check if this popup is focusable and we need to focus + if opts.focus_id and opts.focusable ~= false and opts.focus then + -- Go back to previous window if we are in a focusable one + local current_winnr = api.nvim_get_current_win() + if vim.w[current_winnr][opts.focus_id] then + api.nvim_command('wincmd p') + return bufnr, current_winnr + end + do + local win = find_window_by_var(opts.focus_id, bufnr) + if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then + -- focus and return the existing buf, win + api.nvim_set_current_win(win) + api.nvim_command('stopinsert') + return api.nvim_win_get_buf(win), win + end end end - end - -- check if another floating preview already exists for this buffer - -- and close it if needed - local existing_float = npcall(api.nvim_buf_get_var, bufnr, 'lsp_floating_preview') - if existing_float and api.nvim_win_is_valid(existing_float) then - api.nvim_win_close(existing_float, true) + -- check if another floating preview already exists for this buffer + -- and close it if needed + local existing_float = vim.b[bufnr].lsp_floating_preview + if existing_float and api.nvim_win_is_valid(existing_float) then + api.nvim_win_close(existing_float, true) + end + floating_bufnr = api.nvim_create_buf(false, true) end - -- Create the buffer - local floating_bufnr = api.nvim_create_buf(false, true) - -- Set up the contents, using treesitter for markdown local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil + if do_stylize then local width = M._make_floating_popup_size(contents, opts) contents = M._normalize_markdown(contents, { width = width }) vim.bo[floating_bufnr].filetype = 'markdown' vim.treesitter.start(floating_bufnr) - api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents) else -- Clean up input: trim empty lines contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) @@ -1636,19 +1575,47 @@ function M.open_floating_preview(contents, syntax, opts) if syntax then vim.bo[floating_bufnr].syntax = syntax end - api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) end - -- Compute size of float needed to show (wrapped) lines - if opts.wrap then - opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0) + vim.bo[floating_bufnr].modifiable = true + api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents) + + if floating_winnr then + api.nvim_win_set_config(floating_winnr, { + border = opts.border, + title = opts.title, + }) else - opts.wrap_at = nil - end - local width, height = M._make_floating_popup_size(contents, opts) + -- Compute size of float needed to show (wrapped) lines + if opts.wrap then + opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0) + else + opts.wrap_at = nil + end - local float_option = M.make_floating_popup_options(width, height, opts) - local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) + -- TODO(lewis6991): These function assume the current window to determine options, + -- therefore it won't work for opts._update_win and the current window if the floating + -- window + local width, height = M._make_floating_popup_size(contents, opts) + local float_option = M.make_floating_popup_options(width, height, opts) + + floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) + + api.nvim_buf_set_keymap( + floating_bufnr, + 'n', + 'q', + '<cmd>bdelete<cr>', + { silent = true, noremap = true, nowait = true } + ) + close_preview_autocmd(opts.close_events, floating_winnr, { floating_bufnr, bufnr }) + + -- save focus_id + if opts.focus_id then + api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr) + end + api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr) + end if do_stylize then vim.wo[floating_winnr].conceallevel = 2 @@ -1656,25 +1623,11 @@ function M.open_floating_preview(contents, syntax, opts) vim.wo[floating_winnr].foldenable = false -- Disable folding. vim.wo[floating_winnr].wrap = opts.wrap -- Soft wrapping. vim.wo[floating_winnr].breakindent = true -- Slightly better list presentation. + vim.wo[floating_winnr].smoothscroll = true -- Scroll by screen-line instead of buffer-line. vim.bo[floating_bufnr].modifiable = false vim.bo[floating_bufnr].bufhidden = 'wipe' - api.nvim_buf_set_keymap( - floating_bufnr, - 'n', - 'q', - '<cmd>bdelete<cr>', - { silent = true, noremap = true, nowait = true } - ) - close_preview_autocmd(opts.close_events, floating_winnr, { floating_bufnr, bufnr }) - - -- save focus_id - if opts.focus_id then - api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr) - end - api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr) - return floating_bufnr, floating_winnr end @@ -1683,9 +1636,8 @@ do --[[ References ]] --- Removes document highlights from a buffer. --- - ---@param bufnr integer|nil Buffer id + ---@param bufnr integer? Buffer id function M.buf_clear_references(bufnr) - validate({ bufnr = { bufnr, { 'n' }, true } }) api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1) end @@ -1693,29 +1645,18 @@ do --[[ References ]] --- ---@param bufnr integer Buffer id ---@param references lsp.DocumentHighlight[] objects to highlight - ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32". + ---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' ---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent function M.buf_highlight_references(bufnr, references, offset_encoding) - validate({ - bufnr = { bufnr, 'n', true }, - offset_encoding = { offset_encoding, 'string', false }, - }) + validate('bufnr', bufnr, 'number', true) + validate('offset_encoding', offset_encoding, 'string', false) for _, reference in ipairs(references) do - local start_line, start_char = - reference['range']['start']['line'], reference['range']['start']['character'] - local end_line, end_char = - reference['range']['end']['line'], reference['range']['end']['character'] + local range = reference.range + local start_line = range.start.line + local end_line = range['end'].line - local start_idx = get_line_byte_from_position( - bufnr, - { line = start_line, character = start_char }, - offset_encoding - ) - local end_idx = get_line_byte_from_position( - bufnr, - { line = start_line, character = end_char }, - offset_encoding - ) + local start_idx = get_line_byte_from_position(bufnr, range.start, offset_encoding) + local end_idx = get_line_byte_from_position(bufnr, range['end'], offset_encoding) local document_highlight_kind = { [protocol.DocumentHighlightKind.Text] = 'LspReferenceText', @@ -1723,13 +1664,13 @@ do --[[ References ]] [protocol.DocumentHighlightKind.Write] = 'LspReferenceWrite', } local kind = reference['kind'] or protocol.DocumentHighlightKind.Text - highlight.range( + vim.hl.range( bufnr, reference_ns, document_highlight_kind[kind], { start_line, start_idx }, { end_line, end_idx }, - { priority = vim.highlight.priorities.user } + { priority = vim.hl.priorities.user } ) end end @@ -1739,16 +1680,6 @@ local position_sort = sort_by_key(function(v) return { v.start.line, v.start.character } end) ----@class vim.lsp.util.locations_to_items.ret ----@inlinedoc ----@field filename string ----@field lnum integer 1-indexed line number ----@field end_lnum integer 1-indexed end line number ----@field col integer 1-indexed column ----@field end_col integer 1-indexed end column ----@field text string ----@field user_data lsp.Location|lsp.LocationLink - --- Returns the items with the byte position calculated correctly and in sorted --- order, for display in quickfix and location lists. --- @@ -1759,9 +1690,9 @@ end) --- |setloclist()|. --- ---@param locations lsp.Location[]|lsp.LocationLink[] ----@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32 ---- default to first client of buffer ----@return vim.lsp.util.locations_to_items.ret[] +---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32' +--- default to first client of buffer +---@return vim.quickfix.entry[] # See |setqflist()| for the format function M.locations_to_items(locations, offset_encoding) if offset_encoding == nil then vim.notify_once( @@ -1771,28 +1702,19 @@ function M.locations_to_items(locations, offset_encoding) offset_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding end - local items = {} + local items = {} --- @type vim.quickfix.entry[] + ---@type table<string, {start: lsp.Position, end: lsp.Position, location: lsp.Location|lsp.LocationLink}[]> - local grouped = setmetatable({}, { - __index = function(t, k) - local v = {} - rawset(t, k, v) - return v - end, - }) + local grouped = {} for _, d in ipairs(locations) do -- locations may be Location or LocationLink local uri = d.uri or d.targetUri local range = d.range or d.targetSelectionRange + grouped[uri] = grouped[uri] or {} table.insert(grouped[uri], { start = range.start, ['end'] = range['end'], location = d }) end - ---@type string[] - local keys = vim.tbl_keys(grouped) - table.sort(keys) - -- TODO(ashkan) I wish we could do this lazily. - for _, uri in ipairs(keys) do - local rows = grouped[uri] + for uri, rows in vim.spairs(grouped) do table.sort(rows, position_sort) local filename = vim.uri_to_fname(uri) @@ -1814,10 +1736,10 @@ function M.locations_to_items(locations, offset_encoding) local end_row = end_pos.line local line = lines[row] or '' local end_line = lines[end_row] or '' - local col = M._str_byteindex_enc(line, pos.character, offset_encoding) - local end_col = M._str_byteindex_enc(end_line, end_pos.character, offset_encoding) + local col = vim.str_byteindex(line, offset_encoding, pos.character, false) + local end_col = vim.str_byteindex(end_line, offset_encoding, end_pos.character, false) - table.insert(items, { + items[#items + 1] = { filename = filename, lnum = row + 1, end_lnum = end_row + 1, @@ -1825,58 +1747,51 @@ function M.locations_to_items(locations, offset_encoding) end_col = end_col + 1, text = line, user_data = temp.location, - }) + } end end return items end --- According to LSP spec, if the client set "symbolKind.valueSet", --- the client must handle it properly even if it receives a value outside the specification. --- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol -function M._get_symbol_kind_name(symbol_kind) - return protocol.SymbolKind[symbol_kind] or 'Unknown' -end - --- Converts symbols to quickfix list items. --- ----@param symbols table DocumentSymbol[] or SymbolInformation[] +---@param symbols lsp.DocumentSymbol[]|lsp.SymbolInformation[] ---@param bufnr? integer +---@return vim.quickfix.entry[] # See |setqflist()| for the format function M.symbols_to_items(symbols, bufnr) - local function _symbols_to_items(_symbols, _items, _bufnr) - for _, symbol in ipairs(_symbols) do - if symbol.location then -- SymbolInformation type - local range = symbol.location.range - local kind = M._get_symbol_kind_name(symbol.kind) - table.insert(_items, { - filename = vim.uri_to_fname(symbol.location.uri), - lnum = range.start.line + 1, - col = range.start.character + 1, - kind = kind, - text = '[' .. kind .. '] ' .. symbol.name, - }) - elseif symbol.selectionRange then -- DocumentSymbole type - local kind = M._get_symbol_kind_name(symbol.kind) - table.insert(_items, { - -- bufnr = _bufnr, - filename = api.nvim_buf_get_name(_bufnr), - lnum = symbol.selectionRange.start.line + 1, - col = symbol.selectionRange.start.character + 1, - kind = kind, - text = '[' .. kind .. '] ' .. symbol.name, - }) - if symbol.children then - for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do - for _, s in ipairs(v) do - table.insert(_items, s) - end - end - end - end + bufnr = bufnr or 0 + local items = {} --- @type vim.quickfix.entry[] + for _, symbol in ipairs(symbols) do + --- @type string?, lsp.Position? + local filename, pos + + if symbol.location then + --- @cast symbol lsp.SymbolInformation + filename = vim.uri_to_fname(symbol.location.uri) + pos = symbol.location.range.start + elseif symbol.selectionRange then + --- @cast symbol lsp.DocumentSymbol + filename = api.nvim_buf_get_name(bufnr) + pos = symbol.selectionRange.start + end + + if filename and pos then + local kind = protocol.SymbolKind[symbol.kind] or 'Unknown' + items[#items + 1] = { + filename = filename, + lnum = pos.line + 1, + col = pos.character + 1, + kind = kind, + text = '[' .. kind .. '] ' .. symbol.name, + } + end + + if symbol.children then + list_extend(items, M.symbols_to_items(symbol.children, bufnr)) end - return _items end - return _symbols_to_items(symbols, {}, bufnr or 0) + + return items end --- Removes empty lines from the beginning and end. @@ -1899,7 +1814,7 @@ function M.trim_empty_lines(lines) break end end - return list_extend({}, lines, start, finish) + return vim.list_slice(lines, start, finish) end --- Accepts markdown lines and tries to reduce them to a filetype if they @@ -1932,8 +1847,8 @@ function M.try_trim_markdown_code_blocks(lines) return 'markdown' end ----@param window integer|nil: window handle or 0 for current, defaults to current ----@param offset_encoding? string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` +---@param window integer?: window handle or 0 for current, defaults to current +---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window` local function make_position_param(window, offset_encoding) window = window or 0 local buf = api.nvim_win_get_buf(window) @@ -1945,15 +1860,15 @@ local function make_position_param(window, offset_encoding) return { line = 0, character = 0 } end - col = M._str_utfindex_enc(line, col, offset_encoding) + col = vim.str_utfindex(line, offset_encoding, col, false) return { line = row, character = col } end --- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. --- ----@param window integer|nil: window handle or 0 for current, defaults to current ----@param offset_encoding string|nil utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` +---@param window integer?: window handle or 0 for current, defaults to current +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window` ---@return lsp.TextDocumentPositionParams ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams function M.make_position_params(window, offset_encoding) @@ -1970,11 +1885,9 @@ end ---@param bufnr integer buffer handle or 0 for current, defaults to current ---@return string encoding first client if there is one, nil otherwise function M._get_offset_encoding(bufnr) - validate({ - bufnr = { bufnr, 'n', true }, - }) + validate('bufnr', bufnr, 'number', true) - local offset_encoding + local offset_encoding --- @type 'utf-8'|'utf-16'|'utf-32'? for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do if client.offset_encoding == nil then @@ -2005,8 +1918,8 @@ end --- `textDocument/codeAction`, `textDocument/colorPresentation`, --- `textDocument/rangeFormatting`. --- ----@param window integer|nil: window handle or 0 for current, defaults to current ----@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of buffer of `window` +---@param window integer? window handle or 0 for current, defaults to current +---@param offset_encoding "utf-8"|"utf-16"|"utf-32"? defaults to `offset_encoding` of first client of buffer of `window` ---@return table { textDocument = { uri = `current_file_uri` }, range = { start = ---`current_position`, end = `current_position` } } function M.make_range_params(window, offset_encoding) @@ -2022,33 +1935,33 @@ end --- Using the given range in the current buffer, creates an object that --- is similar to |vim.lsp.util.make_range_params()|. --- ----@param start_pos integer[]|nil {row,col} mark-indexed position. +---@param start_pos [integer,integer]? {row,col} mark-indexed position. --- Defaults to the start of the last visual selection. ----@param end_pos integer[]|nil {row,col} mark-indexed position. +---@param end_pos [integer,integer]? {row,col} mark-indexed position. --- Defaults to the end of the last visual selection. ----@param bufnr integer|nil buffer handle or 0 for current, defaults to current ----@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of `bufnr` +---@param bufnr integer? buffer handle or 0 for current, defaults to current +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of `bufnr` ---@return table { textDocument = { uri = `current_file_uri` }, range = { start = ---`start_position`, end = `end_position` } } function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) - validate({ - start_pos = { start_pos, 't', true }, - end_pos = { end_pos, 't', true }, - offset_encoding = { offset_encoding, 's', true }, - }) + validate('start_pos', start_pos, 'table', true) + validate('end_pos', end_pos, 'table', true) + validate('offset_encoding', offset_encoding, 'string', true) bufnr = bufnr or api.nvim_get_current_buf() offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) - local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<')) - local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>')) + --- @type [integer, integer] + local A = { unpack(start_pos or api.nvim_buf_get_mark(bufnr, '<')) } + --- @type [integer, integer] + local B = { unpack(end_pos or api.nvim_buf_get_mark(bufnr, '>')) } -- convert to 0-index A[1] = A[1] - 1 B[1] = B[1] - 1 -- account for offset_encoding. if A[2] > 0 then - A = { A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding) } + A[2] = M.character_offset(bufnr, A[1], A[2], offset_encoding) end if B[2] > 0 then - B = { B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding) } + B[2] = M.character_offset(bufnr, B[1], B[2], offset_encoding) end -- we need to offset the end character position otherwise we loose the last -- character of the selection, as LSP end position is exclusive @@ -2067,7 +1980,7 @@ end --- Creates a `TextDocumentIdentifier` object for the current buffer. --- ----@param bufnr integer|nil: Buffer handle, defaults to current +---@param bufnr integer?: Buffer handle, defaults to current ---@return lsp.TextDocumentIdentifier ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier function M.make_text_document_params(bufnr) @@ -2085,10 +1998,10 @@ end --- Returns indentation size. --- ---@see 'shiftwidth' ----@param bufnr integer|nil: Buffer handle, defaults to current +---@param bufnr integer?: Buffer handle, defaults to current ---@return integer indentation size function M.get_effective_tabstop(bufnr) - validate({ bufnr = { bufnr, 'n', true } }) + validate('bufnr', bufnr, 'number', true) local bo = bufnr and vim.bo[bufnr] or vim.bo local sw = bo.shiftwidth return (sw == 0 and bo.tabstop) or sw @@ -2096,11 +2009,11 @@ end --- Creates a `DocumentFormattingParams` object for the current buffer and cursor position. --- ----@param options lsp.FormattingOptions|nil with valid `FormattingOptions` entries +---@param options lsp.FormattingOptions? with valid `FormattingOptions` entries ---@return lsp.DocumentFormattingParams object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting function M.make_formatting_params(options) - validate({ options = { options, 't', true } }) + validate('options', options, 'table', true) options = vim.tbl_extend('keep', options or {}, { tabSize = M.get_effective_tabstop(), insertSpaces = vim.bo.expandtab, @@ -2116,7 +2029,8 @@ end ---@param buf integer buffer number (0 for current) ---@param row integer 0-indexed line ---@param col integer 0-indexed byte offset in line ----@param offset_encoding string utf-8|utf-16|utf-32 defaults to `offset_encoding` of first client of `buf` +---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32' +--- defaults to `offset_encoding` of first client of `buf` ---@return integer `offset_encoding` index of the character in line {row} column {col} in buffer {buf} function M.character_offset(buf, row, col, offset_encoding) local line = get_line(buf, row) @@ -2127,7 +2041,7 @@ function M.character_offset(buf, row, col, offset_encoding) ) offset_encoding = vim.lsp.get_clients({ bufnr = buf })[1].offset_encoding end - return M._str_utfindex_enc(line, col, offset_encoding) + return vim.str_utfindex(line, offset_encoding, col, false) end --- Helper function to return nested values in language server settings @@ -2139,6 +2053,7 @@ end function M.lookup_section(settings, section) vim.deprecate('vim.lsp.util.lookup_section()', 'vim.tbl_get() with `vim.split`', '0.12') for part in vim.gsplit(section, '.', { plain = true }) do + --- @diagnostic disable-next-line:no-unknown settings = settings[part] if settings == nil then return vim.NIL @@ -2153,7 +2068,7 @@ end ---@param bufnr integer ---@param start_line integer ---@param end_line integer ----@param offset_encoding lsp.PositionEncodingKind +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' ---@return lsp.Range local function make_line_range_params(bufnr, start_line, end_line, offset_encoding) local last_line = api.nvim_buf_line_count(bufnr) - 1 @@ -2161,7 +2076,7 @@ local function make_line_range_params(bufnr, start_line, end_line, offset_encodi ---@type lsp.Position local end_pos - if end_line == last_line and not vim.api.nvim_get_option_value('endofline', { buf = bufnr }) then + if end_line == last_line and not vim.bo[bufnr].endofline then end_pos = { line = end_line, character = M.character_offset(bufnr, end_line, #get_line(bufnr, end_line), offset_encoding), @@ -2201,9 +2116,7 @@ function M._refresh(method, opts) local textDocument = M.make_text_document_params(bufnr) - local only_visible = opts.only_visible or false - - if only_visible then + if opts.only_visible then for _, window in ipairs(api.nvim_list_wins()) do if api.nvim_win_get_buf(window) == bufnr then local first = vim.fn.line('w0', window) diff --git a/runtime/lua/vim/provider/health.lua b/runtime/lua/vim/provider/health.lua index 47c2080e3c..5ecb00f49b 100644 --- a/runtime/lua/vim/provider/health.lua +++ b/runtime/lua/vim/provider/health.lua @@ -449,7 +449,7 @@ 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') + local pypi_response = download('https://pypi.org/pypi/pynvim/json') if pypi_response ~= '' then local pcall_ok, output = pcall(vim.fn.json_decode, pypi_response) if not pcall_ok then diff --git a/runtime/lua/vim/secure.lua b/runtime/lua/vim/secure.lua index 266725cce2..7b1d071270 100644 --- a/runtime/lua/vim/secure.lua +++ b/runtime/lua/vim/secure.lua @@ -26,7 +26,7 @@ end --- ---@param trust table<string, string> Trust table to write local function write_trust(trust) - vim.validate({ trust = { trust, 't' } }) + vim.validate('trust', trust, 'table') local f = assert(io.open(vim.fn.stdpath('state') .. '/trust', 'w')) local t = {} ---@type string[] @@ -49,7 +49,7 @@ end ---@return (string|nil) The contents of the given file if it exists and is --- trusted, or nil otherwise. function M.read(path) - vim.validate({ path = { path, 's' } }) + vim.validate('path', path, 'string') local fullpath = vim.uv.fs_realpath(vim.fs.normalize(path)) if not fullpath then return nil @@ -132,17 +132,11 @@ end ---@return boolean success true if operation was successful ---@return string msg full path if operation was successful, else error message function M.trust(opts) - vim.validate({ - path = { opts.path, 's', true }, - bufnr = { opts.bufnr, 'n', true }, - action = { - opts.action, - function(m) - return m == 'allow' or m == 'deny' or m == 'remove' - end, - [["allow" or "deny" or "remove"]], - }, - }) + vim.validate('path', opts.path, 'string', true) + vim.validate('bufnr', opts.bufnr, 'number', true) + vim.validate('action', opts.action, function(m) + return m == 'allow' or m == 'deny' or m == 'remove' + end, [["allow" or "deny" or "remove"]]) ---@cast opts vim.trust.opts local path = opts.path diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 4d06cdd77d..4f2373b182 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -109,7 +109,9 @@ function vim.gsplit(s, sep, opts) if type(opts) == 'boolean' then plain = opts -- For backwards compatibility. else - vim.validate({ s = { s, 's' }, sep = { sep, 's' }, opts = { opts, 't', true } }) + vim.validate('s', s, 'string') + vim.validate('sep', sep, 'string') + vim.validate('opts', opts, 'table', true) opts = opts or {} plain, trimempty = opts.plain, opts.trimempty end @@ -249,7 +251,8 @@ end ---@param t table<any, T> Table ---@return table : Table of transformed values function vim.tbl_map(func, t) - vim.validate({ func = { func, 'c' }, t = { t, 't' } }) + vim.validate('func', func, 'callable') + vim.validate('t', t, 'table') --- @cast t table<any,any> local rettab = {} --- @type table<any,any> @@ -266,7 +269,8 @@ end ---@param t table<any, T> (table) Table ---@return T[] : Table of filtered values function vim.tbl_filter(func, t) - vim.validate({ func = { func, 'c' }, t = { t, 't' } }) + vim.validate('func', func, 'callable') + vim.validate('t', t, 'table') --- @cast t table<any,any> local rettab = {} --- @type table<any,any> @@ -303,12 +307,13 @@ end ---@param opts? vim.tbl_contains.Opts Keyword arguments |kwargs|: ---@return boolean `true` if `t` contains `value` function vim.tbl_contains(t, value, opts) - vim.validate({ t = { t, 't' }, opts = { opts, 't', true } }) + vim.validate('t', t, 'table') + vim.validate('opts', opts, 'table', true) --- @cast t table<any,any> local pred --- @type fun(v: any): boolean? if opts and opts.predicate then - vim.validate({ value = { value, 'c' } }) + vim.validate('value', value, 'callable') pred = value else pred = function(v) @@ -550,12 +555,10 @@ end ---@param finish integer? Final index on src. Defaults to `#src` ---@return T dst function vim.list_extend(dst, src, start, finish) - vim.validate({ - dst = { dst, 't' }, - src = { src, 't' }, - start = { start, 'n', true }, - finish = { finish, 'n', true }, - }) + vim.validate('dst', dst, 'table') + vim.validate('src', src, 'table') + vim.validate('start', start, 'number', true) + vim.validate('finish', finish, 'number', true) for i = start or 1, finish or #src do table.insert(dst, src[i]) end @@ -778,231 +781,227 @@ function vim.endswith(s, suffix) end do - --- @alias vim.validate.Type - --- | 't' | 'table' - --- | 's' | 'string' - --- | 'n' | 'number' - --- | 'f' | 'function' - --- | 'c' | 'callable' - --- | 'nil' - --- | 'thread' - --- | 'userdata - - local type_names = { - ['table'] = 'table', - t = 'table', - ['string'] = 'string', - s = 'string', - ['number'] = 'number', - n = 'number', - ['boolean'] = 'boolean', + --- @alias vim.validate.Validator + --- | type + --- | 'callable' + --- | (type|'callable')[] + --- | fun(v:any):boolean, string? + + local type_aliases = { b = 'boolean', - ['function'] = 'function', - f = 'function', - ['callable'] = 'callable', c = 'callable', - ['nil'] = 'nil', - ['thread'] = 'thread', - ['userdata'] = 'userdata', + f = 'function', + n = 'number', + s = 'string', + t = 'table', } --- @nodoc - --- @class vim.validate.Spec [any, string|string[], boolean] + --- @class vim.validate.Spec --- @field [1] any Argument value - --- @field [2] string|string[]|fun(v:any):boolean, string? Type name, or callable - --- @field [3]? boolean + --- @field [2] vim.validate.Validator Argument validator + --- @field [3]? boolean|string Optional flag or error message - local function _is_type(val, t) + local function is_type(val, t) return type(val) == t or (t == 'callable' and vim.is_callable(val)) end --- @param param_name string - --- @param spec vim.validate.Spec + --- @param val any + --- @param validator vim.validate.Validator + --- @param message? string + --- @param allow_alias? boolean Allow short type names: 'n', 's', 't', 'b', 'f', 'c' --- @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 function is_valid(param_name, val, validator, message, allow_alias) + if type(validator) == 'string' then + local expected = allow_alias and type_aliases[validator] or validator - 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 not expected then + return string.format('invalid type name: %s', validator) + end - if vim.is_callable(types) then + if not is_type(val, expected) then + return string.format('%s: expected %s, got %s', param_name, expected, type(val)) + end + elseif vim.is_callable(validator) then -- Check user-provided validation function - local valid, optional_message = types(val) + local valid, opt_msg = validator(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) + local err_msg = + string.format('%s: expected %s, got %s', param_name, message or '?', tostring(val)) + + if opt_msg then + err_msg = string.format('%s. Info: %s', err_msg, opt_msg) end - return error_message + return err_msg 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 + elseif type(validator) == 'table' then + for _, t in ipairs(validator) do + local expected = allow_alias and type_aliases[t] or t + if not expected 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 + if is_type(val, expected) then + return -- success end end - if not success then - return string.format( - '%s: expected %s, got %s', - param_name, - table.concat(types, '|'), - type(val) - ) + + -- Normalize validator types for error message + if allow_alias then + for i, t in ipairs(validator) do + validator[i] = type_aliases[t] or t + end end + + return string.format( + '%s: expected %s, got %s', + param_name, + table.concat(validator, '|'), + type(val) + ) else - return string.format('invalid type name: %s', tostring(types)) + return string.format('invalid validator: %s', tostring(validator)) end end - --- @param opt table<vim.validate.Type,vim.validate.Spec> - --- @return boolean, string? - local function is_valid(opt) - if type(opt) ~= 'table' then - return false, string.format('opt: expected table, got %s', type(opt)) - end - + --- @param opt table<type|'callable',vim.validate.Spec> + --- @return string? + local function validate_spec(opt) local report --- @type table<string,string>? for param_name, spec in pairs(opt) do - local msg = is_param_valid(param_name, spec) - if msg then + local err_msg --- @type string? + if type(spec) ~= 'table' then + err_msg = string.format('opt[%s]: expected table, got %s', param_name, type(spec)) + else + local value, validator = spec[1], spec[2] + local msg = type(spec[3]) == 'string' and spec[3] or nil --[[@as string?]] + local optional = spec[3] == true + if not (optional and value == nil) then + err_msg = is_valid(param_name, value, validator, msg, true) + end + end + + if err_msg then report = report or {} - report[param_name] = msg + report[param_name] = err_msg end end if report then for _, msg in vim.spairs(report) do -- luacheck: ignore - return false, msg + return msg end end - - return true end --- Validate function arguments. --- --- This function has two valid forms: --- - --- 1. vim.validate(name: str, value: any, type: string, optional?: bool) - --- 2. vim.validate(spec: table) + --- 1. `vim.validate(name, value, validator[, optional][, message])` --- - --- Form 1 validates that argument {name} with value {value} has the type - --- {type}. {type} must be a value returned by |lua-type()|. If {optional} is - --- true, then {value} may be null. This form is significantly faster and - --- should be preferred for simple cases. + --- Validates that argument {name} with value {value} satisfies + --- {validator}. If {optional} is given and is `true`, then {value} may be + --- `nil`. If {message} is given, then it is used as the expected type in the + --- error message. --- - --- Example: + --- Example: --- - --- ```lua - --- function vim.startswith(s, prefix) - --- vim.validate('s', s, 'string') - --- vim.validate('prefix', prefix, 'string') - --- ... - --- end - --- ``` + --- ```lua + --- function vim.startswith(s, prefix) + --- vim.validate('s', s, 'string') + --- vim.validate('prefix', prefix, 'string') + --- ... + --- end + --- ``` --- - --- Form 2 validates a parameter specification (types and values). Specs are - --- evaluated in alphanumeric order, until the first failure. + --- 2. `vim.validate(spec)` (deprecated) + --- where `spec` is of type + --- `table<string,[value:any, validator: vim.validate.Validator, optional_or_msg? : boolean|string]>)` --- - --- Usage example: + --- Validates a argument specification. + --- Specs are evaluated in alphanumeric order, until the first failure. --- - --- ```lua - --- function user.new(name, age, hobbies) - --- vim.validate{ - --- name={name, 'string'}, - --- age={age, 'number'}, - --- hobbies={hobbies, 'table'}, - --- } - --- ... - --- end - --- ``` + --- Example: + --- + --- ```lua + --- function user.new(name, age, hobbies) + --- vim.validate{ + --- name={name, 'string'}, + --- age={age, 'number'}, + --- hobbies={hobbies, 'table'}, + --- } + --- ... + --- end + --- ``` --- --- Examples with explicit argument values (can be run directly): --- --- ```lua - --- vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}} + --- vim.validate('arg1', {'foo'}, 'table') + --- --> NOP (success) + --- vim.validate('arg2', 'foo', 'string') --- --> NOP (success) --- - --- vim.validate{arg1={1, 'table'}} + --- vim.validate('arg1', 1, 'table') --- --> error('arg1: expected table, got number') --- - --- vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}} + --- vim.validate('arg1', 3, function(a) return (a % 2) == 0 end, 'even number') --- --> error('arg1: expected even number, got 3') --- ``` --- --- If multiple types are valid they can be given as a list. --- --- ```lua - --- vim.validate{arg1={{'foo'}, {'table', 'string'}}, arg2={'foo', {'table', 'string'}}} + --- vim.validate('arg1', {'foo'}, {'table', 'string'}) + --- vim.validate('arg2', 'foo', {'table', 'string'}) --- -- NOP (success) --- - --- vim.validate{arg1={1, {'string', 'table'}}} + --- vim.validate('arg1', 1, {'string', 'table'}) --- -- error('arg1: expected string|table, got number') --- ``` --- - ---@param opt table<vim.validate.Type,vim.validate.Spec> (table) Names of parameters to validate. Each key is a parameter - --- name; each value is a tuple in one of these forms: - --- 1. (arg_value, type_name, optional) - --- - arg_value: argument value - --- - type_name: string|table type name, one of: ("table", "t", "string", - --- "s", "number", "n", "boolean", "b", "function", "f", "nil", - --- "thread", "userdata") or list of them. - --- - optional: (optional) boolean, if true, `nil` is valid - --- 2. (arg_value, fn, msg) - --- - arg_value: argument value - --- - fn: any function accepting one argument, returns true if and - --- only if the argument is valid. Can optionally return an additional - --- informative error message as the second returned value. - --- - msg: (optional) error string if validation fails - --- @overload fun(name: string, val: any, expected: string, optional?: boolean) - function vim.validate(opt, ...) - local ok = false - local err_msg ---@type string? - local narg = select('#', ...) - if narg == 0 then - ok, err_msg = is_valid(opt) - elseif narg >= 2 then - -- Overloaded signature for fast/simple cases - local name = opt --[[@as string]] - local v, expected, optional = ... ---@type string, string, boolean? - local actual = type(v) - - ok = (actual == expected) or (v == nil and optional == true) + --- @note `validator` set to a value returned by |lua-type()| provides the + --- best performance. + --- + --- @param name string Argument name + --- @param value string Argument value + --- @param validator vim.validate.Validator + --- - (`string|string[]`): Any value that can be returned from |lua-type()| in addition to + --- `'callable'`: `'boolean'`, `'callable'`, `'function'`, `'nil'`, `'number'`, `'string'`, `'table'`, + --- `'thread'`, `'userdata'`. + --- - (`fun(val:any): boolean, string?`) A function that returns a boolean and an optional + --- string message. + --- @param optional? boolean Argument is optional (may be omitted) + --- @param message? string message when validation fails + --- @overload fun(name: string, val: any, validator: vim.validate.Validator, message: string) + --- @overload fun(spec: table<string,[any, vim.validate.Validator, boolean|string]>) + function vim.validate(name, value, validator, optional, message) + local err_msg --- @type string? + if validator then -- Form 1 + -- Check validator as a string first to optimize the common case. + local ok = (type(value) == validator) or (value == nil and optional == true) if not ok then - err_msg = ('%s: expected %s, got %s%s'):format( - name, - expected, - actual, - v and (' (%s)'):format(v) or '' - ) + local msg = type(optional) == 'string' and optional or message --[[@as string?]] + -- Check more complicated validators + err_msg = is_valid(name, value, validator, msg, false) end + elseif type(name) == 'table' then -- Form 2 + vim.deprecate('vim.validate', 'vim.validate(name, value, validator, optional_or_msg)', '1.0') + err_msg = validate_spec(name) else error('invalid arguments') end - if not ok then + if err_msg then error(err_msg, 2) end end end + --- Returns true if object `f` can be called as a function. --- ---@param f any Any object @@ -1143,7 +1142,7 @@ end --- @param mod T --- @return T function vim._defer_require(root, mod) - return setmetatable({}, { + return setmetatable({ _submodules = mod }, { ---@param t table<string, any> ---@param k string __index = function(t, k) @@ -1157,6 +1156,34 @@ function vim._defer_require(root, mod) }) end +--- @private +--- Creates a module alias/shim that lazy-loads a target module. +--- +--- Unlike `vim.defaulttable()` this also: +--- - implements __call +--- - calls vim.deprecate() +--- +--- @param old_name string Name of the deprecated module, which will be shimmed. +--- @param new_name string Name of the new module, which will be loaded by require(). +function vim._defer_deprecated_module(old_name, new_name) + return setmetatable({}, { + ---@param _ table<string, any> + ---@param k string + __index = function(_, k) + vim.deprecate(old_name, new_name, '2.0.0', nil, false) + --- @diagnostic disable-next-line:no-unknown + local target = require(new_name) + return target[k] + end, + __call = function(self) + vim.deprecate(old_name, new_name, '2.0.0', nil, false) + --- @diagnostic disable-next-line:no-unknown + local target = require(new_name) + return target(self) + end, + }) +end + --- @nodoc --- @class vim.context.mods --- @field bo? table<string, any> @@ -1193,11 +1220,14 @@ local state_restore_order = { 'bo', 'wo', 'go', 'env' } --- @param context vim.context.mods --- @return vim.context.state local get_context_state = function(context) + --- @type vim.context.state local res = { bo = {}, env = {}, go = {}, wo = {} } -- Use specific order from possibly most to least intrusive for _, scope in ipairs(scope_order) do - for name, _ in pairs(context[scope] or {}) do + for name, _ in + pairs(context[scope] or {} --[[@as table<string,any>]]) + do local sc = scope == 'o' and scope_map[vim.api.nvim_get_option_info2(name, {}).scope] or scope -- Do not override already set state and fall back to `vim.NIL` for @@ -1291,7 +1321,10 @@ function vim._with(context, f) -- Apply some parts of the context in specific order -- NOTE: triggers `OptionSet` event for _, scope in ipairs(scope_order) do - for name, context_value in pairs(context[scope] or {}) do + for name, context_value in + pairs(context[scope] or {} --[[@as table<string,any>]]) + do + --- @diagnostic disable-next-line:no-unknown vim[scope][name] = context_value end end @@ -1302,7 +1335,10 @@ function vim._with(context, f) -- Restore relevant cached values in specific order, global scope last -- NOTE: triggers `OptionSet` event for _, scope in ipairs(state_restore_order) do - for name, cached_value in pairs(state[scope]) do + for name, cached_value in + pairs(state[scope] --[[@as table<string,any>]]) + do + --- @diagnostic disable-next-line:no-unknown vim[scope][name] = cached_value end end diff --git a/runtime/lua/vim/termcap.lua b/runtime/lua/vim/termcap.lua index 1da2e71839..4aa41bba9b 100644 --- a/runtime/lua/vim/termcap.lua +++ b/runtime/lua/vim/termcap.lua @@ -17,10 +17,8 @@ local M = {} --- otherwise. {seq} is the control sequence for the capability if found, or nil for --- boolean capabilities. function M.query(caps, cb) - vim.validate({ - caps = { caps, { 'string', 'table' } }, - cb = { cb, 'f' }, - }) + vim.validate('caps', caps, { 'string', 'table' }) + vim.validate('cb', cb, 'function') if type(caps) ~= 'table' then caps = { caps } @@ -40,7 +38,7 @@ function M.query(caps, cb) local k, rest = resp:match('^\027P1%+r(%x+)(.*)$') if k and rest then local cap = vim.text.hexdecode(k) - if not pending[cap] then + if not cap or not pending[cap] then -- Received a response for a capability we didn't request. This can happen if there are -- multiple concurrent XTGETTCAP requests return diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index ed7d31e1f7..dca89f413c 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -133,10 +133,8 @@ end --- ---@return vim.treesitter.LanguageTree object to use for parsing function M.get_string_parser(str, lang, opts) - vim.validate({ - str = { str, 'string' }, - lang = { lang, 'string' }, - }) + vim.validate('str', str, 'string') + vim.validate('lang', lang, 'string') return LanguageTree.new(str, lang, opts) end @@ -152,8 +150,7 @@ function M.is_ancestor(dest, source) return false end - -- child_containing_descendant returns nil if dest is a direct parent - return source:parent() == dest or dest:child_containing_descendant(source) ~= nil + return dest:child_with_descendant(source) ~= nil end --- Returns the node's range or an unpacked range table @@ -168,7 +165,7 @@ function M.get_node_range(node_or_range) if type(node_or_range) == 'table' then return unpack(node_or_range) else - return node_or_range:range() + return node_or_range:range(false) end end @@ -244,11 +241,9 @@ end --- ---@return boolean True if the {node} contains the {range} function M.node_contains(node, range) - vim.validate({ - -- allow a table so nodes can be mocked - node = { node, { 'userdata', 'table' } }, - range = { range, M._range.validate, 'integer list with 4 or 6 elements' }, - }) + -- allow a table so nodes can be mocked + vim.validate('node', node, { 'userdata', 'table' }) + vim.validate('range', range, M._range.validate, 'integer list with 4 or 6 elements') return M._range.contains({ node:range() }, range) end diff --git a/runtime/lua/vim/treesitter/_meta/tsnode.lua b/runtime/lua/vim/treesitter/_meta/tsnode.lua index acc9f8d24e..d982b6a505 100644 --- a/runtime/lua/vim/treesitter/_meta/tsnode.lua +++ b/runtime/lua/vim/treesitter/_meta/tsnode.lua @@ -15,7 +15,7 @@ error('Cannot require a meta file') local TSNode = {} -- luacheck: no unused --- Get the node's immediate parent. ---- Prefer |TSNode:child_containing_descendant()| +--- Prefer |TSNode:child_with_descendant()| --- for iterating over the node's ancestors. --- @return TSNode? function TSNode:parent() end @@ -71,8 +71,24 @@ function TSNode:named_child(index) end --- Get the node's child that contains {descendant}. --- @param descendant TSNode --- @return TSNode? +--- @deprecated function TSNode:child_containing_descendant(descendant) end +--- Get the node's child that contains {descendant} (includes {descendant}). +--- +--- For example, with the following node hierarchy: +--- +--- ``` +--- a -> b -> c +--- +--- a:child_with_descendant(c) == b +--- a:child_with_descendant(b) == b +--- a:child_with_descendant(a) == nil +--- ``` +--- @param descendant TSNode +--- @return TSNode? +function TSNode:child_with_descendant(descendant) end + --- Get the node's start position. Return three values: the row, column and --- total byte count (all zero-based). --- @return integer, integer, integer diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index c5e4b86e1e..a825505378 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -240,8 +240,12 @@ function M.omnifunc(findstart, base) table.insert(items, text) end end - for _, s in pairs(parser_info.symbols) do - local text = s[2] and s[1] or string.format('%q', s[1]):gsub('\n', 'n') ---@type string + for text, named in + pairs(parser_info.symbols --[[@as table<string, boolean>]]) + do + if not named then + text = string.format('%q', text:sub(2, -2)):gsub('\n', 'n') ---@type string + end if text:find(base, 1, true) then table.insert(items, text) end diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 90c3720b80..26817cdba5 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -330,9 +330,7 @@ end --- --- @param opts vim.treesitter.dev.inspect_tree.Opts? function M.inspect_tree(opts) - vim.validate({ - opts = { opts, 't', true }, - }) + vim.validate('opts', opts, 'table', true) opts = opts or {} @@ -529,15 +527,22 @@ function M.inspect_tree(opts) end, }) - api.nvim_create_autocmd('BufHidden', { + api.nvim_create_autocmd({ 'BufHidden', 'BufUnload', 'QuitPre' }, { group = group, buffer = buf, - once = true, callback = function() + -- don't close inpector window if source buffer + -- has more than one open window + if #vim.fn.win_findbuf(buf) > 1 then + return + end + -- close all tree windows for _, window in pairs(vim.fn.win_findbuf(b)) do close_win(window) end + + return true end, }) end @@ -667,10 +672,10 @@ function M.edit_query(lang) api.nvim_buf_clear_namespace(query_buf, edit_ns, 0, -1) end, }) - api.nvim_create_autocmd('BufHidden', { + api.nvim_create_autocmd({ 'BufHidden', 'BufUnload' }, { group = group, buffer = buf, - desc = 'Close the editor window when the source buffer is hidden', + desc = 'Close the editor window when the source buffer is hidden or unloaded', once = true, callback = function() close_win(query_win) diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua index 637f9ea543..53b64d1dec 100644 --- a/runtime/lua/vim/treesitter/health.lua +++ b/runtime/lua/vim/treesitter/health.lua @@ -4,10 +4,21 @@ local health = vim.health --- Performs a healthcheck for treesitter integration function M.check() - local parsers = vim.api.nvim_get_runtime_file('parser/*', true) + health.start('Treesitter features') + + health.info( + string.format( + 'Treesitter ABI support: min %d, max %d', + vim.treesitter.minimum_language_version, + ts.language_version + ) + ) - health.info(string.format('Nvim runtime ABI version: %d', ts.language_version)) + local can_wasm = vim._ts_add_language_from_wasm ~= nil + health.info(string.format('WASM parser support: %s', tostring(can_wasm))) + health.start('Treesitter parsers') + local parsers = vim.api.nvim_get_runtime_file('parser/*', true) for _, parser in pairs(parsers) do local parsername = vim.fn.fnamemodify(parser, ':t:r') local is_loadable, err_or_nil = pcall(ts.language.add, parsername) @@ -28,9 +39,6 @@ function M.check() ) end end - - local can_wasm = vim._ts_add_language_from_wasm ~= nil - health.info(string.format('Can load WASM parsers: %s', tostring(can_wasm))) end return M diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index a94c408f4e..8ce8652f7d 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -93,9 +93,6 @@ function TSHighlighter.new(tree, opts) opts = opts or {} ---@type { queries: table<string,string> } self.tree = tree tree:register_cbs({ - on_bytes = function(...) - self:on_bytes(...) - end, on_detach = function() self:on_detach() end, @@ -215,13 +212,6 @@ function TSHighlighter:for_each_highlight_state(fn) end ---@package ----@param start_row integer ----@param new_end integer -function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end) - api.nvim__redraw({ buf = self.bufnr, range = { start_row, start_row + new_end + 1 } }) -end - ----@package function TSHighlighter:on_detach() self:destroy() end @@ -230,7 +220,7 @@ end ---@param changes Range6[] function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes) do - api.nvim__redraw({ buf = self.bufnr, range = { ch[1], ch[4] + 1 } }) + api.nvim__redraw({ buf = self.bufnr, range = { ch[1], ch[4] + 1 }, flush = false }) end end @@ -328,7 +318,7 @@ local function on_line_impl(self, buf, line, is_spell_nav) -- 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 + or vim.hl.priorities.treesitter ) + spell_pri_offset -- The "conceal" attribute can be set at the pattern level or on a particular capture diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 9f7807e036..446051dfd7 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -108,11 +108,9 @@ function M.add(lang, opts) local path = opts.path local symbol_name = opts.symbol_name - vim.validate({ - lang = { lang, 'string' }, - path = { path, 'string', true }, - symbol_name = { symbol_name, 'string', true }, - }) + vim.validate('lang', lang, 'string') + vim.validate('path', path, 'string', true) + vim.validate('symbol_name', symbol_name, 'string', true) -- parser names are assumed to be lowercase (consistent behavior on case-insensitive file systems) lang = lang:lower() @@ -156,10 +154,8 @@ end --- @param lang string Name of parser --- @param filetype string|string[] Filetype(s) to associate with lang function M.register(lang, filetype) - vim.validate({ - lang = { lang, 'string' }, - filetype = { filetype, { 'string', 'table' } }, - }) + vim.validate('lang', lang, 'string') + vim.validate('filetype', filetype, { 'string', 'table' }) for _, f in ipairs(ensure_list(filetype)) do if f ~= '' then @@ -170,7 +166,12 @@ end --- Inspects the provided language. --- ---- Inspecting provides some useful information on the language like node names, ... +--- Inspecting provides some useful information on the language like node and field names, ABI +--- version, and whether the language came from a WASM module. +--- +--- Node names are returned in a table mapping each node name to a `boolean` indicating whether or +--- not the node is named (i.e., not anonymous). Anonymous nodes are surrounded with double quotes +--- (`"`). --- ---@param lang string Language ---@return table diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index fd68c2b910..4b42164dc8 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -1037,7 +1037,7 @@ end --- 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_bytes` : see [nvim_buf_attach()]. --- - `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. diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 4614967799..1677e8d364 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -626,7 +626,7 @@ local directive_handlers = { --- Adds a new predicate to be used in queries --- ---@param name string Name of the predicate, without leading # ----@param handler fun(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: vim.treesitter.query.TSMetadata) +---@param handler fun(match: table<integer,TSNode[]>, pattern: integer, source: integer|string, predicate: any[], metadata: vim.treesitter.query.TSMetadata): boolean? --- - see |vim.treesitter.query.add_directive()| for argument meanings ---@param opts? vim.treesitter.query.add_predicate.Opts function M.add_predicate(name, handler, opts) diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index 532decf5e9..cd159f0172 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -20,7 +20,8 @@ local M = {} --- end) --- ``` --- ----@param items any[] Arbitrary items +---@generic T +---@param items T[] Arbitrary items ---@param opts table Additional options --- - prompt (string|nil) --- Text of the prompt. Defaults to `Select one of:` @@ -32,19 +33,19 @@ local M = {} --- Plugins reimplementing `vim.ui.select` may wish to --- use this to infer the structure or semantics of --- `items`, or the context in which select() was called. ----@param on_choice fun(item: any|nil, idx: integer|nil) +---@param on_choice fun(item: T|nil, idx: integer|nil) --- Called once the user made a choice. --- `idx` is the 1-based index of `item` within `items`. --- `nil` if the user aborted the dialog. function M.select(items, opts, on_choice) - vim.validate({ - items = { items, 'table', false }, - on_choice = { on_choice, 'function', false }, - }) + vim.validate('items', items, 'table') + vim.validate('on_choice', on_choice, 'function') opts = opts or {} local choices = { opts.prompt or 'Select one of:' } local format_item = opts.format_item or tostring - for i, item in ipairs(items) do + for i, item in + ipairs(items --[[@as any[] ]]) + do table.insert(choices, string.format('%d: %s', i, format_item(item))) end local choice = vim.fn.inputlist(choices) @@ -86,10 +87,8 @@ end --- an empty string if nothing was entered), or --- `nil` if the user aborted the dialog. function M.input(opts, on_confirm) - vim.validate({ - opts = { opts, 'table', true }, - on_confirm = { on_confirm, 'function', false }, - }) + vim.validate('opts', opts, 'table', true) + vim.validate('on_confirm', on_confirm, 'function') opts = (opts and not vim.tbl_isempty(opts)) and opts or vim.empty_dict() @@ -135,9 +134,7 @@ end --- ---@see |vim.system()| function M.open(path, opt) - vim.validate({ - path = { path, 'string' }, - }) + vim.validate('path', path, 'string') local is_uri = path:match('%w+:') if not is_uri then path = vim.fs.normalize(path) @@ -165,8 +162,10 @@ function M.open(path, opt) cmd = { 'wslview', path } elseif vim.fn.executable('explorer.exe') == 1 then cmd = { 'explorer.exe', path } + elseif vim.fn.executable('lemonade') == 1 then + cmd = { 'lemonade', 'open', path } else - return nil, 'vim.ui.open: no handler found (tried: wslview, explorer.exe, xdg-open)' + return nil, 'vim.ui.open: no handler found (tried: wslview, explorer.exe, xdg-open, lemonade)' end return vim.system(cmd, job_opt), nil @@ -207,7 +206,9 @@ function M._get_urls() if vim.treesitter.node_contains(node, range) then local url = metadata[id] and metadata[id].url if url and match[url] then - for _, n in ipairs(match[url]) do + for _, n in + ipairs(match[url] --[[@as TSNode[] ]]) + do urls[#urls + 1] = vim.treesitter.get_node_text(n, bufnr, { metadata = metadata[url] }) end |