local vim = vim local validate = vim.validate local vfn = vim.fn local util = require 'vim.lsp.util' local M = {} ---@private --- Returns nil if {status} is false or nil, otherwise returns the rest of the --- arguments. local function ok_or_nil(status, ...) if not status then return end return ... end ---@private --- Swallows errors. --- ---@param fn Function to run ---@param ... Function arguments ---@returns Result of `fn(...)` if there are no errors, otherwise nil. --- Returns nil if errors occur during {fn}, otherwise returns local function npcall(fn, ...) return ok_or_nil(pcall(fn, ...)) end ---@private --- Sends an async request to all active clients attached to the current --- buffer. --- ---@param method (string) LSP method name ---@param params (optional, table) Parameters to send to the server ---@param handler (optional, functionnil) See |lsp-handler|. Follows |lsp-handler-resolution| -- ---@returns 2-tuple: --- - Map of client-id:request-id pairs for all successful 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) end --- Checks whether the language servers attached to the current buffer are --- ready. --- ---@returns `true` if server responds. function M.server_ready() return not not vim.lsp.buf_notify(0, "window/progress", {}) end --- Displays hover information about the symbol under the cursor in a floating --- window. Calling the function twice will jump into the floating window. function M.hover() local params = util.make_position_params() request('textDocument/hover', params) end --- Jumps to the declaration of the symbol under the cursor. ---@note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead. --- function M.declaration() local params = util.make_position_params() request('textDocument/declaration', params) end --- Jumps to the definition of the symbol under the cursor. --- function M.definition() local params = util.make_position_params() request('textDocument/definition', params) end --- Jumps to the definition of the type of the symbol under the cursor. --- function M.type_definition() local params = util.make_position_params() request('textDocument/typeDefinition', params) end --- Lists all the implementations for the symbol under the cursor in the --- quickfix window. function M.implementation() local params = util.make_position_params() request('textDocument/implementation', params) end --- Displays signature information about the symbol under the cursor in a --- floating window. function M.signature_help() local params = util.make_position_params() request('textDocument/signatureHelp', params) end --- Retrieves the completion items at the current cursor position. Can only be --- called in Insert mode. --- ---@param context (context support not yet implemented) Additional information --- about the context in which a completion was triggered (how it was triggered, --- and by which trigger character, if applicable) --- ---@see |vim.lsp.protocol.constants.CompletionTriggerKind| function M.completion(context) local params = util.make_position_params() params.context = context return request('textDocument/completion', params) end ---@private --- If there is more than one client that supports the given method, --- asks the user to select one. -- ---@returns The client that the user selected or nil local function select_client(method) local clients = vim.tbl_values(vim.lsp.buf_get_clients()); clients = vim.tbl_filter(function (client) return client.supports_method(method) end, clients) -- better UX when choices are always in the same order (between restarts) table.sort(clients, function (a, b) return a.name < b.name end) if #clients > 1 then local choices = {} for k,v in pairs(clients) do table.insert(choices, string.format("%d %s", k, v.name)) end local user_choice = vim.fn.confirm( "Select a language server:", table.concat(choices, "\n"), 0, "Question" ) if user_choice == 0 then return nil end return clients[user_choice] elseif #clients < 1 then return nil else return clients[1] end end --- Formats the current buffer. --- ---@param options (optional, table) Can be used to specify FormattingOptions. --- Some unspecified options will be automatically derived from the current --- Neovim options. -- ---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting function M.formatting(options) local client = select_client("textDocument/formatting") if client == nil then return end local params = util.make_formatting_params(options) return client.request("textDocument/formatting", params, nil, vim.api.nvim_get_current_buf()) end --- Performs |vim.lsp.buf.formatting()| synchronously. --- --- Useful for running on save, to make sure buffer is formatted prior to being --- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. Example: --- ---
--- vim.api.nvim_command[[autocmd BufWritePre--- ---@param options Table with valid `FormattingOptions` entries ---@param timeout_ms (number) Request timeout ---@see |vim.lsp.buf.formatting_seq_sync| function M.formatting_sync(options, timeout_ms) local client = select_client("textDocument/formatting") if client == nil then return end local params = util.make_formatting_params(options) local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf()) if result and result.result then util.apply_text_edits(result.result) elseif err then vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN) end end --- Formats the current buffer by sequentially requesting formatting from attached clients. --- --- Useful when multiple clients with formatting capability are attached. --- --- Since it's synchronous, can be used for running on save, to make sure buffer is formatted --- prior to being saved. {timeout_ms} is passed on to the |vim.lsp.client| `request_sync` method. --- Example: ---lua vim.lsp.buf.formatting_sync()]] ---
--- vim.api.nvim_command[[autocmd BufWritePre--- ---@param options (optional, table) `FormattingOptions` entries ---@param timeout_ms (optional, number) Request timeout ---@param order (optional, table) List of client names. Formatting is requested from clients ---in the following order: first all clients that are not in the `order` list, then ---the remaining clients in the order as they occur in the `order` list. function M.formatting_seq_sync(options, timeout_ms, order) local clients = vim.tbl_values(vim.lsp.buf_get_clients()); -- sort the clients according to `order` for _, client_name in pairs(order or {}) do -- if the client exists, move to the end of the list for i, client in pairs(clients) do if client.name == client_name then table.insert(clients, table.remove(clients, i)) break end end end -- loop through the clients and make synchronous formatting requests for _, client in pairs(clients) do if client.resolved_capabilities.document_formatting then local params = util.make_formatting_params(options) local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf()) if result and result.result then util.apply_text_edits(result.result) elseif err then vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN) end end end end --- Formats a given range. --- ---@param options Table with valid `FormattingOptions` entries. ---@param start_pos ({number, number}, optional) mark-indexed position. ---Defaults to the start of the last visual selection. ---@param end_pos ({number, number}, optional) mark-indexed position. ---Defaults to the end of the last visual selection. function M.range_formatting(options, start_pos, end_pos) local client = select_client("textDocument/rangeFormatting") if client == nil then return end local params = util.make_given_range_params(start_pos, end_pos) params.options = util.make_formatting_params(options).options return client.request("textDocument/rangeFormatting", params) end --- Renames all references to the symbol under the cursor. --- ---@param new_name (string) If not provided, the user will be prompted for a new ---name using |vim.ui.input()|. function M.rename(new_name) local opts = { prompt = "New Name: " } ---@private local function on_confirm(input) if not (input and #input > 0) then return end local params = util.make_position_params() params.newName = input request('textDocument/rename', params) end local function prepare_rename(err, result) if err == nil and result == nil then vim.notify('nothing to rename', vim.log.levels.INFO) return end if result and result.placeholder then opts.default = result.placeholder if not new_name then npcall(vim.ui.input, opts, on_confirm) end elseif result and result.start and result['end'] and result.start.line == result['end'].line then local line = vfn.getline(result.start.line+1) local start_char = result.start.character+1 local end_char = result['end'].character opts.default = string.sub(line, start_char, end_char) if not new_name then npcall(vim.ui.input, opts, on_confirm) end else -- fallback to guessing symbol usinglua vim.lsp.buf.formatting_seq_sync()]] ---
--- vim.api.nvim_command [[autocmd CursorHold--- --- Note: Usage of |vim.lsp.buf.document_highlight()| requires the following highlight groups --- to be defined or you won't be able to see the actual highlights. --- |LspReferenceText| --- |LspReferenceRead| --- |LspReferenceWrite| function M.document_highlight() local params = util.make_position_params() request('textDocument/documentHighlight', params) end --- Removes document highlights from current buffer. --- function M.clear_references() util.buf_clear_references() end ---@private -- --- This is not public because the main extension point is --- vim.ui.select which can be overridden independently. --- --- Can't call/use vim.lsp.handlers['textDocument/codeAction'] because it expects --- `(err, CodeAction[] | Command[], ctx)`, but we want to aggregate the results --- from multiple clients to have 1 single UI prompt for the user, yet we still --- need to be able to link a `CodeAction|Command` to the right client for --- `codeAction/resolve` local function on_code_action_results(results, ctx) local action_tuples = {} for client_id, result in pairs(results) do for _, action in pairs(result.result or {}) do table.insert(action_tuples, { client_id, action }) end end if #action_tuples == 0 then vim.notify('No code actions available', vim.log.levels.INFO) return end ---@private local function apply_action(action, client) if action.edit then util.apply_workspace_edit(action.edit) end if action.command then local command = type(action.command) == 'table' and action.command or action 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, enriched_ctx) else M.execute_command(command) end end end ---@private local function on_user_choice(action_tuple) if not action_tuple then return end -- textDocument/codeAction can return either Command[] or CodeAction[] -- -- CodeAction -- ... -- edit?: WorkspaceEdit -- <- must be applied before command -- command?: Command -- -- Command: -- title: string -- command: string -- arguments?: any[] -- local client = vim.lsp.get_client_by_id(action_tuple[1]) local action = action_tuple[2] if not action.edit and client and type(client.resolved_capabilities.code_action) == 'table' and client.resolved_capabilities.code_action.resolveProvider then client.request('codeAction/resolve', action, function(err, resolved_action) if err then vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR) return end apply_action(resolved_action, client) end) else apply_action(action, client) end end 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') end, }, on_user_choice) end --- Requests code actions from all clients and calls the handler exactly once --- with all aggregated results ---@private local function code_action_request(params) local bufnr = vim.api.nvim_get_current_buf() local method = 'textDocument/codeAction' vim.lsp.buf_request_all(bufnr, method, params, function(results) on_code_action_results(results, { bufnr = bufnr, method = method, params = params }) end) end --- Selects a code action available at the current --- cursor position. --- ---@param context table|nil `CodeActionContext` of the LSP specification: --- - diagnostics: (table|nil) --- LSP `Diagnostic[]`. Inferred from the current --- position if not provided. --- - only: (string|nil) --- LSP `CodeActionKind` used to filter the code actions. --- Most language servers support values like `refactor` --- or `quickfix`. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction function M.code_action(context) validate { context = { context, 't', true } } context = context or {} if not context.diagnostics then context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics() end local params = util.make_range_params() params.context = context code_action_request(params) end --- Performs |vim.lsp.buf.code_action()| for a given range. --- --- ---@param context table|nil `CodeActionContext` of the LSP specification: --- - diagnostics: (table|nil) --- LSP `Diagnostic[]`. Inferred from the current --- position if not provided. --- - only: (string|nil) --- LSP `CodeActionKind` used to filter the code actions. --- Most language servers support values like `refactor` --- or `quickfix`. ---@param start_pos ({number, number}, optional) mark-indexed position. ---Defaults to the start of the last visual selection. ---@param end_pos ({number, number}, optional) mark-indexed position. ---Defaults to the end of the last visual selection. function M.range_code_action(context, start_pos, end_pos) validate { context = { context, 't', true } } context = context or {} if not context.diagnostics then context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics() end local params = util.make_given_range_params(start_pos, end_pos) params.context = context code_action_request(params) end --- Executes an LSP server command. --- ---@param command A valid `ExecuteCommandParams` object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand function M.execute_command(command) validate { command = { command.command, 's' }, arguments = { command.arguments, 't', true } } request('workspace/executeCommand', command) end return M -- vim:sw=2 ts=2 etlua vim.lsp.buf.document_highlight()]] --- vim.api.nvim_command [[autocmd CursorHoldI lua vim.lsp.buf.document_highlight()]] --- vim.api.nvim_command [[autocmd CursorMoved lua vim.lsp.buf.clear_references()]] ---