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.lua165
-rw-r--r--runtime/lua/vim/lsp/codelens.lua236
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua469
-rw-r--r--runtime/lua/vim/lsp/handlers.lua264
-rw-r--r--runtime/lua/vim/lsp/health.lua27
-rw-r--r--runtime/lua/vim/lsp/log.lua28
-rw-r--r--runtime/lua/vim/lsp/protocol.lua20
-rw-r--r--runtime/lua/vim/lsp/rpc.lua145
-rw-r--r--runtime/lua/vim/lsp/util.lua562
9 files changed, 1269 insertions, 647 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 5dd7109bb0..8bfcd90f12 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -5,7 +5,7 @@ local util = require 'vim.lsp.util'
local M = {}
---@private
+---@private
--- Returns nil if {status} is false or nil, otherwise returns the rest of the
--- arguments.
local function ok_or_nil(status, ...)
@@ -13,31 +13,31 @@ local function ok_or_nil(status, ...)
return ...
end
---@private
+---@private
--- Swallows errors.
---
---@param fn Function to run
---@param ... Function arguments
---@returns Result of `fn(...)` if there are no errors, otherwise nil.
+---@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
+---@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|
+---@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:
+---@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()|
+---@see |vim.lsp.buf_request()|
local function request(method, params, handler)
validate {
method = {method, 's'};
@@ -49,7 +49,7 @@ end
--- Checks whether the language servers attached to the current buffer are
--- ready.
---
---@returns `true` if server responds.
+---@returns `true` if server responds.
function M.server_ready()
return not not vim.lsp.buf_notify(0, "window/progress", {})
end
@@ -62,7 +62,7 @@ function M.hover()
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.
+---@note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead.
---
function M.declaration()
local params = util.make_position_params()
@@ -100,22 +100,22 @@ 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
+---@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|
+---@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
+---@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
+---@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)
@@ -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(
@@ -146,17 +146,17 @@ end
--- Formats the current buffer.
---
---@param options (optional, table) Can be used to specify FormattingOptions.
+---@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
+---@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)
+ return client.request("textDocument/formatting", params, nil, vim.api.nvim_get_current_buf())
end
--- Performs |vim.lsp.buf.formatting()| synchronously.
@@ -168,15 +168,15 @@ end
--- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()]]
--- </pre>
---
---@param options Table with valid `FormattingOptions` entries
---@param timeout_ms (number) Request timeout
---@see |vim.lsp.buf.formatting_seq_sync|
+---@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)
+ 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
@@ -195,18 +195,18 @@ end
--- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]]
--- </pre>
---
---@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
+---@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 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,10 +215,10 @@ 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)
+ 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
@@ -230,10 +230,10 @@ end
--- Formats a given range.
---
---@param options Table with valid `FormattingOptions` entries.
---@param start_pos ({number, number}, optional) mark-indexed position.
+---@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.
+---@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")
@@ -246,22 +246,43 @@ 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
+---@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.
---
---@param context (table) Context for the request
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
+---@param context (table) Context for the request
+---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
function M.references(context)
validate { context = { context, 't', true } }
local params = util.make_position_params()
@@ -279,14 +300,14 @@ function M.document_symbol()
request('textDocument/documentSymbol', params)
end
---@private
+---@private
local function pick_call_hierarchy_item(call_hierarchy_items)
if not call_hierarchy_items then return end
if #call_hierarchy_items == 1 then
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
@@ -297,6 +318,7 @@ local function pick_call_hierarchy_item(call_hierarchy_items)
return choice
end
+---@private
local function call_hierarchy(method)
local params = util.make_position_params()
request('textDocument/prepareCallHierarchy', params, function(err, _, result)
@@ -327,8 +349,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
@@ -338,7 +360,7 @@ end
--- Add the folder at path to the workspace folders. If {path} is
--- not provided, the user will be prompted for a path using |input()|.
function M.add_workspace_folder(workspace_folder)
- workspace_folder = workspace_folder or npcall(vfn.input, "Workspace Folder: ", vfn.expand('%:p:h'))
+ workspace_folder = workspace_folder or npcall(vfn.input, "Workspace Folder: ", vfn.expand('%:p:h'), 'dir')
vim.api.nvim_command("redraw")
if not (workspace_folder and #workspace_folder > 0) then return end
if vim.fn.isdirectory(workspace_folder) == 0 then
@@ -346,9 +368,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")
@@ -370,8 +392,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
@@ -388,7 +410,7 @@ end
--- call, the user is prompted to enter a string on the command line. An empty
--- string means no filtering is done.
---
---@param query (string, optional)
+---@param query (string, optional)
function M.workspace_symbol(query)
query = query or npcall(vfn.input, "Query: ")
local params = {query = query}
@@ -421,38 +443,53 @@ 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, actions, {bufnr=bufnr, method=method})
+ end)
+end
+
--- Selects a code action from the input list that is available at the current
--- cursor position.
---
---@param context: (table, optional) Valid `CodeActionContext` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
+---
+---@param context: (table, optional) Valid `CodeActionContext` object
+---@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 { 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.
---
---@param context: (table, optional) Valid `CodeActionContext` object
---@param start_pos ({number, number}, optional) mark-indexed position.
+---@param context: (table, optional) Valid `CodeActionContext` object
+---@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.
+---@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 { 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.
---
---@param command A valid `ExecuteCommandParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
+---@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' },
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
new file mode 100644
index 0000000000..9cedb2f1db
--- /dev/null
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -0,0 +1,236 @@
+local util = require('vim.lsp.util')
+local api = vim.api
+local M = {}
+
+--- bufnr → true|nil
+--- to throttle refreshes to at most one at a time
+local active_refreshes = {}
+
+--- bufnr -> client_id -> lenses
+local lens_cache_by_buf = setmetatable({}, {
+ __index = function(t, b)
+ local key = b > 0 and b or api.nvim_get_current_buf()
+ return rawget(t, key)
+ end
+})
+
+local namespaces = setmetatable({}, {
+ __index = function(t, key)
+ local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key)
+ rawset(t, key, value)
+ return value
+ end;
+})
+
+---@private
+M.__namespaces = namespaces
+
+
+---@private
+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)
+
+ -- 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)
+ client.request('workspace/executeCommand', lens.command, function(...)
+ local result = vim.lsp.handlers['workspace/executeCommand'](...)
+ M.refresh()
+ return result
+ end, bufnr)
+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 or 0]
+ if not lenses_by_client then return {} end
+ local lenses = {}
+ for _, client_lenses in pairs(lenses_by_client) do
+ vim.list_extend(lenses, client_lenses)
+ end
+ return lenses
+end
+
+
+--- Run the code lens in the current line
+---
+function M.run()
+ local line = api.nvim_win_get_cursor(0)[1]
+ local bufnr = api.nvim_get_current_buf()
+ local options = {}
+ local lenses_by_client = lens_cache_by_buf[bufnr] or {}
+ for client, lenses in pairs(lenses_by_client) do
+ for _, lens in pairs(lenses) do
+ if lens.range.start.line == (line - 1) then
+ table.insert(options, {client=client, lens=lens})
+ end
+ end
+ end
+ if #options == 0 then
+ vim.notify('No executable codelens found at current line')
+ elseif #options == 1 then
+ local option = options[1]
+ execute_lens(option.lens, bufnr, option.client)
+ else
+ local options_strings = {"Code lenses:"}
+ for i, option in ipairs(options) do
+ table.insert(options_strings, string.format('%d. %s', i, option.lens.command.title))
+ end
+ local choice = vim.fn.inputlist(options_strings)
+ if choice < 1 or choice > #options then
+ return
+ end
+ local option = options[choice]
+ execute_lens(option.lens, bufnr, option.client)
+ end
+end
+
+
+--- Display the lenses using virtual text
+---
+---@param lenses table of lenses to display (`CodeLens[] | null`)
+---@param bufnr number
+---@param client_id number
+function M.display(lenses, bufnr, client_id)
+ if not lenses or not next(lenses) then
+ return
+ end
+ local lenses_by_lnum = {}
+ for _, lens in pairs(lenses) do
+ local line_lenses = lenses_by_lnum[lens.range.start.line]
+ if not line_lenses then
+ line_lenses = {}
+ lenses_by_lnum[lens.range.start.line] = line_lenses
+ end
+ table.insert(line_lenses, lens)
+ end
+ 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] or {}
+ api.nvim_buf_clear_namespace(bufnr, ns, i, i + 1)
+ local chunks = {}
+ 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_extmark(bufnr, ns, i, 0, { virt_text = chunks })
+ end
+ end
+end
+
+
+--- Store lenses for a specific buffer and client
+---
+---@param lenses table of lenses to store (`CodeLens[] | null`)
+---@param bufnr number
+---@param client_id number
+function M.save(lenses, bufnr, client_id)
+ local lenses_by_client = lens_cache_by_buf[bufnr]
+ if not lenses_by_client then
+ lenses_by_client = {}
+ lens_cache_by_buf[bufnr] = lenses_by_client
+ local ns = namespaces[client_id]
+ api.nvim_buf_attach(bufnr, false, {
+ on_detach = function(b) lens_cache_by_buf[b] = nil end,
+ on_lines = function(_, b, _, first_lnum, last_lnum)
+ api.nvim_buf_clear_namespace(b, ns, first_lnum, last_lnum)
+ end
+ })
+ end
+ lenses_by_client[client_id] = lenses
+end
+
+
+---@private
+local function resolve_lenses(lenses, bufnr, client_id, callback)
+ lenses = lenses or {}
+ local num_lens = vim.tbl_count(lenses)
+ if num_lens == 0 then
+ callback()
+ return
+ end
+
+ ---@private
+ local function countdown()
+ num_lens = num_lens - 1
+ if num_lens == 0 then
+ callback()
+ end
+ end
+ local ns = namespaces[client_id]
+ local client = vim.lsp.get_client_by_id(client_id)
+ for _, lens in pairs(lenses or {}) do
+ if lens.command then
+ countdown()
+ else
+ 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
+ -- 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_extmark(
+ bufnr,
+ ns,
+ lens.range.start.line,
+ 0,
+ { virt_text = {{ lens.command.title, 'LspCodeLens' }} }
+ )
+ end
+ countdown()
+ end, bufnr)
+ end
+ end
+end
+
+
+--- |lsp-handler| for the method `textDocument/codeLens`
+---
+function M.on_codelens(err, result, ctx, _)
+ assert(not err, vim.inspect(err))
+
+ M.save(result, ctx.bufnr, ctx.client_id)
+
+ -- Eager display for any resolved (and unresolved) lenses and refresh them
+ -- once resolved.
+ 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
+
+
+--- Refresh the codelens for the current buffer
+---
+--- It is recommended to trigger this using an autocmd or via keymap.
+---
+--- <pre>
+--- autocmd BufEnter,CursorHold,InsertLeave <buffer> lua vim.lsp.codelens.refresh()
+--- </pre>
+---
+function M.refresh()
+ local params = {
+ textDocument = util.make_text_document_params()
+ }
+ local bufnr = api.nvim_get_current_buf()
+ if active_refreshes[bufnr] then
+ return
+ end
+ active_refreshes[bufnr] = true
+ vim.lsp.buf_request(0, 'textDocument/codeLens', params)
+end
+
+
+return M
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index dabe400e0d..ccd325b1ac 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -8,7 +8,7 @@ local util = require('vim.lsp.util')
local if_nil = vim.F.if_nil
---@class DiagnosticSeverity
+---@class DiagnosticSeverity
local DiagnosticSeverity = protocol.DiagnosticSeverity
local to_severity = function(severity)
@@ -46,14 +46,14 @@ end
---@brief lsp-diagnostic
---
---@class Diagnostic
---@field range Range
---@field message string
---@field severity DiagnosticSeverity|nil
---@field code number | string
---@field source string
---@field tags DiagnosticTag[]
---@field relatedInformation DiagnosticRelatedInformation[]
+---@class Diagnostic
+---@field range Range
+---@field message string
+---@field severity DiagnosticSeverity|nil
+---@field code number | string
+---@field source string
+---@field tags DiagnosticTag[]
+---@field relatedInformation DiagnosticRelatedInformation[]
local M = {}
@@ -167,12 +167,12 @@ end
local _diagnostic_namespaces = _make_namespace_table("vim_lsp_diagnostics", true)
local _sign_namespaces = _make_namespace_table("vim_lsp_signs", false)
---@private
+---@private
function M._get_diagnostic_namespace(client_id)
return _diagnostic_namespaces[client_id]
end
---@private
+---@private
function M._get_sign_namespace(client_id)
return _sign_namespaces[client_id]
end
@@ -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)
@@ -250,7 +255,7 @@ local _diagnostic_counts = function(diagnostics)
return counts
end
---@private
+---@private
--- Set the different diagnostic cache after `textDocument/publishDiagnostics`
---@param diagnostics Diagnostic[]
---@param bufnr number
@@ -271,8 +276,12 @@ local function set_diagnostic_cache(diagnostics, bufnr, client_id)
end
-- Account for servers that place diagnostics on terminating newline
if buf_line_count > 0 then
- local start = diagnostic.range.start
- start.line = math.min(start.line, buf_line_count - 1)
+ diagnostic.range.start.line = math.max(math.min(
+ diagnostic.range.start.line, buf_line_count - 1
+ ), 0)
+ diagnostic.range["end"].line = math.max(math.min(
+ diagnostic.range["end"].line, buf_line_count - 1
+ ), 0)
end
end
@@ -282,7 +291,7 @@ local function set_diagnostic_cache(diagnostics, bufnr, client_id)
end
---@private
+---@private
--- Clear the cached diagnostics
---@param bufnr number
---@param client_id number
@@ -317,9 +326,9 @@ function M.save(diagnostics, bufnr, client_id)
-- Clean up our data when the buffer unloads.
api.nvim_buf_attach(bufnr, false, {
- on_detach = function(b)
+ on_detach = function(_, b)
clear_diagnostic_cache(b, client_id)
- _diagnostic_cleanup[bufnr][client_id] = nil
+ _diagnostic_cleanup[b][client_id] = nil
end
})
end
@@ -353,11 +362,12 @@ end
---@param bufnr number
---@param client_id number|nil If nil, then return all of the diagnostics.
--- Else, return just the diagnostics associated with the client_id.
-function M.get(bufnr, client_id)
+---@param predicate function|nil Optional function for filtering diagnostics
+function M.get(bufnr, client_id, predicate)
if client_id == nil then
local all_diagnostics = {}
for iter_client_id, _ in pairs(diagnostic_cache[bufnr]) do
- local iter_diagnostics = M.get(bufnr, iter_client_id)
+ local iter_diagnostics = M.get(bufnr, iter_client_id, predicate)
for _, diagnostic in ipairs(iter_diagnostics) do
table.insert(all_diagnostics, diagnostic)
@@ -367,19 +377,26 @@ function M.get(bufnr, client_id)
return all_diagnostics
end
- return diagnostic_cache[bufnr][client_id] or {}
+ predicate = predicate or function(_) return true end
+ local client_diagnostics = {}
+ for _, diagnostic in ipairs(diagnostic_cache[bufnr][client_id] or {}) do
+ if predicate(diagnostic) then
+ table.insert(client_diagnostics, diagnostic)
+ end
+ end
+ return client_diagnostics
end
--- Get the diagnostics by line
---
----@param bufnr number The buffer number
----@param line_nr number The line number
+---@param bufnr number|nil The buffer number
+---@param line_nr number|nil The line number
---@param opts table|nil Configuration keys
--- - severity: (DiagnosticSeverity, default nil)
--- - Only return diagnostics with this severity. Overrides severity_limit
--- - severity_limit: (DiagnosticSeverity, default nil)
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
----@param client_id number the client id
+---@param client_id|nil number the client id
---@return table Table with map of line number to list of diagnostics.
-- Structured: { [1] = {...}, [5] = {.... } }
function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
@@ -432,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
@@ -455,63 +472,64 @@ end
-- }}}
-- Diagnostic Movements {{{
---- Helper function to iterate through all of the diagnostic lines
----@return table list of diagnostics
-local _iter_diagnostic_lines = function(start, finish, step, bufnr, opts, client_id)
- if bufnr == nil then
- bufnr = vim.api.nvim_get_current_buf()
- end
-
+--- Helper function to find the next diagnostic relative to a position
+---@return table the next diagnostic if found
+local _next_diagnostic = function(position, search_forward, bufnr, opts, client_id)
+ position[1] = position[1] - 1
+ bufnr = bufnr or vim.api.nvim_get_current_buf()
local wrap = if_nil(opts.wrap, true)
-
- local search = function(search_start, search_finish, search_step)
- for line_nr = search_start, search_finish, search_step do
- local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
- if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then
- return line_diagnostics
+ local line_count = vim.api.nvim_buf_line_count(bufnr)
+ for i = 0, line_count do
+ local offset = i * (search_forward and 1 or -1)
+ local line_nr = position[1] + offset
+ if line_nr < 0 or line_nr >= line_count then
+ if not wrap then
+ return
end
+ line_nr = (line_nr + line_count) % line_count
end
- end
-
- local result = search(start, finish, step)
-
- if wrap then
- local wrap_start, wrap_finish
- if step == 1 then
- wrap_start, wrap_finish = 1, start
- else
- wrap_start, wrap_finish = vim.api.nvim_buf_line_count(bufnr), start
- end
-
- if not result then
- result = search(wrap_start, wrap_finish, step)
+ local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
+ if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then
+ local sort_diagnostics, is_next
+ if search_forward then
+ sort_diagnostics = function(a, b) return a.range.start.character < b.range.start.character end
+ is_next = function(diagnostic) return diagnostic.range.start.character > position[2] end
+ else
+ sort_diagnostics = function(a, b) return a.range.start.character > b.range.start.character end
+ is_next = function(diagnostic) return diagnostic.range.start.character < position[2] end
+ end
+ table.sort(line_diagnostics, sort_diagnostics)
+ if i == 0 then
+ for _, v in pairs(line_diagnostics) do
+ if is_next(v) then
+ return v
+ end
+ end
+ else
+ return line_diagnostics[1]
+ end
end
end
-
- return result
end
---@private
---- Helper function to ierate through diagnostic lines and return a position
+---@private
+--- Helper function to return a position from a diagnostic
---
---@return table {row, col}
-local function _iter_diagnostic_lines_pos(opts, line_diagnostics)
+local function _diagnostic_pos(opts, diagnostic)
opts = opts or {}
local win_id = opts.win_id or vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(win_id)
- if line_diagnostics == nil or vim.tbl_isempty(line_diagnostics) then
- return false
- end
+ if not diagnostic then return false end
- local iter_diagnostic = line_diagnostics[1]
- return to_position(iter_diagnostic.range.start, bufnr)
+ return to_position(diagnostic.range.start, bufnr)
end
---@private
+---@private
-- Move to the diagnostic position
-local function _iter_diagnostic_move_pos(name, opts, pos)
+local function _diagnostic_move_pos(name, opts, pos)
opts = opts or {}
local enable_popup = if_nil(opts.enable_popup, true)
@@ -527,7 +545,7 @@ local function _iter_diagnostic_move_pos(name, opts, pos)
if enable_popup then
-- This is a bit weird... I'm surprised that we need to wait til the next tick to do this.
vim.schedule(function()
- M.show_line_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id))
+ M.show_position_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id))
end)
end
end
@@ -543,14 +561,14 @@ function M.get_prev(opts)
local bufnr = vim.api.nvim_win_get_buf(win_id)
local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
- return _iter_diagnostic_lines(cursor_position[1] - 2, 0, -1, bufnr, opts, opts.client_id)
+ return _next_diagnostic(cursor_position, false, bufnr, opts, opts.client_id)
end
--- Return the pos, {row, col}, for the prev diagnostic in the current buffer.
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic position
function M.get_prev_pos(opts)
- return _iter_diagnostic_lines_pos(
+ return _diagnostic_pos(
opts,
M.get_prev(opts)
)
@@ -559,7 +577,7 @@ end
--- Move to the previous diagnostic
---@param opts table See |vim.lsp.diagnostic.goto_next()|
function M.goto_prev(opts)
- return _iter_diagnostic_move_pos(
+ return _diagnostic_move_pos(
"DiagnosticPrevious",
opts,
M.get_prev_pos(opts)
@@ -576,14 +594,14 @@ function M.get_next(opts)
local bufnr = vim.api.nvim_win_get_buf(win_id)
local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
- return _iter_diagnostic_lines(cursor_position[1], vim.api.nvim_buf_line_count(bufnr), 1, bufnr, opts, opts.client_id)
+ return _next_diagnostic(cursor_position, true, bufnr, opts, opts.client_id)
end
--- Return the pos, {row, col}, for the next diagnostic in the current buffer.
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic position
function M.get_next_pos(opts)
- return _iter_diagnostic_lines_pos(
+ return _diagnostic_pos(
opts,
M.get_next(opts)
)
@@ -608,7 +626,7 @@ end
--- - {win_id}: (number, default 0)
--- - Window ID
function M.goto_next(opts)
- return _iter_diagnostic_move_pos(
+ return _diagnostic_move_pos(
"DiagnosticNext",
opts,
M.get_next_pos(opts)
@@ -757,17 +775,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)
@@ -810,10 +831,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)
@@ -822,6 +840,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")
@@ -839,7 +858,7 @@ end
--- Callback scheduled for after leaving insert mode
---
--- Used to handle
---@private
+---@private
function M._execute_scheduled_display(bufnr, client_id)
local args = _bufs_waiting_to_update[bufnr][client_id]
if not args then
@@ -905,20 +924,20 @@ end
-- Diagnostic Private Highlight Utilies {{{
--- Get the severity highlight name
---@private
+---@private
function M._get_severity_highlight_name(severity)
return virtual_text_highlight_map[severity]
end
--- Get floating severity highlight name
---@private
+---@private
function M._get_floating_severity_highlight_name(severity)
return floating_highlight_map[severity]
end
--- This should be called to update the highlights for the LSP client.
function M._define_default_signs_and_highlights()
- --@private
+ ---@private
local function define_default_sign(name, properties)
if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then
vim.fn.sign_define(name, properties)
@@ -1001,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)
@@ -1034,9 +1054,61 @@ function M.on_publish_diagnostics(_, _, params, client_id, _, config)
M.display(diagnostics, bufnr, client_id, config)
end
---@private
+-- 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,
@@ -1104,11 +1176,50 @@ 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
--- }}}
--- Diagnostic User Functions {{{
---- Open a floating window with the diagnostics from {line_nr}
+--- 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,
+ {
+ uri = vim.uri_from_bufnr(bufnr),
+ diagnostics = M.get(bufnr, client_id),
+ },
+ {
+ method = "textDocument/publishDiagnostics",
+ client_id = client_id,
+ bufnr = bufnr,
+ }
+ )
+ end
+
+
+---@private
+--- Open a floating window with the provided diagnostics
---
--- The floating window can be customized with the following highlight groups:
--- <pre>
@@ -1118,30 +1229,21 @@ end
--- LspDiagnosticsFloatingHint
--- </pre>
---@param opts table Configuration table
---- - show_header (boolean, default true): Show "Diagnostics:" header.
----@param bufnr number The buffer number
----@param line_nr number The line number
----@param client_id number|nil the client id
+--- - show_header (boolean, default true): Show "Diagnostics:" header
+--- - all opts for |vim.lsp.util.open_floating_preview()| can be used here
+---@param diagnostics table: The diagnostics to display
---@return table {popup_bufnr, win_id}
-function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
- opts = opts or {}
-
- local show_header = if_nil(opts.show_header, true)
-
- bufnr = bufnr or 0
- line_nr = line_nr or (vim.api.nvim_win_get_cursor(0)[1] - 1)
-
+local function show_diagnostics(opts, diagnostics)
+ if vim.tbl_isempty(diagnostics) then return end
local lines = {}
local highlights = {}
+ local show_header = if_nil(opts.show_header, true)
if show_header then
table.insert(lines, "Diagnostics:")
table.insert(highlights, {0, "Bold"})
end
- local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
- if vim.tbl_isempty(line_diagnostics) then return end
-
- for i, diagnostic in ipairs(line_diagnostics) do
+ for i, diagnostic in ipairs(diagnostics) do
local prefix = string.format("%d. ", i)
local hiname = M._get_floating_severity_highlight_name(diagnostic.severity)
assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity))
@@ -1150,12 +1252,11 @@ 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
- opts.focus_id = "line_diagnostics"
local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts)
for i, hi in ipairs(highlights) do
local prefixlen, hiname = unpack(hi)
@@ -1167,6 +1268,60 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
end
+-- }}}
+-- 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
+--- - severity_limit: (DiagnosticSeverity, default nil)
+--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
+--- - all opts for |show_diagnostics()| can be used here
+---@param buf_nr number|nil The buffer number
+---@param position table|nil The (0,0)-indexed position
+---@return table {popup_bufnr, win_id}
+function M.show_position_diagnostics(opts, buf_nr, position)
+ opts = opts or {}
+ opts.focus_id = "position_diagnostics"
+ buf_nr = buf_nr or vim.api.nvim_get_current_buf()
+ if not position then
+ local curr_position = vim.api.nvim_win_get_cursor(0)
+ curr_position[1] = curr_position[1] - 1
+ position = curr_position
+ end
+ local match_position_predicate = function(diag)
+ return position[1] == diag.range['start'].line and
+ position[2] >= diag.range['start'].character and
+ (position[2] <= diag.range['end'].character or position[1] < diag.range['end'].line)
+ end
+ local position_diagnostics = M.get(buf_nr, nil, match_position_predicate)
+ if opts.severity then
+ position_diagnostics = filter_to_severity_limit(opts.severity, position_diagnostics)
+ elseif opts.severity_limit then
+ position_diagnostics = filter_by_severity_limit(opts.severity_limit, position_diagnostics)
+ end
+ table.sort(position_diagnostics, function(a, b) return a.severity < b.severity end)
+ return show_diagnostics(opts, position_diagnostics)
+end
+
+--- Open a floating window with the diagnostics from {line_nr}
+
+---@param opts table Configuration table
+--- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
+--- |show_diagnostics()| can be used here
+---@param buf_nr number|nil The buffer number
+---@param line_nr number|nil The line number
+---@param client_id number|nil the client id
+---@return table {popup_bufnr, win_id}
+function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id)
+ opts = opts or {}
+ opts.focus_id = "line_diagnostics"
+ line_nr = line_nr or (vim.api.nvim_win_get_cursor(0)[1] - 1)
+ local line_diagnostics = M.get_line_diagnostics(buf_nr, line_nr, opts, client_id)
+ return show_diagnostics(opts, line_diagnostics)
+end
+
--- Clear diagnotics and diagnostic cache
---
--- Handles saving diagnostics from multiple clients in the same buffer.
@@ -1184,10 +1339,11 @@ function M.reset(client_id, buffer_client_map)
end)
end
---- Sets the location list
+---@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
---@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)
@@ -1196,9 +1352,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)
@@ -1208,19 +1363,97 @@ function M.set_loclist(opts)
if severity then
return d.severity == severity
end
- severity = to_severity(opts.severity_limit)
- if severity then
- return d.severity == severity
+ local severity_limit = to_severity(opts.severity_limit)
+ if severity_limit then
+ return d.severity <= severity_limit
end
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
-- }}}
return M
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 6ae54ea253..8fa6f6d024 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -9,31 +9,29 @@ local M = {}
-- FIXME: DOC: Expose in vimdocs
---@private
+---@private
--- Writes to error buffer.
---@param ... (table of strings) Will be concatenated before being written
+---@param ... (table of strings) Will be concatenated before being written
local function err_message(...)
vim.notify(table.concat(vim.tbl_flatten{...}), vim.log.levels.ERROR)
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
-local function progress_handler(_, _, params, client_id)
+---@private
+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
@@ -61,13 +59,14 @@ 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
-M['window/workDoneProgress/create'] = function(_, _, params, client_id)
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create
+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")
@@ -76,12 +75,12 @@ 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
-M['window/showMessageRequest'] = function(_, _, params)
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
+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')
@@ -97,8 +96,9 @@ M['window/showMessageRequest'] = function(_, _, params)
end
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
-M['client/registerCapability'] = function(_, _, _, client_id)
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
+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"
@@ -109,25 +109,25 @@ M['client/registerCapability'] = function(_, _, _, client_id)
return vim.NIL
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
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
+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
@@ -143,8 +143,8 @@ M['textDocument/codeAction'] = function(_, _, actions)
end
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
-M['workspace/applyEdit'] = function(_, _, workspace_edit)
+--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?
if workspace_edit.label then
@@ -157,83 +157,99 @@ 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(_, 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 err then error(vim.inspect(err)) 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(...)
return require('vim.lsp.diagnostic').on_publish_diagnostics(...)
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")
- api.nvim_command("wincmd p")
+M['textDocument/codeLens'] = function(...)
+ return require('vim.lsp.codelens').on_codelens(...)
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")
- api.nvim_command("wincmd p")
+
+---@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, 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, ctx.bufnr))
+ api.nvim_command("lopen")
+ else
+ util.set_qflist(map_result(result, ctx.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
-M['textDocument/rename'] = function(_, _, result)
+
+--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, ctx, _)
if not result then return end
- util.apply_text_edits(result)
+ 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)
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
+M['textDocument/formatting'] = function(_, result, ctx, _)
if not result then return end
- util.apply_text_edits(result)
+ 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)
+--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))
local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1])
@@ -258,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
@@ -274,18 +290,18 @@ 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
+---@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 _ (not used)
+---@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
@@ -298,23 +314,23 @@ local function location_handler(_, method, result)
if #result > 1 then
util.set_qflist(util.locations_to_items(result))
api.nvim_command("copen")
- api.nvim_command("wincmd p")
end
else
util.jump_to_location(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, {
@@ -327,43 +343,53 @@ 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(_, 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
- print('No signature help available')
+ if config.silent ~= true then
+ print('No signature help available')
+ end
return
end
- local ft = api.nvim_buf_get_option(bufnr, 'filetype')
- local lines = util.convert_signature_help_to_markdown_lines(result, ft)
+ 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(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
- 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
-M['textDocument/documentHighlight'] = function(_, _, result, _, bufnr, _)
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
+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
+---@private
---
--- Displays call hierarchy in the quickfix window.
---
---@param direction `"from"` for incoming calls and `"to"` for outgoing calls
---@returns `CallHierarchyIncomingCall[]` if {direction} is `"from"`,
---@returns `CallHierarchyOutgoingCall[]` if {direction} is `"to"`,
+---@param direction `"from"` for incoming calls and `"to"` for outgoing calls
+---@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
@@ -379,20 +405,20 @@ local make_call_hierarchy_handler = function(direction)
end
util.set_qflist(items)
api.nvim_command("copen")
- api.nvim_command("wincmd p")
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
-M['window/logMessage'] = function(_, _, result, client_id)
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage
+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
@@ -410,10 +436,11 @@ M['window/logMessage'] = function(_, _, result, client_id)
return result
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/showMessage
-M['window/showMessage'] = function(_, _, result, client_id)
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessage
+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
@@ -430,16 +457,23 @@ 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
- return err_message(tostring(err))
+ 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;
+ -- 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)
+ 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 471a311c16..5d2e396cc5 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -17,21 +17,32 @@ 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 "/"
- --@private
+ 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)
end
local logfilename = path_join(vim.fn.stdpath('cache'), 'lsp.log')
--- Returns the log filename.
- --@returns (string) log filename
+ ---@returns (string) log filename
function log.get_filename()
return logfilename
end
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
@@ -77,7 +88,7 @@ end
vim.tbl_add_reverse_lookup(log.levels)
--- Sets the current log level.
---@param level (string or number) One of `vim.lsp.log.levels`
+---@param level (string or number) One of `vim.lsp.log.levels`
function log.set_level(level)
if type(level) == 'string' then
current_log_level = assert(log.levels[level:upper()], string.format("Invalid log level: %q", level))
@@ -88,9 +99,14 @@ 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
+---@param level number log level
+---@returns (bool) true if would log, false if not
function log.should_log(level)
return level >= current_log_level
end
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 7e43eb84de..27703b4503 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -5,14 +5,14 @@ local if_nil = vim.F.if_nil
local protocol = {}
--[=[
---@private
+---@private
--- Useful for interfacing with:
--- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md
function transform_schema_comments()
nvim.command [[silent! '<,'>g/\/\*\*\|\*\/\|^$/d]]
nvim.command [[silent! '<,'>s/^\(\s*\) \* \=\(.*\)/\1--\2/]]
end
---@private
+---@private
function transform_schema_to_table()
transform_schema_comments()
nvim.command [[silent! '<,'>s/: \S\+//]]
@@ -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 = {
@@ -1002,8 +1003,7 @@ function protocol.resolve_capabilities(server_capabilities)
elseif type(server_capabilities.declarationProvider) == 'boolean' then
general_properties.declaration = server_capabilities.declarationProvider
elseif type(server_capabilities.declarationProvider) == 'table' then
- -- TODO: support more detailed declarationProvider options.
- general_properties.declaration = false
+ general_properties.declaration = server_capabilities.declarationProvider
else
error("The server sent invalid declarationProvider")
end
@@ -1013,8 +1013,7 @@ function protocol.resolve_capabilities(server_capabilities)
elseif type(server_capabilities.typeDefinitionProvider) == 'boolean' then
general_properties.type_definition = server_capabilities.typeDefinitionProvider
elseif type(server_capabilities.typeDefinitionProvider) == 'table' then
- -- TODO: support more detailed typeDefinitionProvider options.
- general_properties.type_definition = false
+ general_properties.type_definition = server_capabilities.typeDefinitionProvider
else
error("The server sent invalid typeDefinitionProvider")
end
@@ -1024,8 +1023,7 @@ function protocol.resolve_capabilities(server_capabilities)
elseif type(server_capabilities.implementationProvider) == 'boolean' then
general_properties.implementation = server_capabilities.implementationProvider
elseif type(server_capabilities.implementationProvider) == 'table' then
- -- TODO(ashkan) support more detailed implementation options.
- general_properties.implementation = false
+ general_properties.implementation = server_capabilities.implementationProvider
else
error("The server sent invalid implementationProvider")
end
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 98835d6708..eedb708118 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -5,11 +5,11 @@ local protocol = require('vim.lsp.protocol')
local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap
-- TODO replace with a better implementation.
---@private
+---@private
--- Encodes to JSON.
---
---@param data (table) Data to encode
---@returns (string) Encoded object
+---@param data (table) Data to encode
+---@returns (string) Encoded object
local function json_encode(data)
local status, result = pcall(vim.fn.json_encode, data)
if status then
@@ -18,11 +18,11 @@ local function json_encode(data)
return nil, result
end
end
---@private
+---@private
--- Decodes from JSON.
---
---@param data (string) Data to decode
---@returns (table) Decoded JSON object
+---@param data (string) Data to decode
+---@returns (table) Decoded JSON object
local function json_decode(data)
local status, result = pcall(vim.fn.json_decode, data)
if status then
@@ -32,10 +32,10 @@ local function json_decode(data)
end
end
---@private
+---@private
--- Checks whether a given path exists and is a directory.
---@param filename (string) path to check
---@returns (bool)
+---@param filename (string) path to check
+---@returns (bool)
local function is_dir(filename)
local stat = vim.loop.fs_stat(filename)
return stat and stat.type == 'directory' or false
@@ -43,30 +43,35 @@ end
local NIL = vim.NIL
---@private
+---@private
local recursive_convert_NIL
recursive_convert_NIL = function(v, tbl_processed)
if v == NIL then
return nil
elseif not tbl_processed[v] and type(v) == 'table' then
tbl_processed[v] = true
+ local inside_list = vim.tbl_islist(v)
return vim.tbl_map(function(x)
- return recursive_convert_NIL(x, tbl_processed)
+ if not inside_list or (inside_list and type(x) == "table") then
+ return recursive_convert_NIL(x, tbl_processed)
+ else
+ return x
+ end
end, v)
end
return v
end
---@private
+---@private
--- Returns its argument, but converts `vim.NIL` to Lua `nil`.
---@param v (any) Argument
---@returns (any)
+---@param v (any) Argument
+---@returns (any)
local function convert_NIL(v)
return recursive_convert_NIL(v, {})
end
---@private
+---@private
--- Merges current process env with the given env and returns the result as
--- a list of "k=v" strings.
---
@@ -76,8 +81,8 @@ end
--- in: { PRODUCTION="false", PATH="/usr/bin/", PORT=123, HOST="0.0.0.0", }
--- out: { "PRODUCTION=false", "PATH=/usr/bin/", "PORT=123", "HOST=0.0.0.0", }
--- </pre>
---@param env (table) table of environment variable assignments
---@returns (table) list of `"k=v"` strings
+---@param env (table) table of environment variable assignments
+---@returns (table) list of `"k=v"` strings
local function env_merge(env)
if env == nil then
return env
@@ -92,11 +97,11 @@ local function env_merge(env)
return final_env
end
---@private
+---@private
--- Embeds the given string into a table and correctly computes `Content-Length`.
---
---@param encoded_message (string)
---@returns (table) table containing encoded message and `Content-Length` attribute
+---@param encoded_message (string)
+---@returns (table) table containing encoded message and `Content-Length` attribute
local function format_message_with_content_length(encoded_message)
return table.concat {
'Content-Length: '; tostring(#encoded_message); '\r\n\r\n';
@@ -104,11 +109,11 @@ local function format_message_with_content_length(encoded_message)
}
end
---@private
+---@private
--- Parses an LSP Message's header
---
---@param header: The header to parse.
---@returns Parsed headers
+---@param header: The header to parse.
+---@returns Parsed headers
local function parse_headers(header)
if type(header) ~= 'string' then
return nil
@@ -136,7 +141,7 @@ end
-- case insensitive pattern.
local header_start_pattern = ("content"):gsub("%w", function(c) return "["..c..c:upper().."]" end)
---@private
+---@private
--- The actual workhorse.
local function request_parser_loop()
local buffer = '' -- only for header part
@@ -198,8 +203,8 @@ local client_errors = vim.tbl_add_reverse_lookup {
--- Constructs an error message from an LSP error object.
---
---@param err (table) The error object
---@returns (string) The formatted error message
+---@param err (table) The error object
+---@returns (string) The formatted error message
local function format_rpc_error(err)
validate {
err = { err, 't' };
@@ -228,9 +233,9 @@ end
--- Creates an RPC response object/table.
---
---@param code RPC error code defined in `vim.lsp.protocol.ErrorCodes`
---@param message (optional) arbitrary message to send to server
---@param data (optional) arbitrary data to send to server
+---@param code RPC error code defined in `vim.lsp.protocol.ErrorCodes`
+---@param message (optional) arbitrary message to send to server
+---@param data (optional) arbitrary data to send to server
local function rpc_response_error(code, message, data)
-- TODO should this error or just pick a sane error (like InternalError)?
local code_name = assert(protocol.ErrorCodes[code], 'Invalid RPC error code')
@@ -245,38 +250,38 @@ end
local default_dispatchers = {}
---@private
+---@private
--- Default dispatcher for notifications sent to an LSP server.
---
---@param method (string) The invoked LSP method
---@param params (table): Parameters for the invoked LSP method
+---@param method (string) The invoked LSP method
+---@param params (table): Parameters for the invoked LSP method
function default_dispatchers.notification(method, params)
local _ = log.debug() and log.debug('notification', method, params)
end
---@private
+---@private
--- Default dispatcher for requests sent to an LSP server.
---
---@param method (string) The invoked LSP method
---@param params (table): Parameters for the invoked LSP method
---@returns `nil` and `vim.lsp.protocol.ErrorCodes.MethodNotFound`.
+---@param method (string) The invoked LSP method
+---@param params (table): Parameters for the invoked LSP method
+---@returns `nil` and `vim.lsp.protocol.ErrorCodes.MethodNotFound`.
function default_dispatchers.server_request(method, params)
local _ = log.debug() and log.debug('server_request', method, params)
return nil, rpc_response_error(protocol.ErrorCodes.MethodNotFound)
end
---@private
+---@private
--- Default dispatcher for when a client exits.
---
---@param code (number): Exit code
---@param signal (number): Number describing the signal used to terminate (if
+---@param code (number): Exit code
+---@param signal (number): Number describing the signal used to terminate (if
---any)
function default_dispatchers.on_exit(code, signal)
local _ = log.info() and log.info("client_exit", { code = code, signal = signal })
end
---@private
+---@private
--- Default dispatcher for client errors.
---
---@param code (number): Error code
---@param err (any): Details about the error
+---@param code (number): Error code
+---@param err (any): Details about the error
---any)
function default_dispatchers.on_error(code, err)
local _ = log.error() and log.error('client_error:', client_errors[code], err)
@@ -285,25 +290,25 @@ end
--- Starts an LSP server process and create an LSP RPC client object to
--- interact with it.
---
---@param cmd (string) Command to start the LSP server.
---@param cmd_args (table) List of additional string arguments to pass to {cmd}.
---@param dispatchers (table, optional) Dispatchers for LSP message types. Valid
+---@param cmd (string) Command to start the LSP server.
+---@param cmd_args (table) List of additional string arguments to pass to {cmd}.
+---@param dispatchers (table, optional) Dispatchers for LSP message types. Valid
---dispatcher names are:
--- - `"notification"`
--- - `"server_request"`
--- - `"on_error"`
--- - `"on_exit"`
---@param extra_spawn_params (table, optional) Additional context for the LSP
+---@param extra_spawn_params (table, optional) Additional context for the LSP
--- server process. May contain:
--- - {cwd} (string) Working directory for the LSP server process
--- - {env} (table) Additional environment variables for LSP server process
---@returns Client RPC object.
+---@returns Client RPC object.
---
---@returns Methods:
+---@returns Methods:
--- - `notify()` |vim.lsp.rpc.notify()|
--- - `request()` |vim.lsp.rpc.request()|
---
---@returns Members:
+---@returns Members:
--- - {pid} (number) The LSP server's PID.
--- - {handle} A handle for low-level interaction with the LSP server process
--- |vim.loop|.
@@ -353,10 +358,10 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
local handle, pid
do
- --@private
+ ---@private
--- Callback for |vim.loop.spawn()| Closes all streams and runs the `on_exit` dispatcher.
- --@param code (number) Exit code
- --@param signal (number) Signal that was used to terminate (if any)
+ ---@param code (number) Exit code
+ ---@param signal (number) Signal that was used to terminate (if any)
local function onexit(code, signal)
stdin:close()
stdout:close()
@@ -380,12 +385,12 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end
end
- --@private
+ ---@private
--- Encodes {payload} into a JSON-RPC message and sends it to the remote
--- process.
---
- --@param payload (table) Converted into a JSON string, see |json_encode()|
- --@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing.
+ ---@param payload (table) Converted into a JSON string, see |json_encode()|
+ ---@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing.
local function encode_and_send(payload)
local _ = log.debug() and log.debug("rpc.send.payload", payload)
if handle == nil or handle:is_closing() then return false end
@@ -401,9 +406,9 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
-- `start()`
--
--- Sends a notification to the LSP server.
- --@param method (string) The invoked LSP method
- --@param params (table): Parameters for the invoked LSP method
- --@returns (bool) `true` if notification could be sent, `false` if not
+ ---@param method (string) The invoked LSP method
+ ---@param params (table): Parameters for the invoked LSP method
+ ---@returns (bool) `true` if notification could be sent, `false` if not
local function notify(method, params)
return encode_and_send {
jsonrpc = "2.0";
@@ -412,7 +417,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
}
end
- --@private
+ ---@private
--- sends an error object to the remote LSP process.
local function send_response(request_id, err, result)
return encode_and_send {
@@ -428,10 +433,10 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
--
--- Sends a request to the LSP server and runs {callback} upon response.
---
- --@param method (string) The invoked LSP method
- --@param params (table) Parameters for the invoked LSP method
- --@param callback (function) Callback to invoke
- --@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not
+ ---@param method (string) The invoked LSP method
+ ---@param params (table) Parameters for the invoked LSP method
+ ---@param callback (function) Callback to invoke
+ ---@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not
local function request(method, params, callback)
validate {
callback = { callback, 'f' };
@@ -444,7 +449,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
method = method;
params = params;
}
- if result then
+ if result and message_callbacks then
message_callbacks[message_id] = schedule_wrap(callback)
return result, message_id
else
@@ -458,13 +463,13 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end
end)
- --@private
+ ---@private
local function on_error(errkind, ...)
assert(client_errors[errkind])
-- TODO what to do if this fails?
pcall(dispatchers.on_error, errkind, ...)
end
- --@private
+ ---@private
local function pcall_handler(errkind, status, head, ...)
if not status then
on_error(errkind, head, ...)
@@ -472,7 +477,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
end
return status, head, ...
end
- --@private
+ ---@private
local function try_call(errkind, fn, ...)
return pcall_handler(errkind, pcall(fn, ...))
end
@@ -481,7 +486,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
-- time and log them. This would require storing the timestamp. I could call
-- them with an error then, perhaps.
- --@private
+ ---@private
local function handle_body(body)
local decoded, err = json_decode(body)
if not decoded then
@@ -543,14 +548,14 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
-- - The server will not send a result callback after this cancellation.
-- - If the server sent this cancellation ACK after sending the result, the user of this RPC
-- client will ignore the result themselves.
- if result_id then
+ if result_id and message_callbacks then
message_callbacks[result_id] = nil
end
return
end
end
- local callback = message_callbacks[result_id]
+ local callback = message_callbacks and message_callbacks[result_id]
if callback then
message_callbacks[result_id] = nil
validate {
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index cb9a7cbed5..a4c8b69f6c 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -40,24 +40,30 @@ local loclist_type_map = {
}
---@private
--- Check the border given by opts or the default border for the additional
--- size it adds to a float.
---@returns size of border in height and width
+---@private
+--- Check the border given by opts or the default border for the additional
+--- size it adds to a float.
+---@param opts (table, optional) options for the floating window
+--- - border (string or table) the border
+---@returns (table) size of border in the form of { height = height, width = width }
local function get_border_size(opts)
local border = opts and opts.border or default_border
local height = 0
local width = 0
if type(border) == 'string' then
- local border_size = {none = {0, 0}, single = {2, 2}, double = {2, 2}, shadow = {1, 1}}
+ local border_size = {none = {0, 0}, single = {2, 2}, double = {2, 2}, rounded = {2, 2}, solid = {2, 2}, shadow = {1, 1}}
if border_size[border] == nil then
- error("floating preview border is not correct. Please refer to the docs |vim.api.nvim_open_win()|"
- .. vim.inspect(border))
+ error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
end
height, width = unpack(border_size[border])
else
+ if 8 % #border ~= 0 then
+ error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
+ end
+ ---@private
local function border_width(id)
+ id = (id - 1) % #border + 1
if type(border[id]) == "table" then
-- border specified as a table of <character, highlight group>
return vim.fn.strdisplaywidth(border[id][1])
@@ -65,9 +71,11 @@ local function get_border_size(opts)
-- border specified as a list of border characters
return vim.fn.strdisplaywidth(border[id])
end
- error("floating preview border is not correct. Please refer to the docs |vim.api.nvim_open_win()|" .. vim.inspect(border))
+ error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
end
+ ---@private
local function border_height(id)
+ id = (id - 1) % #border + 1
if type(border[id]) == "table" then
-- border specified as a table of <character, highlight group>
return #border[id][1] > 0 and 1 or 0
@@ -75,7 +83,7 @@ local function get_border_size(opts)
-- border specified as a list of border characters
return #border[id] > 0 and 1 or 0
end
- error("floating preview border is not correct. Please refer to the docs |vim.api.nvim_open_win()|" .. vim.inspect(border))
+ error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
end
height = height + border_height(2) -- top
height = height + border_height(6) -- bottom
@@ -86,7 +94,7 @@ local function get_border_size(opts)
return { height = height, width = width }
end
---@private
+---@private
local function split_lines(value)
return split(value, '\n', true)
end
@@ -95,11 +103,11 @@ end
---
--- CAUTION: Changes in-place!
---
---@param lines (table) Original list of strings
---@param A (table) Start position; a 2-tuple of {line, col} numbers
---@param B (table) End position; a 2-tuple of {line, col} numbers
---@param new_lines A list of strings to replace the original
---@returns (table) The modified {lines} object
+---@param lines (table) Original list of strings
+---@param A (table) Start position; a 2-tuple of {line, col} numbers
+---@param B (table) End position; a 2-tuple of {line, col} numbers
+---@param new_lines A list of strings to replace the original
+---@returns (table) The modified {lines} object
function M.set_lines(lines, A, B, new_lines)
-- 0-indexing to 1-indexing
local i_0 = A[1] + 1
@@ -133,7 +141,7 @@ function M.set_lines(lines, A, B, new_lines)
return lines
end
---@private
+---@private
local function sort_by_key(fn)
return function(a,b)
local ka, kb = fn(a), fn(b)
@@ -147,12 +155,12 @@ local function sort_by_key(fn)
return false
end
end
---@private
+---@private
local edit_sort_key = sort_by_key(function(e)
return {e.A[1], e.A[2], e.i}
end)
---@private
+---@private
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
--- Returns a zero-indexed column, since set_lines() does the conversion to
--- 1-indexed
@@ -238,8 +246,9 @@ function M.get_progress_messages()
end
--- Applies a list of text edits to a buffer.
---@param text_edits (table) list of `TextEdit` objects
---@param buf_nr (number) Buffer id
+---@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
@@ -294,11 +303,11 @@ end
-- function M.glob_to_regex(glob)
-- end
---@private
+---@private
--- Finds the first line and column of the difference between old and new lines
---@param old_lines table list of lines
---@param new_lines table list of lines
---@returns (int, int) start_line_idx and start_col_idx of range
+---@param old_lines table list of lines
+---@param new_lines table list of lines
+---@returns (int, int) start_line_idx and start_col_idx of range
local function first_difference(old_lines, new_lines, start_line_idx)
local line_count = math.min(#old_lines, #new_lines)
if line_count == 0 then return 1, 1 end
@@ -324,12 +333,12 @@ local function first_difference(old_lines, new_lines, start_line_idx)
end
---@private
+---@private
--- Finds the last line and column of the differences between old and new lines
---@param old_lines table list of lines
---@param new_lines table list of lines
---@param start_char integer First different character idx of range
---@returns (int, int) end_line_idx and end_col_idx of range
+---@param old_lines table list of lines
+---@param new_lines table list of lines
+---@param start_char integer First different character idx of range
+---@returns (int, int) end_line_idx and end_col_idx of range
local function last_difference(old_lines, new_lines, start_char, end_line_idx)
local line_count = math.min(#old_lines, #new_lines)
if line_count == 0 then return 0,0 end
@@ -368,14 +377,14 @@ local function last_difference(old_lines, new_lines, start_char, end_line_idx)
end
---@private
+---@private
--- Get the text of the range defined by start and end line/column
---@param lines table list of lines
---@param start_char integer First different character idx of range
---@param end_char integer Last different character idx of range
---@param start_line integer First different line idx of range
---@param end_line integer Last different line idx of range
---@returns string text extracted from defined region
+---@param lines table list of lines
+---@param start_char integer First different character idx of range
+---@param end_char integer Last different character idx of range
+---@param start_line integer First different line idx of range
+---@param end_line integer Last different line idx of range
+---@returns string text extracted from defined region
local function extract_text(lines, start_line, start_char, end_line, end_char)
if start_line == #lines + end_line + 1 then
if end_line == 0 then return '' end
@@ -395,14 +404,14 @@ local function extract_text(lines, start_line, start_char, end_line, end_char)
return result
end
---@private
+---@private
--- Compute the length of the substituted range
---@param lines table list of lines
---@param start_char integer First different character idx of range
---@param end_char integer Last different character idx of range
---@param start_line integer First different line idx of range
---@param end_line integer Last different line idx of range
---@returns (int, int) end_line_idx and end_col_idx of range
+---@param lines table list of lines
+---@param start_char integer First different character idx of range
+---@param end_char integer Last different character idx of range
+---@param start_line integer First different line idx of range
+---@param end_line integer Last different line idx of range
+---@returns (int, int) end_line_idx and end_col_idx of range
local function compute_length(lines, start_line, start_char, end_line, end_char)
local adj_end_line = #lines + end_line + 1
local adj_end_char
@@ -423,12 +432,12 @@ local function compute_length(lines, start_line, start_char, end_line, end_char)
end
--- Returns the range table for the difference between old and new lines
---@param old_lines table list of lines
---@param new_lines table list of lines
---@param start_line_idx int line to begin search for first difference
---@param end_line_idx int line to begin search for last difference
---@param offset_encoding string encoding requested by language server
---@returns table start_line_idx and start_col_idx of range
+---@param old_lines table list of lines
+---@param new_lines table list of lines
+---@param start_line_idx int line to begin search for first difference
+---@param end_line_idx int line to begin search for last difference
+---@param offset_encoding string encoding requested by language server
+---@returns table start_line_idx and start_col_idx of range
function M.compute_diff(old_lines, new_lines, start_line_idx, end_line_idx, offset_encoding)
local start_line, start_char = first_difference(old_lines, new_lines, start_line_idx)
local end_line, end_char = last_difference(vim.list_slice(old_lines, start_line, #old_lines),
@@ -468,9 +477,9 @@ end
--- Can be used to extract the completion items from a
--- `textDocument/completion` request, which may return one of
--- `CompletionItem[]`, `CompletionList` or null.
---@param result (table) The result of a `textDocument/completion` request
---@returns (table) List of completion items
---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
+---@param result (table) The result of a `textDocument/completion` request
+---@returns (table) List of completion items
+---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
function M.extract_completion_items(result)
if type(result) == 'table' and result.items then
-- result is a `CompletionList`
@@ -514,12 +523,12 @@ function M.apply_text_document_edit(text_document_edit, index)
M.apply_text_edits(text_document_edit.edits, bufnr)
end
---@private
+---@private
--- Recursively parses snippets in a completion entry.
---
---@param input (string) Snippet text to parse for snippets
---@param inner (bool) Whether this function is being called recursively
---@returns 2-tuple of strings: The first is the parsed result, the second is the
+---@param input (string) Snippet text to parse for snippets
+---@param inner (bool) Whether this function is being called recursively
+---@returns 2-tuple of strings: The first is the parsed result, the second is the
---unparsed rest of the input
local function parse_snippet_rec(input, inner)
local res = ""
@@ -576,28 +585,28 @@ end
--- Parses snippets in a completion entry.
---
---@param input (string) unparsed snippet
---@returns (string) parsed snippet
+---@param input (string) unparsed snippet
+---@returns (string) parsed snippet
function M.parse_snippet(input)
local res, _ = parse_snippet_rec(input, false)
return res
end
---@private
+---@private
--- Sorts by CompletionItem.sortText.
---
---@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
local function sort_completion_items(items)
table.sort(items, function(a, b)
return (a.sortText or a.label) < (b.sortText or b.label)
end)
end
---@private
+---@private
--- Returns text that should be inserted when selecting completion item. The
--- precedence is as follows: textEdit.newText > insertText > label
---@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
local function get_completion_word(item)
if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= "" then
local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
@@ -617,7 +626,7 @@ local function get_completion_word(item)
return item.label
end
---@private
+---@private
--- Some language servers return complementary candidates whose prefixes do not
--- match are also returned. So we exclude completion candidates whose prefix
--- does not match.
@@ -632,9 +641,9 @@ end
--- the client must handle it properly even if it receives a value outside the
--- specification.
---
---@param completion_item_kind (`vim.lsp.protocol.completionItemKind`)
---@returns (`vim.lsp.protocol.completionItemKind`)
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
+---@param completion_item_kind (`vim.lsp.protocol.completionItemKind`)
+---@returns (`vim.lsp.protocol.completionItemKind`)
+---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
function M._get_completion_item_kind_name(completion_item_kind)
return protocol.CompletionItemKind[completion_item_kind] or "Unknown"
end
@@ -642,12 +651,12 @@ end
--- Turns the result of a `textDocument/completion` request into vim-compatible
--- |complete-items|.
---
---@param result The result of a `textDocument/completion` call, e.g. from
+---@param result The result of a `textDocument/completion` call, e.g. from
---|vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
--- `CompletionList` or `null`
---@param prefix (string) the prefix to filter the completion items
---@returns { matches = complete-items table, incomplete = bool }
---@see |complete-items|
+---@param prefix (string) the prefix to filter the completion items
+---@returns { matches = complete-items table, incomplete = bool }
+---@see |complete-items|
function M.text_document_completion_list_to_complete_items(result, prefix)
local items = M.extract_completion_items(result)
if vim.tbl_isempty(items) then
@@ -697,8 +706,8 @@ end
--- Rename old_fname to new_fname
---
---@param opts (table)
+---
+---@param opts (table)
-- overwrite? bool
-- ignoreIfExists? bool
function M.rename(old_fname, new_fname, opts)
@@ -753,8 +762,8 @@ end
--- Applies a `WorkspaceEdit`.
---
---@param workspace_edit (table) `WorkspaceEdit`
--- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
+---@param workspace_edit (table) `WorkspaceEdit`
+--see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
function M.apply_workspace_edit(workspace_edit)
if workspace_edit.documentChanges then
for idx, change in ipairs(workspace_edit.documentChanges) do
@@ -793,10 +802,10 @@ end
--- window for `textDocument/hover`, for parsing the result of
--- `textDocument/signatureHelp`, and potentially others.
---
---@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`)
---@param contents (table, optional, default `{}`) List of strings to extend with converted lines
---@returns {contents}, extended with lines of converted markdown.
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
+---@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`)
+---@param contents (table, optional, default `{}`) List of strings to extend with converted lines
+---@returns {contents}, extended with lines of converted markdown.
+---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
function M.convert_input_to_markdown_lines(input, contents)
contents = contents or {}
-- MarkedString variation 1
@@ -806,14 +815,20 @@ function M.convert_input_to_markdown_lines(input, contents)
assert(type(input) == 'table', "Expected a table for Hover.contents")
-- MarkupContent
if input.kind then
- -- The kind can be either plaintext or markdown. However, either way we
- -- will just be rendering markdown, so we handle them both the same way.
- -- TODO these can have escaped/sanitized html codes in markdown. We
- -- should make sure we handle this correctly.
+ -- The kind can be either plaintext or markdown.
+ -- If it's plaintext, then wrap it in a <text></text> block
-- Some servers send input.value as empty, so let's ignore this :(
- -- assert(type(input.value) == 'string')
- list_extend(contents, split_lines(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
+ value = string.format("<text>\n%s\n</text>", value)
+ end
+
+ -- 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 :(
@@ -837,11 +852,12 @@ end
--- Converts `textDocument/SignatureHelp` response to markdown lines.
---
---@param signature_help Response of `textDocument/SignatureHelp`
---@param ft optional filetype that will be use as the `lang` for the label markdown code block
---@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)
+---@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, triggers)
if not signature_help.signatures then
return
end
@@ -850,6 +866,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
@@ -861,7 +878,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft)
end
local label = signature.label
if ft then
- -- wrap inside a code block so fancy_markdown can render it properly
+ -- wrap inside a code block so stylize_markdown can render it properly
label = ("```%s\n%s\n```"):format(ft, label)
end
vim.list_extend(contents, vim.split(label, '\n', true))
@@ -869,11 +886,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
--[=[
@@ -894,22 +917,44 @@ 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
--- table can be passed to |nvim_open_win()|.
---
---@param width (number) window width (in character cells)
---@param height (number) window height (in character cells)
---@param opts (table, optional)
---@returns (table) Options
+---@param width (number) window width (in character cells)
+---@param height (number) window height (in character cells)
+---@param opts (table, optional)
+---@returns (table) Options
function M.make_floating_popup_options(width, height, opts)
validate {
opts = { opts, 't', true };
@@ -936,7 +981,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
@@ -954,13 +999,14 @@ 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
--- Jumps to a location.
---
---@param location (`Location`|`LocationLink`)
---@returns `true` if the jump succeeded
+---@param location (`Location`|`LocationLink`)
+---@returns `true` if the jump succeeded
function M.jump_to_location(location)
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
@@ -990,8 +1036,8 @@ end
--- - for Location, range is shown (e.g., function definition)
--- - for LocationLink, targetRange is shown (e.g., body of function definition)
---
---@param location a single `Location` or `LocationLink`
---@returns (bufnr,winnr) buffer and window number of floating window or nil
+---@param location a single `Location` or `LocationLink`
+---@returns (bufnr,winnr) buffer and window number of floating window or nil
function M.preview_location(location, opts)
-- location may be LocationLink or Location (more useful for the former)
local uri = location.targetUri or location.uri
@@ -1005,7 +1051,7 @@ function M.preview_location(location, opts)
local syntax = api.nvim_buf_get_option(bufnr, 'syntax')
if syntax == "" then
-- When no syntax is set, we use filetype as fallback. This might not result
- -- in a valid syntax definition. See also ft detection in fancy_floating_win.
+ -- in a valid syntax definition. See also ft detection in stylize_markdown.
-- An empty syntax is more common now with TreeSitter, since TS disables syntax.
syntax = api.nvim_buf_get_option(bufnr, 'filetype')
end
@@ -1014,7 +1060,7 @@ function M.preview_location(location, opts)
return M.open_floating_preview(contents, syntax, opts)
end
---@private
+---@private
local function find_window_by_var(name, value)
for _, win in ipairs(api.nvim_list_wins()) do
if npcall(api.nvim_win_get_var, win, name) == value then
@@ -1023,53 +1069,6 @@ local function find_window_by_var(name, value)
end
end
---- Enters/leaves the focusable window associated with the current buffer via the
---window - variable `unique_name`. If no such window exists, run the function
---{fn}.
----
---@param unique_name (string) Window variable
---@param fn (function) should return create a new window and return a tuple of
----({focusable_buffer_id}, {window_id}). if {focusable_buffer_id} is a valid
----buffer id, the newly created window will be the new focus associated with
----the current buffer via the tag `unique_name`.
---@returns (pbufnr, pwinnr) if `fn()` has created a new window; nil otherwise
----@deprecated please use open_floating_preview directly
-function M.focusable_float(unique_name, fn)
- vim.notify("focusable_float is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
- -- Go back to previous window if we are in a focusable one
- if npcall(api.nvim_win_get_var, 0, unique_name) then
- return api.nvim_command("wincmd p")
- end
- local bufnr = api.nvim_get_current_buf()
- do
- local win = find_window_by_var(unique_name, bufnr)
- if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then
- api.nvim_set_current_win(win)
- api.nvim_command("stopinsert")
- return
- end
- end
- local pbufnr, pwinnr = fn()
- if pbufnr then
- api.nvim_win_set_var(pwinnr, unique_name, bufnr)
- return pbufnr, pwinnr
- end
-end
-
---- Focuses/unfocuses the floating preview window associated with the current
---- buffer via the window variable `unique_name`. If no such preview window
---- exists, makes a new one.
----
---@param unique_name (string) Window variable
---@param fn (function) The return values of this function will be passed
----directly to |vim.lsp.util.open_floating_preview()|, in the case that a new
----floating window should be created
----@deprecated please use open_floating_preview directly
-function M.focusable_preview(unique_name, fn)
- vim.notify("focusable_preview is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
- return M.open_floating_preview(fn(), {focus_id = unique_name})
-end
-
--- Trims empty lines from input and pad top and bottom with empty lines
---
---@param contents table of lines to trim and pad
@@ -1097,12 +1096,19 @@ function M._trim(contents, opts)
return contents
end
-
-
---- @deprecated please use open_floating_preview directly
-function M.fancy_floating_markdown(contents, opts)
- vim.notify("fancy_floating_markdown is deprecated. Please use open_floating_preview and pass focus_id = [unique_name] instead", vim.log.levels.WARN)
- return M.open_floating_preview(contents, "markdown", opts)
+--- Generates a table mapping markdown code block lang to vim syntax,
+--- based on g:markdown_fenced_languages
+---@return a table of lang -> syntax mappings
+---@private
+local function get_markdown_fences()
+ local fences = {}
+ for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do
+ local lang, syntax = fence:match("^(.*)=(.*)$")
+ if lang then
+ fences[lang] = syntax
+ end
+ end
+ return fences
end
--- Converts markdown into syntax highlighted regions by stripping the code
@@ -1134,26 +1140,52 @@ function M.stylize_markdown(bufnr, contents, opts)
}
opts = opts or {}
+ -- table of fence types to {ft, begin, end}
+ -- when ft is nil, we get the ft from the regex match
+ local matchers = {
+ block = {nil, "```+([a-zA-Z0-9_]*)", "```+"},
+ pre = {"", "<pre>", "</pre>"},
+ code = {"", "<code>", "</code>"},
+ text = {"plaintex", "<text>", "</text>"},
+ }
+
+ local match_begin = function(line)
+ for type, pattern in pairs(matchers) do
+ local ret = line:match(string.format("^%%s*%s%%s*$", pattern[2]))
+ if ret then
+ return {
+ type = type,
+ ft = pattern[1] or ret
+ }
+ end
+ end
+ end
+
+ local match_end = function(line, match)
+ local pattern = matchers[match.type]
+ return line:match(string.format("^%%s*%s%%s*$", pattern[3]))
+ end
+
+ -- 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
+ local markdown_lines = {}
do
local i = 1
while i <= #contents do
local line = contents[i]
- -- TODO(ashkan): use a more strict regex for filetype?
- local ft = line:match("^```([a-zA-Z0-9_]*)$")
- -- local ft = line:match("^```(.*)$")
- -- TODO(ashkan): validate the filetype here.
- local is_pre = line:match("^%s*<pre>%s*$")
- if is_pre then
- ft = ""
- end
- if ft then
+ local match = match_begin(line)
+ if match then
local start = #stripped
i = i + 1
while i <= #contents do
line = contents[i]
- if line == "```" or (is_pre and line:match("^%s*</pre>%s*$")) then
+ if match_end(line, match) then
i = i + 1
break
end
@@ -1161,56 +1193,59 @@ function M.stylize_markdown(bufnr, contents, opts)
i = i + 1
end
table.insert(highlights, {
- ft = ft;
+ ft = match.ft;
start = start + 1;
- finish = #stripped + 1 - 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)
+ -- 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
end
- -- Clean up
- stripped = M._trim(stripped, 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)
- -- 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]:match("^---+$") then
- stripped[h.finish + 1] = string.rep("─", math.min(width, opts.wrap_at or width))
- else
- table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at or width)))
- offset = offset + 1
- height = height + 1
- end
- end
+ local sep_line = string.rep("─", math.min(width, opts.wrap_at or width))
+
+ for l in pairs(markdown_lines) do
+ if stripped[l]:match("^---+$") then
+ stripped[l] = sep_line
end
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
local idx = 1
- --@private
+ ---@private
-- keep track of syntaxes we already inlcuded.
-- no need to include the same syntax more than once
local langs = {}
+ local fences = get_markdown_fences()
local function apply_syntax_to_region(ft, start, finish)
if ft == "" then
vim.cmd(string.format("syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend", start, finish + 1))
return
end
+ ft = fences[ft] or ft
local name = ft..idx
idx = idx + 1
local lang = "@"..ft:upper()
@@ -1239,7 +1274,7 @@ function M.stylize_markdown(bufnr, contents, opts)
apply_syntax_to_region(h.ft, h.start, h.finish)
last = h.finish + 1
end
- if last < #stripped then
+ if last <= #stripped then
apply_syntax_to_region("lsp_markdown", last, #stripped)
end
end)
@@ -1249,26 +1284,26 @@ end
--- Creates autocommands to close a preview window when events happen.
---
---@param events (table) list of events
---@param winnr (number) window id of preview window
---@see |autocmd-events|
+---@param events (table) list of events
+---@param winnr (number) window id of preview window
+---@see |autocmd-events|
function M.close_preview_autocmd(events, winnr)
if #events > 0 then
api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)")
end
end
---@internal
+---@internal
--- Computes size of float needed to show contents (with optional wrapping)
---
---@param contents table of lines to show in window
---@param opts dictionary with optional fields
--- - height of floating window
--- - width of floating window
--- - wrap_at character to wrap at for computing height
--- - max_width maximal width of floating window
--- - max_height maximal height of floating window
---@returns width,height size of float
+---@param contents table of lines to show in window
+---@param opts dictionary with optional fields
+--- - height of floating window
+--- - width of floating window
+--- - wrap_at character to wrap at for computing height
+--- - max_width maximal width of floating window
+--- - max_height maximal height of floating window
+---@returns width,height size of float
function M._make_floating_popup_size(contents, opts)
validate {
contents = { contents, 't' };
@@ -1335,9 +1370,9 @@ end
--- Shows contents in a floating window.
---
---@param contents table of lines to show in window
---@param syntax string of syntax to set for opened buffer
---@param opts dictionary with optional fields
+---@param contents table of lines to show in window
+---@param syntax string of syntax to set for opened buffer
+---@param opts dictionary with optional fields
--- - height of floating window
--- - width of floating window
--- - wrap boolean enable wrapping of long lines (defaults to true)
@@ -1351,7 +1386,7 @@ end
--- - focus_id if a popup with this id is opened, then focus it
--- - close_events list of events that closes the floating window
--- - focusable (boolean, default true): Make float focusable
---@returns bufnr,winnr buffer and window number of the newly created floating
+---@returns bufnr,winnr buffer and window number of the newly created floating
---preview window
function M.open_floating_preview(contents, syntax, opts)
validate {
@@ -1447,7 +1482,7 @@ do --[[ References ]]
--- Removes document highlights from a buffer.
---
- --@param bufnr buffer id
+ ---@param bufnr buffer id
function M.buf_clear_references(bufnr)
validate { bufnr = {bufnr, 'n', true} }
api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1)
@@ -1455,8 +1490,9 @@ do --[[ References ]]
--- Shows a list of document highlights for a certain buffer.
---
- --@param bufnr buffer id
- --@param references List of `DocumentHighlight` objects to highlight
+ ---@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
@@ -1477,24 +1513,24 @@ local position_sort = sort_by_key(function(v)
return {v.start.line, v.start.character}
end)
--- Gets the zero-indexed line from the given uri.
+--- Gets the zero-indexed line from the given uri.
+---@param uri string uri of the resource to get the line from
+---@param row number zero-indexed line number
+---@return string the line at row in filename
-- For non-file uris, we load the buffer and get the line.
-- If a loaded buffer exists, then that is used.
-- Otherwise we get the line using libuv which is a lot faster than loading the buffer.
---@param uri string uri of the resource to get the line from
---@param row number zero-indexed line number
---@return string the line at row in filename
function M.get_line(uri, row)
return M.get_lines(uri, { row })[row]
end
--- Gets the zero-indexed lines from the given uri.
+--- Gets the zero-indexed lines from the given uri.
+---@param uri string uri of the resource to get the lines from
+---@param rows number[] zero-indexed line numbers
+---@return table<number string> a table mapping rows to lines
-- For non-file uris, we load the buffer and get the lines.
-- If a loaded buffer exists, then that is used.
-- Otherwise we get the lines using libuv which is a lot faster than loading the buffer.
---@param uri string uri of the resource to get the lines from
---@param rows number[] zero-indexed line numbers
---@return table<number string> a table mapping rows to lines
function M.get_lines(uri, rows)
rows = type(rows) == "table" and rows or { rows }
@@ -1562,8 +1598,8 @@ end
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
---@param locations (table) list of `Location`s or `LocationLink`s
---@returns (table) list of items
+---@param locations (table) list of `Location`s or `LocationLink`s
+---@returns (table) list of items
function M.locations_to_items(locations)
local items = {}
local grouped = setmetatable({}, {
@@ -1620,7 +1656,7 @@ end
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
--- Defaults to current window.
---
---@param items (table) list of items
+---@param items (table) list of items
function M.set_loclist(items, win_id)
vim.fn.setloclist(win_id or 0, {}, ' ', {
title = 'Language Server';
@@ -1631,7 +1667,7 @@ end
--- Fills quickfix list with given list of items.
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
---
---@param items (table) list of items
+---@param items (table) list of items
function M.set_qflist(items)
vim.fn.setqflist({}, ' ', {
title = 'Language Server';
@@ -1648,9 +1684,9 @@ end
--- Converts symbols to quickfix list items.
---
---@param symbols DocumentSymbol[] or SymbolInformation[]
+---@param symbols DocumentSymbol[] or SymbolInformation[]
function M.symbols_to_items(symbols, bufnr)
- --@private
+ ---@private
local function _symbols_to_items(_symbols, _items, _bufnr)
for _, symbol in ipairs(_symbols) do
if symbol.location then -- SymbolInformation type
@@ -1686,19 +1722,19 @@ function M.symbols_to_items(symbols, bufnr)
end
--- Removes empty lines from the beginning and end.
---@param lines (table) list of lines to trim
---@returns (table) trimmed list of lines
+---@param lines (table) list of lines to trim
+---@returns (table) trimmed list of lines
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
@@ -1711,8 +1747,8 @@ end
---
--- CAUTION: Modifies the input in-place!
---
---@param lines (table) list of lines
---@returns (string) filetype or 'markdown' if it was unchanged.
+---@param lines (table) list of lines
+---@returns (string) filetype or 'markdown' if it was unchanged.
function M.try_trim_markdown_code_blocks(lines)
local language_id = lines[1]:match("^```(.*)")
if language_id then
@@ -1735,7 +1771,7 @@ function M.try_trim_markdown_code_blocks(lines)
end
local str_utfindex = vim.str_utfindex
---@private
+---@private
local function make_position_param()
local row, col = unpack(api.nvim_win_get_cursor(0))
row = row - 1
@@ -1749,8 +1785,8 @@ end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
---@returns `TextDocumentPositionParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
+---@returns `TextDocumentPositionParams` object
+---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
function M.make_position_params()
return {
textDocument = M.make_text_document_params();
@@ -1763,7 +1799,7 @@ end
--- `textDocument/codeAction`, `textDocument/colorPresentation`,
--- `textDocument/rangeFormatting`.
---
---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
+---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
---`current_position`, end = `current_position` } }
function M.make_range_params()
local position = make_position_param()
@@ -1776,11 +1812,11 @@ end
--- Using the given range in the current buffer, creates an object that
--- is similar to |vim.lsp.util.make_range_params()|.
---
---@param start_pos ({number, number}, optional) mark-indexed position.
+---@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.
+---@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
+---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
---`start_position`, end = `end_position` } }
function M.make_given_range_params(start_pos, end_pos)
validate {
@@ -1816,23 +1852,23 @@ end
--- Creates a `TextDocumentIdentifier` object for the current buffer.
---
---@returns `TextDocumentIdentifier`
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
+---@returns `TextDocumentIdentifier`
+---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
function M.make_text_document_params()
return { uri = vim.uri_from_bufnr(0) }
end
--- Create the workspace params
---@param added
---@param removed
+---@param added
+---@param removed
function M.make_workspace_params(added, removed)
return { event = { added = added; removed = removed; } }
end
--- Returns visual width of tabstop.
---
---@see |softtabstop|
---@param bufnr (optional, number): Buffer handle, defaults to current
---@returns (number) tabstop visual width
+---@see |softtabstop|
+---@param bufnr (optional, number): Buffer handle, defaults to current
+---@returns (number) tabstop visual width
function M.get_effective_tabstop(bufnr)
validate { bufnr = {bufnr, 'n', true} }
local bo = bufnr and vim.bo[bufnr] or vim.bo
@@ -1840,11 +1876,11 @@ 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
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
+---@param options Table with valid `FormattingOptions` entries
+---@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} }
options = vim.tbl_extend('keep', options or {}, {
@@ -1859,10 +1895,10 @@ end
--- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer.
---
---@param buf buffer id (0 for current)
---@param row 0-indexed line
---@param col 0-indexed byte offset in line
---@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf}
+---@param buf buffer id (0 for current)
+---@param row 0-indexed line
+---@param col 0-indexed byte offset in line
+---@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf}
function M.character_offset(bufnr, row, col)
local uri = vim.uri_from_bufnr(bufnr)
local line = M.get_line(uri, row)
@@ -1875,9 +1911,9 @@ end
--- Helper function to return nested values in language server settings
---
---@param settings a table of language server settings
---@param section a string indicating the field of the settings table
---@returns (table or string) The value of settings accessed via section
+---@param settings a table of language server settings
+---@param section a string indicating the field of the settings table
+---@returns (table or string) The value of settings accessed via section
function M.lookup_section(settings, section)
for part in vim.gsplit(section, '.', true) do
settings = settings[part]
@@ -1892,10 +1928,10 @@ end
--- Convert diagnostics grouped by bufnr to a list of items for use in the
--- quickfix or location list.
---
---@param diagnostics_by_bufnr table bufnr -> Diagnostic[]
---@param predicate an optional function to filter the diagnostics.
--- If present, only diagnostic items matching will be included.
---@return table (A list of items)
+---@param diagnostics_by_bufnr table bufnr -> Diagnostic[]
+---@param predicate an optional function to filter the diagnostics.
+--- If present, only diagnostic items matching will be included.
+---@return table (A list of items)
function M.diagnostics_to_items(diagnostics_by_bufnr, predicate)
local items = {}
for bufnr, diagnostics in pairs(diagnostics_by_bufnr or {}) do