aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r--runtime/lua/vim/lsp/buf.lua41
-rw-r--r--runtime/lua/vim/lsp/codelens.lua16
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua207
-rw-r--r--runtime/lua/vim/lsp/handlers.lua154
-rw-r--r--runtime/lua/vim/lsp/log.lua2
-rw-r--r--runtime/lua/vim/lsp/protocol.lua7
-rw-r--r--runtime/lua/vim/lsp/util.lua106
7 files changed, 392 insertions, 141 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index b13d662ccb..29f8d6c3bc 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -126,7 +126,7 @@ local function select_client(method)
if #clients > 1 then
local choices = {}
- for k,v in ipairs(clients) do
+ for k,v in pairs(clients) do
table.insert(choices, string.format("%d %s", k, v.name))
end
local user_choice = vim.fn.confirm(
@@ -204,9 +204,9 @@ 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 ipairs(order or {}) do
+ for _, client_name in pairs(order or {}) do
-- if the client exists, move to the end of the list
- for i, client in ipairs(clients) do
+ for i, client in pairs(clients) do
if client.name == client_name then
table.insert(clients, table.remove(clients, i))
break
@@ -215,7 +215,7 @@ function M.formatting_seq_sync(options, timeout_ms, order)
end
-- loop through the clients and make synchronous formatting requests
- for _, client in ipairs(clients) do
+ 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())
@@ -286,7 +286,7 @@ local function pick_call_hierarchy_item(call_hierarchy_items)
return call_hierarchy_items[1]
end
local items = {}
- for i, item in ipairs(call_hierarchy_items) do
+ 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
@@ -328,8 +328,8 @@ end
---
function M.list_workspace_folders()
local workspace_folders = {}
- for _, client in ipairs(vim.lsp.buf_get_clients()) do
- for _, folder in ipairs(client.workspaceFolders) do
+ for _, client in pairs(vim.lsp.buf_get_clients()) do
+ for _, folder in pairs(client.workspaceFolders) do
table.insert(workspace_folders, folder.name)
end
end
@@ -347,9 +347,9 @@ function M.add_workspace_folder(workspace_folder)
return
end
local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}})
- for _, client in ipairs(vim.lsp.buf_get_clients()) do
+ for _, client in pairs(vim.lsp.buf_get_clients()) do
local found = false
- for _, folder in ipairs(client.workspaceFolders) do
+ for _, folder in pairs(client.workspaceFolders) do
if folder.name == workspace_folder then
found = true
print(workspace_folder, "is already part of this workspace")
@@ -371,8 +371,8 @@ function M.remove_workspace_folder(workspace_folder)
vim.api.nvim_command("redraw")
if not (workspace_folder and #workspace_folder > 0) then return end
local params = util.make_workspace_params({{}}, {{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}})
- for _, client in ipairs(vim.lsp.buf_get_clients()) do
- for idx, folder in ipairs(client.workspaceFolders) do
+ for _, client in pairs(vim.lsp.buf_get_clients()) do
+ for idx, folder in pairs(client.workspaceFolders) do
if folder.name == workspace_folder then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
client.workspaceFolders[idx] = nil
@@ -422,6 +422,21 @@ function M.clear_references()
util.buf_clear_references()
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)
+ local actions = {}
+ for _, r in pairs(results) do
+ vim.list_extend(actions, r.result or {})
+ end
+ vim.lsp.handlers[method](nil, method, actions, nil, bufnr)
+ end)
+end
+
--- Selects a code action from the input list that is available at the current
--- cursor position.
--
@@ -432,7 +447,7 @@ function M.code_action(context)
context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
local params = util.make_range_params()
params.context = context
- request('textDocument/codeAction', params)
+ code_action_request(params)
end
--- Performs |vim.lsp.buf.code_action()| for a given range.
@@ -447,7 +462,7 @@ function M.range_code_action(context, start_pos, end_pos)
context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
local params = util.make_given_range_params(start_pos, end_pos)
params.context = context
- request('textDocument/codeAction', params)
+ code_action_request(params)
end
--- Executes an LSP server command.
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index fbd37e3830..fe581e42ad 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -111,15 +111,19 @@ function M.display(lenses, bufnr, client_id)
local ns = namespaces[client_id]
local num_lines = api.nvim_buf_line_count(bufnr)
for i = 0, num_lines do
- local line_lenses = lenses_by_lnum[i]
+ local line_lenses = lenses_by_lnum[i] or {}
api.nvim_buf_clear_namespace(bufnr, ns, i, i + 1)
local chunks = {}
- for _, lens in pairs(line_lenses or {}) do
+ local num_line_lenses = #line_lenses
+ for j, lens in ipairs(line_lenses) do
local text = lens.command and lens.command.title or 'Unresolved lens ...'
table.insert(chunks, {text, 'LspCodeLens' })
+ if j < num_line_lenses then
+ table.insert(chunks, {' | ', 'LspCodeLensSeparator' })
+ end
end
if #chunks > 0 then
- api.nvim_buf_set_virtual_text(bufnr, ns, i, chunks, {})
+ api.nvim_buf_set_extmark(bufnr, ns, i, 0, { virt_text = chunks })
end
end
end
@@ -175,12 +179,12 @@ local function resolve_lenses(lenses, bufnr, client_id, callback)
-- Eager display to have some sort of incremental feedback
-- Once all lenses got resolved there will be a full redraw for all lenses
-- So that multiple lens per line are properly displayed
- api.nvim_buf_set_virtual_text(
+ api.nvim_buf_set_extmark(
bufnr,
ns,
lens.range.start.line,
- {{ lens.command.title, 'LspCodeLens' },},
- {}
+ 0,
+ { virt_text = {{ lens.command.title, 'LspCodeLens' }} }
)
end
countdown()
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 64dde78f17..5efd8d74a7 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -203,8 +203,13 @@ local bufnr_and_client_cacher_mt = {
-- Diagnostic Saving & Caching {{{
local _diagnostic_cleanup = setmetatable({}, bufnr_and_client_cacher_mt)
local diagnostic_cache = setmetatable({}, bufnr_and_client_cacher_mt)
+local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_client_cacher_mt)
local diagnostic_cache_lines = setmetatable({}, bufnr_and_client_cacher_mt)
local diagnostic_cache_counts = setmetatable({}, bufnr_and_client_cacher_mt)
+local diagnostic_attached_buffers = {}
+
+-- Disabled buffers and clients
+local diagnostic_disabled = setmetatable({}, bufnr_and_client_cacher_mt)
local _bufs_waiting_to_update = setmetatable({}, bufnr_and_client_cacher_mt)
@@ -761,17 +766,20 @@ function M.set_virtual_text(diagnostics, bufnr, client_id, diagnostic_ns, opts)
local virt_texts = M.get_virtual_text_chunks_for_line(bufnr, line, line_diagnostics, opts)
if virt_texts then
- api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {})
+ api.nvim_buf_set_extmark(bufnr, diagnostic_ns, line, 0, {
+ virt_text = virt_texts,
+ })
end
end
end
---- Default function to get text chunks to display using `nvim_buf_set_virtual_text`.
+--- Default function to get text chunks to display using |nvim_buf_set_extmark()|.
---@param bufnr number The buffer to display the virtual text in
---@param line number The line number to display the virtual text on
---@param line_diags Diagnostic[] The diagnostics associated with the line
---@param opts table See {opts} from |vim.lsp.diagnostic.set_virtual_text()|
----@return table chunks, as defined by |nvim_buf_set_virtual_text()|
+---@return an array of [text, hl_group] arrays. This can be passed directly to
+--- the {virt_text} option of |nvim_buf_set_extmark()|.
function M.get_virtual_text_chunks_for_line(bufnr, line, line_diags, opts)
assert(bufnr or line)
@@ -814,10 +822,7 @@ end
---@param diagnostic_ns number|nil Associated diagnostic namespace
---@param sign_ns number|nil Associated sign namespace
function M.clear(bufnr, client_id, diagnostic_ns, sign_ns)
- validate { bufnr = { bufnr, 'n' } }
-
- bufnr = (bufnr == 0 and api.nvim_get_current_buf()) or bufnr
-
+ bufnr = get_bufnr(bufnr)
if client_id == nil then
return vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _)
return M.clear(bufnr, iter_client_id)
@@ -826,6 +831,7 @@ function M.clear(bufnr, client_id, diagnostic_ns, sign_ns)
diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
sign_ns = sign_ns or M._get_sign_namespace(client_id)
+ diagnostic_cache_extmarks[bufnr][client_id] = {}
assert(bufnr, "bufnr is required")
assert(diagnostic_ns, "Need diagnostic_ns, got nil")
@@ -1038,9 +1044,61 @@ function M.on_publish_diagnostics(_, _, params, client_id, _, config)
M.display(diagnostics, bufnr, client_id, config)
end
+-- restores the extmarks set by M.display
+--- @param last number last line that was changed
+-- @private
+local function restore_extmarks(bufnr, last)
+ for client_id, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do
+ local ns = M._get_diagnostic_namespace(client_id)
+ local extmarks_current = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true})
+ local found = {}
+ for _, extmark in ipairs(extmarks_current) do
+ -- nvim_buf_set_lines will move any extmark to the line after the last
+ -- nvim_buf_set_text will move any extmark to the last line
+ if extmark[2] ~= last + 1 then
+ found[extmark[1]] = true
+ end
+ end
+ for _, extmark in ipairs(extmarks) do
+ if not found[extmark[1]] then
+ local opts = extmark[4]
+ opts.id = extmark[1]
+ -- HACK: end_row should be end_line
+ if opts.end_row then
+ opts.end_line = opts.end_row
+ opts.end_row = nil
+ end
+ pcall(api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
+ end
+ end
+ end
+end
+
+-- caches the extmarks set by M.display
+-- @private
+local function save_extmarks(bufnr, client_id)
+ bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
+ if not diagnostic_attached_buffers[bufnr] then
+ api.nvim_buf_attach(bufnr, false, {
+ on_lines = function(_, _, _, _, _, last)
+ restore_extmarks(bufnr, last - 1)
+ end,
+ on_detach = function()
+ diagnostic_cache_extmarks[bufnr] = nil
+ end})
+ diagnostic_attached_buffers[bufnr] = true
+ end
+ local ns = M._get_diagnostic_namespace(client_id)
+ diagnostic_cache_extmarks[bufnr][client_id] = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true})
+end
+
--@private
--- Display diagnostics for the buffer, given a configuration.
function M.display(diagnostics, bufnr, client_id, config)
+ if diagnostic_disabled[bufnr][client_id] then
+ return
+ end
+
config = vim.lsp._with_extend('vim.lsp.diagnostic.on_publish_diagnostics', {
signs = true,
underline = true,
@@ -1108,7 +1166,45 @@ function M.display(diagnostics, bufnr, client_id, config)
if signs_opts then
M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts)
end
+
+ -- cache extmarks
+ save_extmarks(bufnr, client_id)
+end
+
+--- Redraw diagnostics for the given buffer and client
+---
+--- This calls the "textDocument/publishDiagnostics" handler manually using
+--- the cached diagnostics already received from the server. This can be useful
+--- for redrawing diagnostics after making changes in diagnostics
+--- configuration. |lsp-handler-configuration|
+---
+--- @param bufnr (optional, number): Buffer handle, defaults to current
+--- @param client_id (optional, number): Redraw diagnostics for the given
+--- client. The default is to redraw diagnostics for all attached
+--- clients.
+function M.redraw(bufnr, client_id)
+ bufnr = get_bufnr(bufnr)
+ if not client_id then
+ return vim.lsp.for_each_buffer_client(bufnr, function(client)
+ M.redraw(bufnr, client.id)
+ end)
+ end
+
+ -- We need to invoke the publishDiagnostics handler directly instead of just
+ -- calling M.display so that we can preserve any custom configuration options
+ -- 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
+
-- }}}
-- Diagnostic User Functions {{{
@@ -1156,7 +1252,7 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
table.insert(lines, prefix..message_lines[1])
table.insert(highlights, {#prefix, hiname})
for j = 2, #message_lines do
- table.insert(lines, message_lines[j])
+ table.insert(lines, string.rep(' ', #prefix) .. message_lines[j])
table.insert(highlights, {0, hiname})
end
end
@@ -1190,10 +1286,10 @@ function M.reset(client_id, buffer_client_map)
end)
end
---- Sets the location list
+--- 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
---@param opts table|nil Configuration table. Keys:
---- - {open_loclist}: (boolean, default true)
---- - Open loclist after set
--- - {client_id}: (number)
--- - If nil, will consider all clients attached to buffer.
--- - {severity}: (DiagnosticSeverity)
@@ -1202,9 +1298,8 @@ end
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
--- - {workspace}: (boolean, default false)
--- - Set the list with workspace diagnostics
-function M.set_loclist(opts)
+local function apply_to_diagnostic_items(item_handler, command, opts)
opts = opts or {}
- local open_loclist = if_nil(opts.open_loclist, true)
local current_bufnr = api.nvim_get_current_buf()
local diags = opts.workspace and M.get_all(opts.client_id) or {
[current_bufnr] = M.get(current_bufnr, opts.client_id)
@@ -1221,11 +1316,89 @@ function M.set_loclist(opts)
return true
end
local items = util.diagnostics_to_items(diags, predicate)
- local win_id = vim.api.nvim_get_current_win()
- util.set_loclist(items, win_id)
- if open_loclist then
- vim.cmd [[lopen]]
+ item_handler(items)
+ if command then
+ vim.cmd(command)
+ end
+end
+
+--- Sets the quickfix list
+---@param opts table|nil Configuration table. Keys:
+--- - {open}: (boolean, default true)
+--- - Open quickfix list after set
+--- - {client_id}: (number)
+--- - If nil, will consider all clients attached to buffer.
+--- - {severity}: (DiagnosticSeverity)
+--- - Exclusive severity to consider. Overrides {severity_limit}
+--- - {severity_limit}: (DiagnosticSeverity)
+--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
+--- - {workspace}: (boolean, default true)
+--- - Set the list with workspace diagnostics
+function M.set_qflist(opts)
+ opts = opts or {}
+ opts.workspace = if_nil(opts.workspace, true)
+ local open_qflist = if_nil(opts.open, true)
+ local command = open_qflist and [[copen]] or nil
+ apply_to_diagnostic_items(util.set_qflist, command, opts)
+end
+
+--- Sets the location list
+---@param opts table|nil Configuration table. Keys:
+--- - {open}: (boolean, default true)
+--- - Open loclist after set
+--- - {client_id}: (number)
+--- - If nil, will consider all clients attached to buffer.
+--- - {severity}: (DiagnosticSeverity)
+--- - Exclusive severity to consider. Overrides {severity_limit}
+--- - {severity_limit}: (DiagnosticSeverity)
+--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
+--- - {workspace}: (boolean, default false)
+--- - Set the list with workspace diagnostics
+function M.set_loclist(opts)
+ opts = opts or {}
+ local open_loclist = if_nil(opts.open, true)
+ local command = open_loclist and [[lopen]] or nil
+ apply_to_diagnostic_items(util.set_loclist, command, opts)
+end
+
+--- Disable diagnostics for the given buffer and client
+--- @param bufnr (optional, number): Buffer handle, defaults to current
+--- @param client_id (optional, number): Disable diagnostics for the given
+--- client. The default is to disable diagnostics for all attached
+--- clients.
+-- Note that when diagnostics are disabled for a buffer, the server will still
+-- send diagnostic information and the client will still process it. The
+-- diagnostics are simply not displayed to the user.
+function M.disable(bufnr, client_id)
+ if not client_id then
+ return vim.lsp.for_each_buffer_client(bufnr, function(client)
+ M.disable(bufnr, client.id)
+ end)
+ end
+
+ diagnostic_disabled[bufnr][client_id] = true
+ M.clear(bufnr, client_id)
+end
+
+--- Enable diagnostics for the given buffer and client
+--- @param bufnr (optional, number): Buffer handle, defaults to current
+--- @param client_id (optional, number): Enable diagnostics for the given
+--- client. The default is to enable diagnostics for all attached
+--- clients.
+function M.enable(bufnr, client_id)
+ if not client_id then
+ return vim.lsp.for_each_buffer_client(bufnr, function(client)
+ M.enable(bufnr, client.id)
+ end)
end
+
+ if not diagnostic_disabled[bufnr][client_id] then
+ return
+ end
+
+ diagnostic_disabled[bufnr][client_id] = nil
+
+ M.redraw(bufnr, client_id)
end
-- }}}
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 41852b9d88..b7f1ea0ab6 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -17,15 +17,12 @@ local function err_message(...)
api.nvim_command("redraw")
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
-M['workspace/executeCommand'] = function(err, _)
- if err then
- error("Could not execute code action: "..err.message)
- end
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
+M['workspace/executeCommand'] = function()
+ -- Error handling is done implicitly by wrapping all handlers; see end of this file
end
--- @msg of type ProgressParams
--- Basically a token of type number/string
+--@private
local function progress_handler(_, _, params, 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)
@@ -61,10 +58,10 @@ local function progress_handler(_, _, params, client_id)
vim.api.nvim_command("doautocmd <nomodeline> User LspProgressUpdate")
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
M['$/progress'] = progress_handler
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create
M['window/workDoneProgress/create'] = function(_, _, params, client_id)
local client = vim.lsp.get_client_by_id(client_id)
local token = params.token -- string or number
@@ -76,7 +73,7 @@ M['window/workDoneProgress/create'] = function(_, _, params, client_id)
return vim.NIL
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
M['window/showMessageRequest'] = function(_, _, params)
local actions = params.actions
@@ -97,7 +94,7 @@ M['window/showMessageRequest'] = function(_, _, params)
end
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
M['client/registerCapability'] = function(_, _, _, client_id)
local warning_tpl = "The language server %s triggers a registerCapability "..
"handler despite dynamicRegistration set to false. "..
@@ -109,7 +106,7 @@ M['client/registerCapability'] = function(_, _, _, client_id)
return vim.NIL
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
+--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
print("No code actions available")
@@ -143,7 +140,7 @@ M['textDocument/codeAction'] = function(_, _, actions)
end
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
M['workspace/applyEdit'] = function(_, _, workspace_edit)
if not workspace_edit then return end
-- TODO(ashkan) Do something more with label?
@@ -157,14 +154,13 @@ M['workspace/applyEdit'] = function(_, _, workspace_edit)
}
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration
-M['workspace/configuration'] = function(err, _, params, client_id)
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration
+M['workspace/configuration'] = function(_, _, params, 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 err then error(vim.inspect(err)) end
if not params.items then
return {}
end
@@ -191,50 +187,64 @@ M['textDocument/codeLens'] = function(...)
return require('vim.lsp.codelens').on_codelens(...)
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
-M['textDocument/references'] = function(_, _, result)
- if not result then return end
- util.set_qflist(util.locations_to_items(result))
- api.nvim_command("copen")
-end
---@private
---- Prints given list of symbols to the quickfix list.
---@param _ (not used)
---@param _ (not used)
---@param result (list of Symbols) LSP method name
---@param result (table) result of LSP method; a location or a list of locations.
----(`textDocument/definition` can return `Location` or `Location[]`
-local symbol_handler = function(_, _, result, _, bufnr)
- if not result or vim.tbl_isempty(result) then return end
- util.set_qflist(util.symbols_to_items(result, bufnr))
- api.nvim_command("copen")
+--@private
+--- Return a function that converts LSP responses to list items and opens the list
+---
+--- The returned function has an optional {config} parameter that accepts a table
+--- with the following keys:
+---
+--- loclist: (boolean) use the location list (default is to use the quickfix list)
+---
+--- @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)
+ 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))
+ api.nvim_command("lopen")
+ else
+ util.set_qflist(map_result(result, bufnr))
+ api.nvim_command("copen")
+ end
+ end
+ end
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
-M['textDocument/documentSymbol'] = symbol_handler
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
-M['workspace/symbol'] = symbol_handler
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename
+
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
+M['textDocument/references'] = response_to_list(util.locations_to_items, 'references')
+
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
+M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols')
+
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
+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)
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)
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting
+M['textDocument/rangeFormatting'] = function(_, _, result, _, bufnr)
if not result then return end
- util.apply_text_edits(result)
+ util.apply_text_edits(result, bufnr)
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
-M['textDocument/formatting'] = function(_, _, result)
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
+M['textDocument/formatting'] = function(_, _, result, _, bufnr)
if not result then return end
- util.apply_text_edits(result)
+ util.apply_text_edits(result, bufnr)
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
M['textDocument/completion'] = function(_, _, result)
if vim.tbl_isempty(result or {}) then return end
local row, col = unpack(api.nvim_win_get_cursor(0))
@@ -276,7 +286,7 @@ function M.hover(_, method, result, _, _, config)
return util.open_floating_preview(markdown_lines, "markdown", config)
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
M['textDocument/hover'] = M.hover
--@private
@@ -306,16 +316,17 @@ local function location_handler(_, method, result)
end
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration
M['textDocument/declaration'] = location_handler
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
M['textDocument/definition'] = location_handler
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition
M['textDocument/typeDefinition'] = location_handler
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
M['textDocument/implementation'] = location_handler
---- |lsp-handler| for the method "textDocument/signatureHelp"
+--- |lsp-handler| for the method "textDocument/signatureHelp".
+--- The active parameter is highlighted with |hl-LspSignatureActiveParameter|.
--- <pre>
--- vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
--- vim.lsp.handlers.signature_help, {
@@ -328,29 +339,39 @@ 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, _, bufnr, config)
+function M.signature_help(_, method, result, client_id, bufnr, config)
config = config or {}
config.focus_id = 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
- print('No signature help available')
+ if config.silent ~= true then
+ print('No signature help available')
+ end
return
end
+ local client = vim.lsp.get_client_by_id(client_id)
+ local triggers = client.resolved_capabilities.signature_help_trigger_characters
local ft = api.nvim_buf_get_option(bufnr, 'filetype')
- local lines = util.convert_signature_help_to_markdown_lines(result, ft)
+ 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
- print('No signature help available')
+ if config.silent ~= true then
+ print('No signature help available')
+ end
return
end
- return util.open_floating_preview(lines, "markdown", config)
+ local fbuf, fwin = util.open_floating_preview(lines, "markdown", config)
+ if hl then
+ api.nvim_buf_add_highlight(fbuf, -1, "LspSignatureActiveParameter", 0, unpack(hl))
+ end
+ return fbuf, fwin
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
M['textDocument/signatureHelp'] = M.signature_help
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
M['textDocument/documentHighlight'] = function(_, _, result, _, bufnr, _)
if not result then return end
util.buf_highlight_references(bufnr, result)
@@ -383,13 +404,13 @@ local make_call_hierarchy_handler = function(direction)
end
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/incomingCalls
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_incomingCalls
M['callHierarchy/incomingCalls'] = make_call_hierarchy_handler('from')
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/outgoingCalls
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_outgoingCalls
M['callHierarchy/outgoingCalls'] = make_call_hierarchy_handler('to')
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/logMessage
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage
M['window/logMessage'] = function(_, _, result, client_id)
local message_type = result.type
local message = result.message
@@ -410,7 +431,7 @@ M['window/logMessage'] = function(_, _, result, client_id)
return result
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/showMessage
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessage
M['window/showMessage'] = function(_, _, result, client_id)
local message_type = result.type
local message = result.message
@@ -436,7 +457,14 @@ for k, fn in pairs(M) do
})
if err then
- return err_message(tostring(err))
+ 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)
+ -- LSP spec:
+ -- interface ResponseError:
+ -- code: integer;
+ -- message: string;
+ -- data?: string | number | boolean | array | object | null;
+ return err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message)
end
return fn(err, method, params, client_id, bufnr, config)
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
index 471a311c16..73fafb9715 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -17,7 +17,7 @@ local current_log_level = log.levels.WARN
local log_date_format = "%FT%H:%M:%S%z"
do
- local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/"
+ local path_sep = vim.loop.os_uname().version:match("Windows") and "\\" or "/"
--@private
local function path_join(...)
return table.concat(vim.tbl_flatten{...}, path_sep)
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 7e43eb84de..6d02b9ba74 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -691,10 +691,11 @@ function protocol.make_client_capabilities()
signatureHelp = {
dynamicRegistration = false;
signatureInformation = {
+ activeParameterSupport = true;
documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
- -- parameterInformation = {
- -- labelOffsetSupport = false;
- -- };
+ parameterInformation = {
+ labelOffsetSupport = true;
+ };
};
};
references = {
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 195e3a0e65..d682fdc17e 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -240,6 +240,7 @@ end
--- Applies a list of text edits to a buffer.
--@param text_edits (table) list of `TextEdit` objects
--@param buf_nr (number) Buffer id
+---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
function M.apply_text_edits(text_edits, bufnr)
if not next(text_edits) then return end
if not api.nvim_buf_is_loaded(bufnr) then
@@ -810,16 +811,16 @@ function M.convert_input_to_markdown_lines(input, contents)
-- If it's plaintext, then wrap it in a <text></text> block
-- Some servers send input.value as empty, so let's ignore this :(
- input.value = input.value or ''
+ local value = input.value or ''
if input.kind == "plaintext" then
-- wrap this in a <text></text> block so that stylize_markdown
-- can properly process it as plaintext
- input.value = string.format("<text>\n%s\n</text>", input.value or "")
+ value = string.format("<text>\n%s\n</text>", value)
end
- -- assert(type(input.value) == 'string')
- list_extend(contents, split_lines(input.value))
+ -- assert(type(value) == 'string')
+ list_extend(contents, split_lines(value))
-- MarkupString variation 2
elseif input.language then
-- Some servers send input.value as empty, so let's ignore this :(
@@ -845,9 +846,10 @@ end
---
--@param signature_help Response of `textDocument/SignatureHelp`
--@param ft optional filetype that will be use as the `lang` for the label markdown code block
+--@param triggers optional list of trigger characters from the lsp server. used to better determine parameter offsets
--@returns list of lines of converted markdown.
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
-function M.convert_signature_help_to_markdown_lines(signature_help, ft)
+function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers)
if not signature_help.signatures then
return
end
@@ -856,6 +858,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft)
--=== 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 active_signature = signature_help.activeSignature or 0
-- If the activeSignature is not inside the valid range, then clip it.
if active_signature >= #signature_help.signatures then
@@ -875,11 +878,17 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft)
M.convert_input_to_markdown_lines(signature.documentation, contents)
end
if signature.parameters and #signature.parameters > 0 then
- local active_parameter = signature_help.activeParameter or 0
- -- If the activeParameter is not inside the valid range, then clip it.
+ local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0)
+ if active_parameter < 0
+ then active_parameter = 0
+ end
+
+ -- 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 = 0
+ active_parameter = #signature.parameters - 1
end
+
local parameter = signature.parameters[active_parameter + 1]
if parameter then
--[=[
@@ -900,13 +909,35 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft)
documentation?: string | MarkupContent;
}
--]=]
- -- TODO highlight parameter
+ 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
+ end
+ end
if parameter.documentation then
M.convert_input_to_markdown_lines(parameter.documentation, contents)
end
end
end
- return contents
+ return contents, active_hl
end
--- Creates a table with sensible default options for a floating window. The
@@ -942,7 +973,7 @@ function M.make_floating_popup_options(width, height, opts)
row = -get_border_size(opts).height
end
- if vim.fn.wincol() + width <= api.nvim_get_option('columns') then
+ if vim.fn.wincol() + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then
anchor = anchor..'W'
col = 0
else
@@ -960,6 +991,7 @@ function M.make_floating_popup_options(width, height, opts)
style = 'minimal',
width = width,
border = opts.border or default_border,
+ zindex = opts.zindex or 50,
}
end
@@ -1129,6 +1161,8 @@ function M.stylize_markdown(bufnr, contents, opts)
-- Clean up
contents = M._trim(contents, opts)
+ -- Insert blank line separator after code block?
+ local add_sep = opts.separator == nil and true or opts.separator
local stripped = {}
local highlights = {}
-- keep track of lnums that contain markdown
@@ -1155,9 +1189,24 @@ function M.stylize_markdown(bufnr, contents, opts)
start = start + 1;
finish = #stripped;
})
+ -- add a separator, but not on the last line
+ if add_sep and i < #contents then
+ table.insert(stripped, "---")
+ markdown_lines[#stripped] = true
+ end
else
- table.insert(stripped, line)
- markdown_lines[#stripped] = true
+ -- strip any emty 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
@@ -1165,7 +1214,7 @@ function M.stylize_markdown(bufnr, contents, opts)
-- 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))
- local width, height = M._make_floating_popup_size(stripped, opts)
+ local width = M._make_floating_popup_size(stripped, opts)
local sep_line = string.rep("─", math.min(width, opts.wrap_at or width))
@@ -1175,26 +1224,6 @@ function M.stylize_markdown(bufnr, contents, opts)
end
end
- -- Insert blank line separator after code block
- local insert_separator = opts.separator
- if insert_separator == nil then insert_separator = true end
- if insert_separator then
- local offset = 0
- for _, h in ipairs(highlights) do
- h.start = h.start + offset
- h.finish = h.finish + offset
- -- check if a seperator already exists and use that one instead of creating a new one
- if h.finish + 1 <= #stripped then
- if stripped[h.finish + 1] ~= sep_line then
- table.insert(stripped, h.finish + 1, sep_line)
- offset = offset + 1
- height = height + 1
- end
- end
- end
- end
-
-
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
local idx = 1
@@ -1455,6 +1484,7 @@ do --[[ References ]]
---
--@param bufnr buffer id
--@param references List of `DocumentHighlight` objects to highlight
+ ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight
function M.buf_highlight_references(bufnr, references)
validate { bufnr = {bufnr, 'n', true} }
for _, reference in ipairs(references) do
@@ -1689,14 +1719,14 @@ end
function M.trim_empty_lines(lines)
local start = 1
for i = 1, #lines do
- if #lines[i] > 0 then
+ if lines[i] ~= nil and #lines[i] > 0 then
start = i
break
end
end
local finish = 1
for i = #lines, 1, -1 do
- if #lines[i] > 0 then
+ if lines[i] ~= nil and #lines[i] > 0 then
finish = i
break
end
@@ -1838,10 +1868,10 @@ function M.get_effective_tabstop(bufnr)
return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop
end
---- Creates a `FormattingOptions` object for the current buffer and cursor position.
+--- Creates a `DocumentFormattingParams` object for the current buffer and cursor position.
---
--@param options Table with valid `FormattingOptions` entries
---@returns `FormattingOptions object
+--@returns `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} }