diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/diagnostic.lua | 380 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 97 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 5 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/codelens.lua | 6 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/diagnostic.lua | 3 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 29 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 21 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/ui.lua | 5 |
10 files changed, 338 insertions, 212 deletions
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 4cf22282a2..911d482bfd 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -24,6 +24,16 @@ local global_diagnostic_options = { severity_sort = false, } +M.handlers = setmetatable({}, { + __newindex = function(t, name, handler) + vim.validate { handler = {handler, "t" } } + rawset(t, name, handler) + if not global_diagnostic_options[name] then + global_diagnostic_options[name] = true + end + end, +}) + -- Local functions {{{ ---@private @@ -97,30 +107,8 @@ end local all_namespaces = {} ---@private -local function get_namespace(ns) - if not all_namespaces[ns] then - local name - for k, v in pairs(vim.api.nvim_get_namespaces()) do - if ns == v then - name = k - break - end - end - - assert(name, "namespace does not exist or is anonymous") - - all_namespaces[ns] = { - name = name, - sign_group = string.format("vim.diagnostic.%s", name), - opts = {} - } - end - return all_namespaces[ns] -end - ----@private local function enabled_value(option, namespace) - local ns = namespace and get_namespace(namespace) or {} + local ns = namespace and M.get_namespace(namespace) or {} if ns.opts and type(ns.opts[option]) == "table" then return ns.opts[option] end @@ -154,7 +142,7 @@ end ---@private local function get_resolved_options(opts, namespace, bufnr) - local ns = namespace and get_namespace(namespace) or {} + local ns = namespace and M.get_namespace(namespace) or {} -- Do not use tbl_deep_extend so that an empty table can be used to reset to default values local resolved = vim.tbl_extend('keep', opts or {}, ns.opts or {}, global_diagnostic_options) for k in pairs(global_diagnostic_options) do @@ -343,7 +331,7 @@ local registered_autocmds = {} ---@private local function make_augroup_key(namespace, bufnr) - local ns = get_namespace(namespace) + local ns = M.get_namespace(namespace) return string.format("DiagnosticInsertLeave:%s:%s", bufnr, ns.name) end @@ -566,7 +554,7 @@ function M.config(opts, namespace) local t if namespace then - local ns = get_namespace(namespace) + local ns = M.get_namespace(namespace) t = ns.opts else t = global_diagnostic_options @@ -633,6 +621,32 @@ function M.set(namespace, bufnr, diagnostics, opts) vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged") end +--- Get namespace metadata. +--- +---@param ns number Diagnostic namespace +---@return table Namespace metadata +function M.get_namespace(namespace) + vim.validate { namespace = { namespace, 'n' } } + if not all_namespaces[namespace] then + local name + for k, v in pairs(vim.api.nvim_get_namespaces()) do + if namespace == v then + name = k + break + end + end + + assert(name, "namespace does not exist or is anonymous") + + all_namespaces[namespace] = { + name = name, + opts = {}, + user_data = {}, + } + end + return all_namespaces[namespace] +end + --- Get current diagnostic namespaces. --- ---@return table A list of active diagnostic namespaces |vim.diagnostic|. @@ -782,156 +796,167 @@ function M.goto_next(opts) ) end --- Diagnostic Setters {{{ - ---- Set signs for given diagnostics. ---- ----@param namespace number The diagnostic namespace ----@param bufnr number Buffer number ----@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the ---- current diagnostics in the given buffer are used. ----@param opts table Configuration table with the following keys: ---- - priority: Set the priority of the signs |sign-priority|. ----@private -function M._set_signs(namespace, bufnr, diagnostics, opts) - vim.validate { - namespace = {namespace, 'n'}, - bufnr = {bufnr, 'n'}, - diagnostics = {diagnostics, 't'}, - opts = {opts, 't', true}, - } - - bufnr = get_bufnr(bufnr) - opts = get_resolved_options({ signs = opts }, namespace, bufnr) +M.handlers.signs = { + show = function(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = {namespace, 'n'}, + bufnr = {bufnr, 'n'}, + diagnostics = {diagnostics, 't'}, + opts = {opts, 't', true}, + } - if opts.signs and opts.signs.severity then - diagnostics = filter_by_severity(opts.signs.severity, diagnostics) - end + bufnr = get_bufnr(bufnr) - local ns = get_namespace(namespace) + if opts.signs and opts.signs.severity then + diagnostics = filter_by_severity(opts.signs.severity, diagnostics) + end - define_default_signs() + define_default_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 - 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) + -- 10 is the default sign priority when none is explicitly specified + local priority = opts.signs and opts.signs.priority or 10 + local get_priority + 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(severity) - return priority + (vim.diagnostic.severity.HINT - severity) + get_priority = function() + return priority end end - else - get_priority = function() - return priority - end - end - for _, diagnostic in ipairs(diagnostics) do - vim.fn.sign_place( - 0, - ns.sign_group, - sign_highlight_map[diagnostic.severity], - bufnr, - { - priority = get_priority(diagnostic.severity), - lnum = diagnostic.lnum + 1 - } - ) - end -end - ---- Set underline for given diagnostics. ---- ----@param namespace number The diagnostic namespace ----@param bufnr number Buffer number ----@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the ---- current diagnostics in the given buffer are used. ----@param opts table Configuration table. Currently unused. ----@private -function M._set_underline(namespace, bufnr, diagnostics, opts) - vim.validate { - namespace = {namespace, 'n'}, - bufnr = {bufnr, 'n'}, - diagnostics = {diagnostics, 't'}, - opts = {opts, 't', true}, - } + local ns = M.get_namespace(namespace) + if not ns.user_data.sign_group then + ns.user_data.sign_group = string.format("vim.diagnostic.%s", ns.name) + end - bufnr = get_bufnr(bufnr) - opts = get_resolved_options({ underline = opts }, namespace, bufnr).underline + local sign_group = ns.user_data.sign_group + for _, diagnostic in ipairs(diagnostics) do + vim.fn.sign_place( + 0, + sign_group, + sign_highlight_map[diagnostic.severity], + bufnr, + { + priority = get_priority(diagnostic.severity), + lnum = diagnostic.lnum + 1 + } + ) + end + end, + hide = function(namespace, bufnr) + local ns = M.get_namespace(namespace) + if ns.user_data.sign_group then + vim.fn.sign_unplace(ns.user_data.sign_group, {buffer=bufnr}) + end + end, +} - if opts and opts.severity then - diagnostics = filter_by_severity(opts.severity, diagnostics) - end +M.handlers.underline = { + show = function(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = {namespace, 'n'}, + bufnr = {bufnr, 'n'}, + diagnostics = {diagnostics, 't'}, + opts = {opts, 't', true}, + } - for _, diagnostic in ipairs(diagnostics) do - local higroup = underline_highlight_map[diagnostic.severity] + bufnr = get_bufnr(bufnr) - if higroup == nil then - -- Default to error if we don't have a highlight associated - higroup = underline_highlight_map.Error + if opts.underline and opts.underline.severity then + diagnostics = filter_by_severity(opts.underline.severity, diagnostics) end - vim.highlight.range( - bufnr, - namespace, - higroup, - { diagnostic.lnum, diagnostic.col }, - { diagnostic.end_lnum, diagnostic.end_col } - ) - end -end + local ns = M.get_namespace(namespace) + if not ns.user_data.underline_ns then + ns.user_data.underline_ns = vim.api.nvim_create_namespace("") + end ---- Set virtual text for given diagnostics. ---- ----@param namespace number The diagnostic namespace ----@param bufnr number Buffer number ----@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the ---- current diagnostics in the given buffer are used. ----@param opts table|nil Configuration table with the following keys: ---- - prefix: (string) Prefix to display before virtual text on line. ---- - spacing: (number) Number of spaces to insert before virtual text. ---- - source: (string) Include the diagnostic source in virtual text. One of "always" or ---- "if_many". ----@private -function M._set_virtual_text(namespace, bufnr, diagnostics, opts) - vim.validate { - namespace = {namespace, 'n'}, - bufnr = {bufnr, 'n'}, - diagnostics = {diagnostics, 't'}, - opts = {opts, 't', true}, - } + local underline_ns = ns.user_data.underline_ns + for _, diagnostic in ipairs(diagnostics) do + local higroup = underline_highlight_map[diagnostic.severity] - bufnr = get_bufnr(bufnr) - opts = get_resolved_options({ virtual_text = opts }, namespace, bufnr).virtual_text + if higroup == nil then + -- Default to error if we don't have a highlight associated + higroup = underline_highlight_map.Error + end - if opts and opts.format then - diagnostics = reformat_diagnostics(opts.format, diagnostics) + vim.highlight.range( + bufnr, + underline_ns, + higroup, + { diagnostic.lnum, diagnostic.col }, + { diagnostic.end_lnum, diagnostic.end_col } + ) + end + end, + hide = function(namespace, bufnr) + local ns = M.get_namespace(namespace) + if ns.user_data.underline_ns then + vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.underline_ns, 0, -1) + end end +} - if opts and opts.source then - diagnostics = prefix_source(opts.source, diagnostics) - end +M.handlers.virtual_text = { + show = function(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = {namespace, 'n'}, + bufnr = {bufnr, 'n'}, + diagnostics = {diagnostics, 't'}, + opts = {opts, 't', true}, + } + + bufnr = get_bufnr(bufnr) - local buffer_line_diagnostics = diagnostic_lines(diagnostics) - for line, line_diagnostics in pairs(buffer_line_diagnostics) do - if opts and opts.severity then - line_diagnostics = filter_by_severity(opts.severity, line_diagnostics) + local severity + if opts.virtual_text then + if opts.virtual_text.format then + diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics) + end + if opts.virtual_text.source then + diagnostics = prefix_source(opts.virtual_text.source, diagnostics) + end + if opts.virtual_text.severity then + severity = opts.virtual_text.severity + end end - local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts) - if virt_texts then - vim.api.nvim_buf_set_extmark(bufnr, namespace, line, 0, { - hl_mode = "combine", - virt_text = virt_texts, - }) + local ns = M.get_namespace(namespace) + if not ns.user_data.virt_text_ns then + ns.user_data.virt_text_ns = vim.api.nvim_create_namespace("") end - end -end + + local virt_text_ns = ns.user_data.virt_text_ns + local buffer_line_diagnostics = diagnostic_lines(diagnostics) + for line, line_diagnostics in pairs(buffer_line_diagnostics) do + if severity then + line_diagnostics = filter_by_severity(severity, line_diagnostics) + end + local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts.virtual_text) + + if virt_texts then + vim.api.nvim_buf_set_extmark(bufnr, virt_text_ns, line, 0, { + hl_mode = "combine", + virt_text = virt_texts, + }) + end + end + end, + hide = function(namespace, bufnr) + local ns = M.get_namespace(namespace) + if ns.user_data.virt_text_ns then + vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_text_ns, 0, -1) + end + end, +} --- Get virtual text chunks to display using |nvim_buf_set_extmark()|. --- @@ -1011,19 +1036,16 @@ function M.hide(namespace, bufnr) bufnr = get_bufnr(bufnr) diagnostic_cache_extmarks[bufnr][namespace] = {} - local ns = get_namespace(namespace) - - -- clear sign group - vim.fn.sign_unplace(ns.sign_group, {buffer=bufnr}) - - -- clear virtual text namespace - vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) + for _, handler in pairs(M.handlers) do + if handler.hide then + handler.hide(namespace, bufnr) + end + end end - --- Display diagnostics for the given namespace and buffer. --- ----@param namespace number Diagnostic namespace +---@param namespace number Diagnostic namespace. ---@param bufnr number|nil Buffer number. Defaults to the current buffer. ---@param diagnostics table|nil The diagnostics to display. When omitted, use the --- saved diagnostics for the given namespace and @@ -1074,16 +1096,10 @@ function M.show(namespace, bufnr, diagnostics, opts) clamp_line_numbers(bufnr, diagnostics) - if opts.underline then - M._set_underline(namespace, bufnr, diagnostics, opts.underline) - end - - if opts.virtual_text then - M._set_virtual_text(namespace, bufnr, diagnostics, opts.virtual_text) - end - - if opts.signs then - M._set_signs(namespace, bufnr, diagnostics, opts.signs) + for handler_name, handler in pairs(M.handlers) do + if handler.show and opts[handler_name] then + handler.show(namespace, bufnr, diagnostics, opts) + end end save_extmarks(namespace, bufnr) @@ -1138,6 +1154,17 @@ function M.open_float(bufnr, opts) error("Invalid value for option 'scope'") end + do + -- Resolve options with user settings from vim.diagnostic.config + -- Unlike the other decoration functions (e.g. set_virtual_text, set_signs, etc.) `open_float` + -- does not have a dedicated table for configuration options; instead, the options are mixed in + -- with its `opts` table which also includes "keyword" parameters. So we create a dedicated + -- options table that inherits missing keys from the global configuration before resolving. + local t = global_diagnostic_options.float + local float_opts = vim.tbl_extend("keep", opts, type(t) == "table" and t or {}) + opts = get_resolved_options({ float = float_opts }, nil, bufnr).float + end + local diagnostics = M.get(bufnr, opts) clamp_line_numbers(bufnr, diagnostics) @@ -1168,17 +1195,6 @@ function M.open_float(bufnr, opts) end end - do - -- Resolve options with user settings from vim.diagnostic.config - -- Unlike the other decoration functions (e.g. set_virtual_text, set_signs, etc.) `open_float` - -- does not have a dedicated table for configuration options; instead, the options are mixed in - -- with its `opts` table which also includes "keyword" parameters. So we create a dedicated - -- options table that inherits missing keys from the global configuration before resolving. - local t = global_diagnostic_options.float - local float_opts = vim.tbl_extend("keep", opts, type(t) == "table" and t or {}) - opts = get_resolved_options({ float = float_opts }, nil, bufnr).float - end - local lines = {} local highlights = {} local show_header = vim.F.if_nil(opts.show_header, true) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 56ac1cbc66..fb4718c1bb 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -239,6 +239,7 @@ local function validate_client_config(config) on_exit = { config.on_exit, "f", true }; on_init = { config.on_init, "f", true }; settings = { config.settings, "t", true }; + commands = { config.commands, 't', true }; before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; flags = { config.flags, "t", true }; @@ -590,6 +591,11 @@ end --- returned to the language server if requested via `workspace/configuration`. --- Keys are case-sensitive. --- +---@param commands table Table that maps string of clientside commands to user-defined functions. +--- Commands passed to start_client take precedence over the global command registry. Each key +--- must be a unique comand name, and the value is a function which is called if any LSP action +--- (code action, code lenses, ...) triggers the command. +--- ---@param init_options Values to pass in the initialization request --- as `initializationOptions`. See `initialize` in the LSP spec. --- @@ -772,8 +778,11 @@ function lsp.start_client(config) attached_buffers = {}; handlers = handlers; + commands = config.commands or {}; + + requests = {}; -- for $/progress report - messages = { name = name, messages = {}, progress = {}, status = {} } + messages = { name = name, messages = {}, progress = {}, status = {} }; } -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. @@ -906,11 +915,21 @@ function lsp.start_client(config) end -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state changetracking.flush(client) - + bufnr = resolve_bufnr(bufnr) local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr) - return rpc.request(method, params, function(err, result) + local success, request_id = rpc.request(method, params, function(err, result) handler(err, result, {method=method, client_id=client_id, bufnr=bufnr, params=params}) + end, function(request_id) + client.requests[request_id] = nil + nvim_command("doautocmd <nomodeline> User LspRequest") end) + + if success then + client.requests[request_id] = { type='pending', bufnr=bufnr, method=method } + nvim_command("doautocmd <nomodeline> User LspRequest") + end + + return success, request_id end ---@private @@ -970,6 +989,11 @@ function lsp.start_client(config) ---@see |vim.lsp.client.notify()| function client.cancel_request(id) validate{id = {id, 'n'}} + local request = client.requests[id] + if request and request.type == 'pending' then + request.type = 'cancel' + nvim_command("doautocmd <nomodeline> User LspRequest") + end return rpc.notify("$/cancelRequest", { id = id }) end @@ -1238,27 +1262,30 @@ function lsp._vim_exit_handler() send_kill = true timeouts[client_id] = timeout max_timeout = math.max(timeout, max_timeout) - else - active_clients[client_id] = nil end end local poll_time = 50 local function check_clients_closed() + for client_id, timeout in pairs(timeouts) do + timeouts[client_id] = timeout - poll_time + end + for client_id, _ in pairs(active_clients) do - timeouts[client_id] = timeouts[client_id] - poll_time - if timeouts[client_id] < 0 then - active_clients[client_id] = nil + if timeouts[client_id] ~= nil and timeouts[client_id] > 0 then + return false end end - return tbl_isempty(active_clients) + return true end if send_kill then if not vim.wait(max_timeout, check_clients_closed, poll_time) then - for _, client in pairs(active_clients) do - client.stop(true) + for client_id, client in pairs(active_clients) do + if timeouts[client_id] ~= nil then + client.stop(true) + end end end end @@ -1300,7 +1327,7 @@ function lsp.buf_request(bufnr, method, params, handler) if not tbl_isempty(all_buffer_active_clients[resolve_bufnr(bufnr)] or {}) and not method_supported then vim.notify(lsp._unsupported_method(method), vim.log.levels.ERROR) vim.api.nvim_command("redraw") - return + return {}, function() end end local client_request_ids = {} @@ -1512,6 +1539,52 @@ function lsp.omnifunc(findstart, base) return -2 end +--- Provides an interface between the built-in client and a `formatexpr` function. +--- +--- Currently only supports a single client. This can be set via +--- `setlocal formatexpr=v:lua.vim.lsp.formatexpr()` but will typically or in `on_attach` +--- via `vim.api.nvim_buf_set_option(bufnr, 'formatexpr', 'v:lua.vim.lsp.formatexpr(#{timeout_ms:250})')`. +--- +---@param opts table options for customizing the formatting expression which takes the +--- following optional keys: +--- * timeout_ms (default 500ms). The timeout period for the formatting request. +function lsp.formatexpr(opts) + opts = opts or {} + local timeout_ms = opts.timeout_ms or 500 + + if vim.tbl_contains({'i', 'R', 'ic', 'ix'}, vim.fn.mode()) then + -- `formatexpr` is also called when exceeding `textwidth` in insert mode + -- fall back to internal formatting + return 1 + end + + local start_line = vim.v.lnum + local end_line = start_line + vim.v.count - 1 + + if start_line > 0 and end_line > 0 then + local params = { + textDocument = util.make_text_document_params(); + range = { + start = { line = start_line - 1; character = 0; }; + ["end"] = { line = end_line - 1; character = 0; }; + }; + }; + params.options = util.make_formatting_params().options + local client_results = vim.lsp.buf_request_sync(0, "textDocument/rangeFormatting", params, timeout_ms) + + -- Apply the text edits from one and only one of the clients. + for _, response in pairs(client_results) do + if response.result then + vim.lsp.util.apply_text_edits(response.result, 0) + return 0 + end + end + end + + -- do not run builtin formatter. + return 0 +end + ---Checks whether a client is stopped. --- ---@param client_id (Number) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 245f29943e..128f0b01ad 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -480,11 +480,11 @@ local function on_code_action_results(results, ctx) end if action.command then local command = type(action.command) == 'table' and action.command or action - local fn = vim.lsp.commands[command.command] + local fn = client.commands[command.command] or vim.lsp.commands[command.command] if fn then local enriched_ctx = vim.deepcopy(ctx) enriched_ctx.client_id = client.id - fn(command, ctx) + fn(command, enriched_ctx) else M.execute_command(command) end @@ -529,6 +529,7 @@ local function on_code_action_results(results, ctx) vim.ui.select(action_tuples, { prompt = 'Code actions:', + kind = 'codeaction', format_item = function(action_tuple) local title = action_tuple[2].title:gsub('\r\n', '\\r\\n') return title:gsub('\n', '\\n') diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index 63fcbe430b..9eb64c9a2e 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -31,15 +31,15 @@ local function execute_lens(lens, bufnr, client_id) local line = lens.range.start.line api.nvim_buf_clear_namespace(bufnr, namespaces[client_id], line, line + 1) + local client = vim.lsp.get_client_by_id(client_id) + assert(client, 'Client is required to execute lens, client_id=' .. client_id) local command = lens.command - local fn = vim.lsp.commands[command.command] + local fn = client.commands[command.command] or vim.lsp.commands[command.command] if fn then fn(command, { bufnr = bufnr, client_id = client_id }) return end -- Need to use the client that returned the lens → must not use buf_request - local client = vim.lsp.get_client_by_id(client_id) - assert(client, 'Client is required to execute lens, client_id=' .. client_id) local command_provider = client.server_capabilities.executeCommandProvider local commands = type(command_provider) == 'table' and command_provider.commands or {} if not vim.tbl_contains(commands, command.command) then diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index bea0e44aca..1e6f83c1ba 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -146,7 +146,8 @@ local _client_namespaces = {} function M.get_namespace(client_id) vim.validate { client_id = { client_id, 'n' } } if not _client_namespaces[client_id] then - local name = string.format("vim.lsp.client-%d", client_id) + local client = vim.lsp.get_client_by_id(client_id) + local name = string.format("vim.lsp.%s.%d", client and client.name or "unknown", client_id) _client_namespaces[client_id] = vim.api.nvim_create_namespace(name) end return _client_namespaces[client_id] diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index eff27807be..01d7102e8f 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -185,7 +185,7 @@ local function response_to_list(map_result, entity) title = 'Language Server'; items = map_result(result, ctx.bufnr); }) - api.nvim_command("copen") + api.nvim_command("botright copen") end end end diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index d9a684a738..bce1e9f35d 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -297,6 +297,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) local message_index = 0 local message_callbacks = {} + local notify_reply_callbacks = {} local handle, pid do @@ -309,8 +310,9 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) stdout:close() stderr:close() handle:close() - -- Make sure that message_callbacks can be gc'd. + -- Make sure that message_callbacks/notify_reply_callbacks can be gc'd. message_callbacks = nil + notify_reply_callbacks = nil dispatchers.on_exit(code, signal) end local spawn_params = { @@ -375,10 +377,12 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) ---@param method (string) The invoked LSP method ---@param params (table) Parameters for the invoked LSP method ---@param callback (function) Callback to invoke + ---@param notify_reply_callback (function) Callback to invoke as soon as a request is no longer pending ---@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not - local function request(method, params, callback) + local function request(method, params, callback, notify_reply_callback) validate { callback = { callback, 'f' }; + notify_reply_callback = { notify_reply_callback, 'f', true }; } message_index = message_index + 1 local message_id = message_index @@ -388,8 +392,15 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) method = method; params = params; } - if result and message_callbacks then - message_callbacks[message_id] = schedule_wrap(callback) + if result then + if message_callbacks then + message_callbacks[message_id] = schedule_wrap(callback) + else + return false + end + if notify_reply_callback and notify_reply_callbacks then + notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback) + end return result, message_id else return false @@ -466,6 +477,16 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) -- We sent a number, so we expect a number. local result_id = tonumber(decoded.id) + -- Notify the user that a response was received for the request + 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' }; + } + notify_reply_callback(result_id) + notify_reply_callbacks[result_id] = nil + end + -- Do not surface RequestCancelled to users, it is RPC-internal. if decoded.error then local mute_error = false diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 7a0ac458f3..a5bf0efcb1 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -152,6 +152,7 @@ end --- Returns a zero-indexed column, since set_lines() does the conversion to --- 1-indexed local function get_line_byte_from_position(bufnr, position) + -- TODO handle offset_encoding -- LSP's line and characters are 0-indexed -- Vim's line and columns are 1-indexed local col = position.character @@ -165,7 +166,7 @@ local function get_line_byte_from_position(bufnr, position) local line = position.line local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false) if #lines > 0 then - local ok, result = pcall(vim.str_byteindex, lines[1], col) + local ok, result = pcall(vim.str_byteindex, lines[1], col, true) if ok then return result @@ -276,7 +277,8 @@ function M.apply_text_edits(text_edits, bufnr) -- 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. local has_eol_text_edit = false local max = vim.api.nvim_buf_line_count(bufnr) - local len = vim.str_utfindex(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') + -- TODO handle offset_encoding + local _, len = vim.str_utfindex(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') text_edits = vim.tbl_map(function(text_edit) if max <= text_edit.range.start.line then text_edit.range.start.line = max - 1 @@ -1720,7 +1722,9 @@ function M.symbols_to_items(symbols, bufnr) }) if symbol.children then for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do - vim.list_extend(_items, v) + for _, s in ipairs(v) do + table.insert(_items, s) + end end end end @@ -1788,7 +1792,9 @@ local function make_position_param() if not line then return { line = 0; character = 0; } end - col = str_utfindex(line, col) + -- TODO handle offset_encoding + local _ + _, col = str_utfindex(line, col) return { line = row; character = col; } end @@ -1838,11 +1844,14 @@ function M.make_given_range_params(start_pos, end_pos) A[1] = A[1] - 1 B[1] = B[1] - 1 -- account for encoding. + -- TODO handle offset_encoding if A[2] > 0 then - A = {A[1], M.character_offset(0, A[1], A[2])} + local _, char = M.character_offset(0, A[1], A[2]) + A = {A[1], char} end if B[2] > 0 then - B = {B[1], M.character_offset(0, B[1], B[2])} + local _, char = M.character_offset(0, B[1], B[2]) + B = {B[1], char} end -- we need to offset the end character position otherwise we loose the last -- character of the selection, as LSP end position is exclusive diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index b57b7ad4ad..6e40b6ca52 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -605,7 +605,7 @@ do function vim.validate(opt) local ok, err_msg = is_valid(opt) if not ok then - error(debug.traceback(err_msg, 2), 2) + error(err_msg, 2) end end end diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index 5eab20fc54..adc1e16759 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -9,6 +9,11 @@ local M = {} --- - format_item (function item -> text) --- Function to format an --- individual item from `items`. Defaults to `tostring`. +--- - kind (string|nil) +--- Arbitrary hint string indicating the item shape. +--- 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 function ((item|nil, idx|nil) -> ()) --- Called once the user made a choice. --- `idx` is the 1-based index of `item` within `item`. |