diff options
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 35 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/codelens.lua | 17 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/diagnostic.lua | 30 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 112 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/health.lua | 27 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/log.lua | 16 |
6 files changed, 157 insertions, 80 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 250af0a0a7..8bfcd90f12 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -249,13 +249,34 @@ end ---@param new_name (string) If not provided, the user will be prompted for a new ---name using |input()|. function M.rename(new_name) - -- TODO(ashkan) use prepareRename - -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. local params = util.make_position_params() - new_name = new_name or npcall(vfn.input, "New Name: ", vfn.expand('<cword>')) - if not (new_name and #new_name > 0) then return end - params.newName = new_name - request('textDocument/rename', params) + 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 + new_name = new_name or npcall(vfn.input, "New Name: ", result.placeholder) + 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 + new_name = new_name or npcall(vfn.input, "New Name: ", string.sub(line, start_char, end_char)) + else + -- fallback to guessing symbol using <cword> + -- + -- this can happen if the language server does not support prepareRename, + -- returns an unexpected response, or requests for "default behavior" + -- + -- see https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename + new_name = new_name or npcall(vfn.input, "New Name: ", vfn.expand('<cword>')) + end + if not (new_name and #new_name > 0) then return end + params.newName = new_name + request('textDocument/rename', params) + end + request('textDocument/prepareRename', params, prepare_rename) end --- Lists all the references to the symbol under the cursor in the quickfix window. @@ -433,7 +454,7 @@ local function code_action_request(params) for _, r in pairs(results) do vim.list_extend(actions, r.result or {}) end - vim.lsp.handlers[method](nil, method, actions, nil, bufnr) + vim.lsp.handlers[method](nil, actions, {bufnr=bufnr, method=method}) end) end diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index ca5a44077e..9cedb2f1db 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -44,9 +44,10 @@ end --- Return all lenses for the given buffer --- +---@param bufnr number Buffer number. 0 can be used for the current buffer. ---@return table (`CodeLens[]`) function M.get(bufnr) - local lenses_by_client = lens_cache_by_buf[bufnr] + local lenses_by_client = lens_cache_by_buf[bufnr or 0] if not lenses_by_client then return {} end local lenses = {} for _, client_lenses in pairs(lenses_by_client) do @@ -173,7 +174,7 @@ local function resolve_lenses(lenses, bufnr, client_id, callback) if lens.command then countdown() else - client.request('codeLens/resolve', lens, function(_, _, result) + client.request('codeLens/resolve', lens, function(_, result) if result and result.command then lens.command = result.command -- Eager display to have some sort of incremental feedback @@ -196,17 +197,17 @@ end --- |lsp-handler| for the method `textDocument/codeLens` --- -function M.on_codelens(err, _, result, client_id, bufnr) +function M.on_codelens(err, result, ctx, _) assert(not err, vim.inspect(err)) - M.save(result, bufnr, client_id) + M.save(result, ctx.bufnr, ctx.client_id) -- Eager display for any resolved (and unresolved) lenses and refresh them -- once resolved. - M.display(result, bufnr, client_id) - resolve_lenses(result, bufnr, client_id, function() - M.display(result, bufnr, client_id) - active_refreshes[bufnr] = nil + M.display(result, ctx.bufnr, ctx.client_id) + resolve_lenses(result, ctx.bufnr, ctx.client_id, function() + M.display(result, ctx.bufnr, ctx.client_id) + active_refreshes[ctx.bufnr] = nil end) end diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index 45aeb17465..ccd325b1ac 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -449,7 +449,7 @@ end --- endif --- return sl --- endfunction ---- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus() +--- autocmd BufWinEnter * let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus() --- </pre> --- ---@param bufnr number The buffer number @@ -1020,15 +1020,16 @@ end --- - Update diagnostics in InsertMode or wait until InsertLeave --- - severity_sort: (default=false) --- - Sort diagnostics (and thus signs and virtual text) -function M.on_publish_diagnostics(_, _, params, client_id, _, config) - local uri = params.uri +function M.on_publish_diagnostics(_, result, ctx, config) + local client_id = ctx.client_id + local uri = result.uri local bufnr = vim.uri_to_bufnr(uri) if not bufnr then return end - local diagnostics = params.diagnostics + local diagnostics = result.diagnostics if config and if_nil(config.severity_sort, false) then table.sort(diagnostics, function(a, b) return a.severity > b.severity end) @@ -1204,19 +1205,20 @@ function M.redraw(bufnr, client_id) -- the user may have set with vim.lsp.with. vim.lsp.handlers["textDocument/publishDiagnostics"]( nil, - "textDocument/publishDiagnostics", { uri = vim.uri_from_bufnr(bufnr), diagnostics = M.get(bufnr, client_id), }, - client_id, - bufnr - ) -end + { + method = "textDocument/publishDiagnostics", + client_id = client_id, + bufnr = bufnr, + } + ) + end --- }}} --- Diagnostic User Functions {{{ +---@private --- Open a floating window with the provided diagnostics --- --- The floating window can be customized with the following highlight groups: @@ -1265,8 +1267,11 @@ local function show_diagnostics(opts, diagnostics) return popup_bufnr, winnr end ---- Open a floating window with the diagnostics from {position} +-- }}} +-- Diagnostic User Functions {{{ + +--- Open a floating window with the diagnostics from {position} ---@param opts table|nil Configuration keys --- - severity: (DiagnosticSeverity, default nil) --- - Only return diagnostics with this severity. Overrides severity_limit @@ -1334,6 +1339,7 @@ function M.reset(client_id, buffer_client_map) end) end +---@private --- Gets diagnostics, converts them to quickfix/location list items, and applies the item_handler callback to the items. ---@param item_handler function Callback to apply to the diagnostic items ---@param command string|nil Command to execute after applying the item_handler diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index befb9fcec3..8fa6f6d024 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -18,19 +18,20 @@ local function err_message(...) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand -M['workspace/executeCommand'] = function() +M['workspace/executeCommand'] = function(_, _, _, _) -- Error handling is done implicitly by wrapping all handlers; see end of this file end ---@private -local function progress_handler(_, _, params, client_id) +local function progress_handler(_, result, ctx, _) + 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) if not client then err_message("LSP[", client_name, "] client has shut down after sending the message") end - local val = params.value -- unspecified yet - local token = params.token -- string or number + local val = result.value -- unspecified yet + local token = result.token -- string or number if val.kind then @@ -62,9 +63,10 @@ end M['$/progress'] = progress_handler --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create -M['window/workDoneProgress/create'] = function(_, _, params, client_id) +M['window/workDoneProgress/create'] = function(_, result, ctx) + local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) - local token = params.token -- string or number + local token = result.token -- string or number local client_name = client and client.name or string.format("id=%d", client_id) if not client then err_message("LSP[", client_name, "] client has shut down after sending the message") @@ -74,11 +76,11 @@ M['window/workDoneProgress/create'] = function(_, _, params, client_id) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest -M['window/showMessageRequest'] = function(_, _, params) +M['window/showMessageRequest'] = function(_, result) - local actions = params.actions - print(params.message) - local option_strings = {params.message, "\nRequest Actions:"} + local actions = result.actions + print(result.message) + local option_strings = {result.message, "\nRequest Actions:"} for i, action in ipairs(actions) do local title = action.title:gsub('\r\n', '\\r\\n') title = title:gsub('\n', '\\n') @@ -95,7 +97,8 @@ M['window/showMessageRequest'] = function(_, _, params) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability -M['client/registerCapability'] = function(_, _, _, client_id) +M['client/registerCapability'] = function(_, _, ctx) + local client_id = ctx.client_id local warning_tpl = "The language server %s triggers a registerCapability ".. "handler despite dynamicRegistration set to false. ".. "Report upstream, this warning is harmless" @@ -107,24 +110,24 @@ M['client/registerCapability'] = function(_, _, _, client_id) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction -M['textDocument/codeAction'] = function(_, _, actions) - if actions == nil or vim.tbl_isempty(actions) then +M['textDocument/codeAction'] = function(_, result) + if result == nil or vim.tbl_isempty(result) then print("No code actions available") return end - local option_strings = {"Code Actions:"} - for i, action in ipairs(actions) do + local option_strings = {"Code actions:"} + for i, action in ipairs(result) do local title = action.title:gsub('\r\n', '\\r\\n') title = title:gsub('\n', '\\n') table.insert(option_strings, string.format("%d. %s", i, title)) end local choice = vim.fn.inputlist(option_strings) - if choice < 1 or choice > #actions then + if choice < 1 or choice > #result then return end - local action_chosen = actions[choice] + local action_chosen = result[choice] -- textDocument/codeAction can return either Command[] or CodeAction[]. -- If it is a CodeAction, it can have either an edit, a command or both. -- Edits should be executed first @@ -141,7 +144,7 @@ M['textDocument/codeAction'] = function(_, _, actions) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit -M['workspace/applyEdit'] = function(_, _, workspace_edit) +M['workspace/applyEdit'] = function(_, workspace_edit) if not workspace_edit then return end -- TODO(ashkan) Do something more with label? if workspace_edit.label then @@ -155,28 +158,29 @@ M['workspace/applyEdit'] = function(_, _, workspace_edit) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration -M['workspace/configuration'] = function(_, _, params, client_id) +M['workspace/configuration'] = function(_, result, ctx) + local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) if not client then err_message("LSP[id=", client_id, "] client has shut down after sending the message") return end - if not params.items then + if not result.items then return {} end - local result = {} - for _, item in ipairs(params.items) do + local response = {} + for _, item in ipairs(result.items) do if item.section then local value = util.lookup_section(client.config.settings, item.section) or vim.NIL -- For empty sections with no explicit '' key, return settings as is if value == vim.NIL and item.section == '' then value = client.config.settings or vim.NIL end - table.insert(result, value) + table.insert(response, value) end end - return result + return response end M['textDocument/publishDiagnostics'] = function(...) @@ -200,16 +204,16 @@ end ---@param map_result function `((resp, bufnr) -> list)` to convert the response ---@param entity name of the resource used in a `not found` error message local function response_to_list(map_result, entity) - return function(_, _, result, _, bufnr, config) + return function(_,result, ctx, config) if not result or vim.tbl_isempty(result) then vim.notify('No ' .. entity .. ' found') else config = config or {} if config.loclist then - util.set_loclist(map_result(result, bufnr)) + util.set_loclist(map_result(result, ctx.bufnr)) api.nvim_command("lopen") else - util.set_qflist(map_result(result, bufnr)) + util.set_qflist(map_result(result, ctx.bufnr)) api.nvim_command("copen") end end @@ -227,25 +231,25 @@ M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'docu M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols') --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename -M['textDocument/rename'] = function(_, _, result) +M['textDocument/rename'] = function(_, result, _) if not result then return end util.apply_workspace_edit(result) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting -M['textDocument/rangeFormatting'] = function(_, _, result, _, bufnr) +M['textDocument/rangeFormatting'] = function(_, result, ctx, _) if not result then return end - util.apply_text_edits(result, bufnr) + util.apply_text_edits(result, ctx.bufnr) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting -M['textDocument/formatting'] = function(_, _, result, _, bufnr) +M['textDocument/formatting'] = function(_, result, ctx, _) if not result then return end - util.apply_text_edits(result, bufnr) + util.apply_text_edits(result, ctx.bufnr) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion -M['textDocument/completion'] = function(_, _, result) +M['textDocument/completion'] = function(_, result, _, _) if vim.tbl_isempty(result or {}) then return end local row, col = unpack(api.nvim_win_get_cursor(0)) local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) @@ -270,9 +274,9 @@ end --- - border: (default=nil) --- - Add borders to the floating window --- - See |vim.api.nvim_open_win()| -function M.hover(_, method, result, _, _, config) +function M.hover(_, result, ctx, config) config = config or {} - config.focus_id = method + config.focus_id = ctx.method if not (result and result.contents) then -- return { 'No information available' } return @@ -292,12 +296,12 @@ M['textDocument/hover'] = M.hover ---@private --- Jumps to a location. Used as a handler for multiple LSP methods. ---@param _ (not used) ----@param method (string) LSP method name ---@param result (table) result of LSP method; a location or a list of locations. +---@param ctx (table) table containing the context of the request, including the method ---(`textDocument/definition` can return `Location` or `Location[]` -local function location_handler(_, method, result) +local function location_handler(_, result, ctx, _) if result == nil or vim.tbl_isempty(result) then - local _ = log.info() and log.info(method, 'No location found') + local _ = log.info() and log.info(ctx.method, 'No location found') return nil end @@ -339,9 +343,9 @@ M['textDocument/implementation'] = location_handler --- - border: (default=nil) --- - Add borders to the floating window --- - See |vim.api.nvim_open_win()| -function M.signature_help(_, method, result, client_id, bufnr, config) +function M.signature_help(_, result, ctx, config) config = config or {} - config.focus_id = method + config.focus_id = ctx.method -- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore if not (result and result.signatures and result.signatures[1]) then @@ -350,9 +354,9 @@ function M.signature_help(_, method, result, client_id, bufnr, config) end return end - local client = vim.lsp.get_client_by_id(client_id) + local client = vim.lsp.get_client_by_id(ctx.client_id) local triggers = client.resolved_capabilities.signature_help_trigger_characters - local ft = api.nvim_buf_get_option(bufnr, 'filetype') + local ft = api.nvim_buf_get_option(ctx.bufnr, 'filetype') local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers) lines = util.trim_empty_lines(lines) if vim.tbl_isempty(lines) then @@ -372,9 +376,9 @@ end M['textDocument/signatureHelp'] = M.signature_help --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight -M['textDocument/documentHighlight'] = function(_, _, result, _, bufnr, _) +M['textDocument/documentHighlight'] = function(_, result, ctx, _) if not result then return end - util.buf_highlight_references(bufnr, result) + util.buf_highlight_references(ctx.bufnr, result) end ---@private @@ -385,7 +389,7 @@ end ---@returns `CallHierarchyIncomingCall[]` if {direction} is `"from"`, ---@returns `CallHierarchyOutgoingCall[]` if {direction} is `"to"`, local make_call_hierarchy_handler = function(direction) - return function(_, _, result) + return function(_, result) if not result then return end local items = {} for _, call_hierarchy_call in pairs(result) do @@ -411,9 +415,10 @@ M['callHierarchy/incomingCalls'] = make_call_hierarchy_handler('from') M['callHierarchy/outgoingCalls'] = make_call_hierarchy_handler('to') --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage -M['window/logMessage'] = function(_, _, result, client_id) +M['window/logMessage'] = function(_, result, ctx, _) local message_type = result.type local message = result.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) if not client then @@ -432,9 +437,10 @@ M['window/logMessage'] = function(_, _, result, client_id) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessage -M['window/showMessage'] = function(_, _, result, client_id) +M['window/showMessage'] = function(_, result, ctx, _) local message_type = result.type local message = result.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) if not client then @@ -451,14 +457,14 @@ end -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do - M[k] = function(err, method, params, client_id, bufnr, config) - local _ = log.debug() and log.debug('default_handler', method, { - params = params, client_id = client_id, err = err, bufnr = bufnr, config = config + M[k] = function(err, result, ctx, config) + local _ = log.debug() and log.debug('default_handler', ctx.method, { + err = err, result = result, ctx=vim.inspect(ctx), config = config }) if err then - local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("client_id=%d", client_id) + local client = vim.lsp.get_client_by_id(ctx.client_id) + local client_name = client and client.name or string.format("client_id=%d", ctx.client_id) -- LSP spec: -- interface ResponseError: -- code: integer; @@ -467,7 +473,7 @@ for k, fn in pairs(M) do return err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message) end - return fn(err, method, params, client_id, bufnr, config) + return fn(err, result, ctx, config) end end diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua new file mode 100644 index 0000000000..855679a2df --- /dev/null +++ b/runtime/lua/vim/lsp/health.lua @@ -0,0 +1,27 @@ +local M = {} + +--- Performs a healthcheck for LSP +function M.check_health() + local report_info = vim.fn['health#report_info'] + local report_warn = vim.fn['health#report_warn'] + + local log = require('vim.lsp.log') + local current_log_level = log.get_level() + local log_level_string = log.levels[current_log_level] + report_info(string.format("LSP log level : %s", log_level_string)) + + if current_log_level < log.levels.WARN then + report_warn(string.format("Log level %s will cause degraded performance and high disk usage", log_level_string)) + end + + local log_path = vim.lsp.get_log_path() + report_info(string.format("Log path: %s", log_path)) + + local log_size = vim.loop.fs_stat(log_path).size + + local report_fn = (log_size / 1000000 > 100 and report_warn or report_info) + report_fn(string.format("Log size: %d KB", log_size / 1000 )) +end + +return M + diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index a2d5c3a774..5d2e396cc5 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -32,6 +32,17 @@ do vim.fn.mkdir(vim.fn.stdpath('cache'), "p") local logfile = assert(io.open(logfilename, "a+")) + + local log_info = vim.loop.fs_stat(logfilename) + if log_info and log_info.size > 1e9 then + local warn_msg = string.format( + "LSP client log is large (%d MB): %s", + log_info.size / (1000 * 1000), + logfilename + ) + vim.notify(warn_msg) + end + -- Start message for logging logfile:write(string.format("[ START ] %s ] LSP logging initiated\n", os.date(log_date_format))) for level, levelnr in pairs(log.levels) do @@ -88,6 +99,11 @@ function log.set_level(level) end end +--- Gets the current log level. +function log.get_level() + return current_log_level +end + --- Checks whether the level is sufficient for logging. ---@param level number log level ---@returns (bool) true if would log, false if not |