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/_snippet.lua399
-rw-r--r--runtime/lua/vim/lsp/buf.lua281
-rw-r--r--runtime/lua/vim/lsp/codelens.lua57
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua1448
-rw-r--r--runtime/lua/vim/lsp/handlers.lua287
-rw-r--r--runtime/lua/vim/lsp/health.lua27
-rw-r--r--runtime/lua/vim/lsp/log.lua46
-rw-r--r--runtime/lua/vim/lsp/protocol.lua24
-rw-r--r--runtime/lua/vim/lsp/rpc.lua168
-rw-r--r--runtime/lua/vim/lsp/util.lua674
10 files changed, 1745 insertions, 1666 deletions
diff --git a/runtime/lua/vim/lsp/_snippet.lua b/runtime/lua/vim/lsp/_snippet.lua
new file mode 100644
index 0000000000..0140b0aee3
--- /dev/null
+++ b/runtime/lua/vim/lsp/_snippet.lua
@@ -0,0 +1,399 @@
+local P = {}
+
+---Take characters until the target characters (The escape sequence is '\' + char)
+---@param targets string[] The character list for stop consuming text.
+---@param specials string[] If the character isn't contained in targets/specials, '\' will be left.
+P.take_until = function(targets, specials)
+ targets = targets or {}
+ specials = specials or {}
+
+ return function(input, pos)
+ local new_pos = pos
+ local raw = {}
+ local esc = {}
+ while new_pos <= #input do
+ local c = string.sub(input, new_pos, new_pos)
+ if c == '\\' then
+ table.insert(raw, '\\')
+ new_pos = new_pos + 1
+ c = string.sub(input, new_pos, new_pos)
+ if not vim.tbl_contains(targets, c) and not vim.tbl_contains(specials, c) then
+ table.insert(esc, '\\')
+ end
+ table.insert(raw, c)
+ table.insert(esc, c)
+ new_pos = new_pos + 1
+ else
+ if vim.tbl_contains(targets, c) then
+ break
+ end
+ table.insert(raw, c)
+ table.insert(esc, c)
+ new_pos = new_pos + 1
+ end
+ end
+
+ if new_pos == pos then
+ return P.unmatch(pos)
+ end
+
+ return {
+ parsed = true,
+ value = {
+ raw = table.concat(raw, ''),
+ esc = table.concat(esc, '')
+ },
+ pos = new_pos,
+ }
+ end
+end
+
+P.unmatch = function(pos)
+ return {
+ parsed = false,
+ value = nil,
+ pos = pos,
+ }
+end
+
+P.map = function(parser, map)
+ return function(input, pos)
+ local result = parser(input, pos)
+ if result.parsed then
+ return {
+ parsed = true,
+ value = map(result.value),
+ pos = result.pos,
+ }
+ end
+ return P.unmatch(pos)
+ end
+end
+
+P.lazy = function(factory)
+ return function(input, pos)
+ return factory()(input, pos)
+ end
+end
+
+P.token = function(token)
+ return function(input, pos)
+ local maybe_token = string.sub(input, pos, pos + #token - 1)
+ if token == maybe_token then
+ return {
+ parsed = true,
+ value = maybe_token,
+ pos = pos + #token,
+ }
+ end
+ return P.unmatch(pos)
+ end
+end
+
+P.pattern = function(p)
+ return function(input, pos)
+ local maybe_match = string.match(string.sub(input, pos), '^' .. p)
+ if maybe_match then
+ return {
+ parsed = true,
+ value = maybe_match,
+ pos = pos + #maybe_match,
+ }
+ end
+ return P.unmatch(pos)
+ end
+end
+
+P.many = function(parser)
+ return function(input, pos)
+ local values = {}
+ local new_pos = pos
+ while new_pos <= #input do
+ local result = parser(input, new_pos)
+ if not result.parsed then
+ break
+ end
+ table.insert(values, result.value)
+ new_pos = result.pos
+ end
+ if #values > 0 then
+ return {
+ parsed = true,
+ value = values,
+ pos = new_pos,
+ }
+ end
+ return P.unmatch(pos)
+ end
+end
+
+P.any = function(...)
+ local parsers = { ... }
+ return function(input, pos)
+ for _, parser in ipairs(parsers) do
+ local result = parser(input, pos)
+ if result.parsed then
+ return result
+ end
+ end
+ return P.unmatch(pos)
+ end
+end
+
+P.opt = function(parser)
+ return function(input, pos)
+ local result = parser(input, pos)
+ return {
+ parsed = true,
+ value = result.value,
+ pos = result.pos,
+ }
+ end
+end
+
+P.seq = function(...)
+ local parsers = { ... }
+ return function(input, pos)
+ local values = {}
+ local new_pos = pos
+ for _, parser in ipairs(parsers) do
+ local result = parser(input, new_pos)
+ if result.parsed then
+ table.insert(values, result.value)
+ new_pos = result.pos
+ else
+ return P.unmatch(pos)
+ end
+ end
+ return {
+ parsed = true,
+ value = values,
+ pos = new_pos,
+ }
+ end
+end
+
+local Node = {}
+
+Node.Type = {
+ SNIPPET = 0,
+ TABSTOP = 1,
+ PLACEHOLDER = 2,
+ VARIABLE = 3,
+ CHOICE = 4,
+ TRANSFORM = 5,
+ FORMAT = 6,
+ TEXT = 7,
+}
+
+function Node:__tostring()
+ local insert_text = {}
+ if self.type == Node.Type.SNIPPET then
+ for _, c in ipairs(self.children) do
+ table.insert(insert_text, tostring(c))
+ end
+ elseif self.type == Node.Type.CHOICE then
+ table.insert(insert_text, self.items[1])
+ elseif self.type == Node.Type.PLACEHOLDER then
+ for _, c in ipairs(self.children or {}) do
+ table.insert(insert_text, tostring(c))
+ end
+ elseif self.type == Node.Type.TEXT then
+ table.insert(insert_text, self.esc)
+ end
+ return table.concat(insert_text, '')
+end
+
+--@see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar
+
+local S = {}
+S.dollar = P.token('$')
+S.open = P.token('{')
+S.close = P.token('}')
+S.colon = P.token(':')
+S.slash = P.token('/')
+S.comma = P.token(',')
+S.pipe = P.token('|')
+S.plus = P.token('+')
+S.minus = P.token('-')
+S.question = P.token('?')
+S.int = P.map(P.pattern('[0-9]+'), function(value)
+ return tonumber(value, 10)
+end)
+S.var = P.pattern('[%a_][%w_]+')
+S.text = function(targets, specials)
+ return P.map(P.take_until(targets, specials), function(value)
+ return setmetatable({
+ type = Node.Type.TEXT,
+ raw = value.raw,
+ esc = value.esc,
+ }, Node)
+ end)
+end
+
+S.toplevel = P.lazy(function()
+ return P.any(S.placeholder, S.tabstop, S.variable, S.choice)
+end)
+
+S.format = P.any(
+ P.map(P.seq(S.dollar, S.int), function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[2],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.slash, P.any(
+ P.token('upcase'),
+ P.token('downcase'),
+ P.token('capitalize'),
+ P.token('camelcase'),
+ P.token('pascalcase')
+ ), S.close), function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ modifier = values[6],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.any(
+ P.seq(S.question, P.take_until({ ':' }, { '\\' }), S.colon, P.take_until({ '}' }, { '\\' })),
+ P.seq(S.plus, P.take_until({ '}' }, { '\\' })),
+ P.seq(S.minus, P.take_until({ '}' }, { '\\' }))
+ ), S.close), function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ if_text = values[5][2].esc,
+ else_text = (values[5][4] or {}).esc,
+ }, Node)
+ end)
+)
+
+S.transform = P.map(P.seq(
+ S.slash,
+ P.take_until({ '/' }, { '\\' }),
+ S.slash,
+ P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))),
+ S.slash,
+ P.opt(P.pattern('[ig]+'))
+), function(values)
+ return setmetatable({
+ type = Node.Type.TRANSFORM,
+ pattern = values[2].raw,
+ format = values[4],
+ option = values[6],
+ }, Node)
+end)
+
+S.tabstop = P.any(
+ P.map(P.seq(S.dollar, S.int), function(values)
+ return setmetatable({
+ type = Node.Type.TABSTOP,
+ tabstop = values[2],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
+ return setmetatable({
+ type = Node.Type.TABSTOP,
+ tabstop = values[3],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.int, S.transform, S.close), function(values)
+ return setmetatable({
+ type = Node.Type.TABSTOP,
+ tabstop = values[3],
+ transform = values[4],
+ }, Node)
+ end)
+)
+
+S.placeholder = P.any(
+ P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values)
+ return setmetatable({
+ type = Node.Type.PLACEHOLDER,
+ tabstop = values[3],
+ children = values[5],
+ }, Node)
+ end)
+)
+
+S.choice = P.map(P.seq(
+ S.dollar,
+ S.open,
+ S.int,
+ S.pipe,
+ P.many(
+ P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values)
+ return values[1].esc
+ end)
+ ),
+ S.pipe,
+ S.close
+), function(values)
+ return setmetatable({
+ type = Node.Type.CHOICE,
+ tabstop = values[3],
+ items = values[5],
+ }, Node)
+end)
+
+S.variable = P.any(
+ P.map(P.seq(S.dollar, S.var), function(values)
+ return setmetatable({
+ type = Node.Type.VARIABLE,
+ name = values[2],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.var, S.close), function(values)
+ return setmetatable({
+ type = Node.Type.VARIABLE,
+ name = values[3],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.var, S.transform, S.close), function(values)
+ return setmetatable({
+ type = Node.Type.VARIABLE,
+ name = values[3],
+ transform = values[4],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.var, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values)
+ return setmetatable({
+ type = Node.Type.VARIABLE,
+ name = values[3],
+ children = values[5],
+ }, Node)
+ end)
+)
+
+S.snippet = P.map(P.many(P.any(S.toplevel, S.text({ '$' }, { '}', '\\' }))), function(values)
+ return setmetatable({
+ type = Node.Type.SNIPPET,
+ children = values,
+ }, Node)
+end)
+
+local M = {}
+
+---The snippet node type enum
+---@types table<string, number>
+M.NodeType = Node.Type
+
+---Parse snippet string and returns the AST
+---@param input string
+---@return table
+function M.parse(input)
+ local result = S.snippet(input, 1)
+ if not result.parsed then
+ error('snippet parsing failed.')
+ end
+ return result.value
+end
+
+return M
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index b13d662ccb..245f29943e 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,11 +146,11 @@ 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
@@ -168,9 +168,9 @@ 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
@@ -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,7 +215,7 @@ function M.formatting_seq_sync(options, timeout_ms, order)
end
-- loop through the clients and make synchronous formatting requests
- for _, client in ipairs(clients) do
+ for _, client in pairs(clients) do
if client.resolved_capabilities.document_formatting then
local params = util.make_formatting_params(options)
local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf())
@@ -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,29 +246,49 @@ 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()
params.context = context or {
includeDeclaration = true;
}
- params[vim.type_idx] = vim.types.dictionary
request('textDocument/references', params)
end
@@ -279,14 +299,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,16 +317,24 @@ local function pick_call_hierarchy_item(call_hierarchy_items)
return choice
end
---@private
+---@private
local function call_hierarchy(method)
local params = util.make_position_params()
- request('textDocument/prepareCallHierarchy', params, function(err, _, result)
+ request('textDocument/prepareCallHierarchy', params, function(err, result, ctx)
if err then
vim.notify(err.message, vim.log.levels.WARN)
return
end
local call_hierarchy_item = pick_call_hierarchy_item(result)
- vim.lsp.buf_request(0, method, { item = call_hierarchy_item })
+ local client = vim.lsp.get_client_by_id(ctx.client_id)
+ if client then
+ client.request(method, { item = call_hierarchy_item }, nil, ctx.bufnr)
+ else
+ vim.notify(string.format(
+ 'Client with id=%d disappeared during call hierarchy request', ctx.client_id),
+ vim.log.levels.WARN
+ )
+ end
end)
end
@@ -328,8 +356,8 @@ end
---
function M.list_workspace_folders()
local workspace_folders = {}
- for _, client in ipairs(vim.lsp.buf_get_clients()) do
- for _, folder in ipairs(client.workspaceFolders) do
+ for _, client in pairs(vim.lsp.buf_get_clients()) do
+ for _, folder in pairs(client.workspaceFolders) do
table.insert(workspace_folders, folder.name)
end
end
@@ -347,9 +375,9 @@ function M.add_workspace_folder(workspace_folder)
return
end
local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}})
- for _, client in ipairs(vim.lsp.buf_get_clients()) do
+ for _, client in pairs(vim.lsp.buf_get_clients()) do
local found = false
- for _, folder in ipairs(client.workspaceFolders) do
+ for _, folder in pairs(client.workspaceFolders) do
if folder.name == workspace_folder then
found = true
print(workspace_folder, "is already part of this workspace")
@@ -371,8 +399,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
@@ -389,7 +417,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}
@@ -422,38 +450,157 @@ function M.clear_references()
util.buf_clear_references()
end
---- Selects a code action from the input list that is available at the current
---- cursor position.
+
+---@private
--
---@param context: (table, optional) Valid `CodeActionContext` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
+--- This is not public because the main extension point is
+--- vim.ui.select which can be overridden independently.
+---
+--- Can't call/use vim.lsp.handlers['textDocument/codeAction'] because it expects
+--- `(err, CodeAction[] | Command[], ctx)`, but we want to aggregate the results
+--- from multiple clients to have 1 single UI prompt for the user, yet we still
+--- need to be able to link a `CodeAction|Command` to the right client for
+--- `codeAction/resolve`
+local function on_code_action_results(results, ctx)
+ local action_tuples = {}
+ for client_id, result in pairs(results) do
+ for _, action in pairs(result.result or {}) do
+ table.insert(action_tuples, { client_id, action })
+ end
+ end
+ if #action_tuples == 0 then
+ vim.notify('No code actions available', vim.log.levels.INFO)
+ return
+ end
+
+ ---@private
+ local function apply_action(action, client)
+ if action.edit then
+ util.apply_workspace_edit(action.edit)
+ end
+ if action.command then
+ local command = type(action.command) == 'table' and action.command or action
+ local fn = vim.lsp.commands[command.command]
+ if fn then
+ local enriched_ctx = vim.deepcopy(ctx)
+ enriched_ctx.client_id = client.id
+ fn(command, ctx)
+ else
+ M.execute_command(command)
+ end
+ end
+ end
+
+ ---@private
+ local function on_user_choice(action_tuple)
+ if not action_tuple then
+ return
+ end
+ -- textDocument/codeAction can return either Command[] or CodeAction[]
+ --
+ -- CodeAction
+ -- ...
+ -- edit?: WorkspaceEdit -- <- must be applied before command
+ -- command?: Command
+ --
+ -- Command:
+ -- title: string
+ -- command: string
+ -- arguments?: any[]
+ --
+ local client = vim.lsp.get_client_by_id(action_tuple[1])
+ local action = action_tuple[2]
+ if not action.edit
+ and client
+ and type(client.resolved_capabilities.code_action) == 'table'
+ and client.resolved_capabilities.code_action.resolveProvider then
+
+ client.request('codeAction/resolve', action, function(err, resolved_action)
+ if err then
+ vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
+ return
+ end
+ apply_action(resolved_action, client)
+ end)
+ else
+ apply_action(action, client)
+ end
+ end
+
+ vim.ui.select(action_tuples, {
+ prompt = 'Code actions:',
+ format_item = function(action_tuple)
+ local title = action_tuple[2].title:gsub('\r\n', '\\r\\n')
+ return title:gsub('\n', '\\n')
+ end,
+ }, on_user_choice)
+end
+
+
+--- Requests code actions from all clients and calls the handler exactly once
+--- with all aggregated results
+---@private
+local function code_action_request(params)
+ local bufnr = vim.api.nvim_get_current_buf()
+ local method = 'textDocument/codeAction'
+ vim.lsp.buf_request_all(bufnr, method, params, function(results)
+ on_code_action_results(results, { bufnr = bufnr, method = method, params = params })
+ end)
+end
+
+--- Selects a code action available at the current
+--- cursor position.
+---
+---@param context table|nil `CodeActionContext` of the LSP specification:
+--- - diagnostics: (table|nil)
+--- LSP `Diagnostic[]`. Inferred from the current
+--- position if not provided.
+--- - only: (string|nil)
+--- LSP `CodeActionKind` used to filter the code actions.
+--- Most language servers support values like `refactor`
+--- or `quickfix`.
+---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
function M.code_action(context)
validate { context = { context, 't', true } }
- context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
+ context = context or {}
+ if not context.diagnostics then
+ context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
+ end
local params = util.make_range_params()
params.context = context
- 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|nil `CodeActionContext` of the LSP specification:
+--- - diagnostics: (table|nil)
+--- LSP `Diagnostic[]`. Inferred from the current
+--- position if not provided.
+--- - only: (string|nil)
+--- LSP `CodeActionKind` used to filter the code actions.
+--- Most language servers support values like `refactor`
+--- or `quickfix`.
+---@param start_pos ({number, number}, optional) mark-indexed position.
---Defaults to the start of the last visual selection.
---@param end_pos ({number, number}, optional) mark-indexed position.
+---@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() }
+ context = context or {}
+ if not context.diagnostics then
+ context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
+ end
local params = util.make_given_range_params(start_pos, end_pos)
params.context = context
- 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
index fbd37e3830..20b203fe99 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -22,19 +22,33 @@ local namespaces = setmetatable({}, {
end;
})
---@private
+---@private
M.__namespaces = namespaces
---@private
+---@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)
+ local command = lens.command
+ local fn = vim.lsp.commands[command.command]
+ if fn then
+ fn(command, { bufnr = bufnr, client_id = client_id })
+ return
+ end
-- Need to use the client that returned the lens → must not use buf_request
local client = vim.lsp.get_client_by_id(client_id)
assert(client, 'Client is required to execute lens, client_id=' .. client_id)
- client.request('workspace/executeCommand', lens.command, function(...)
+ local command_provider = client.server_capabilities.executeCommandProvider
+ local commands = type(command_provider) == 'table' and command_provider.commands or {}
+ if not vim.tbl_contains(commands, command.command) then
+ vim.notify(string.format(
+ "Language server does not support command `%s`. This command may require a client extension.", command.command),
+ vim.log.levels.WARN)
+ return
+ end
+ client.request('workspace/executeCommand', command, function(...)
local result = vim.lsp.handlers['workspace/executeCommand'](...)
M.refresh()
return result
@@ -44,9 +58,10 @@ end
--- Return all lenses for the given buffer
---
+---@param bufnr number Buffer number. 0 can be used for the current buffer.
---@return table (`CodeLens[]`)
function M.get(bufnr)
- local lenses_by_client = lens_cache_by_buf[bufnr]
+ local lenses_by_client = lens_cache_by_buf[bufnr or 0]
if not lenses_by_client then return {} end
local lenses = {}
for _, client_lenses in pairs(lenses_by_client) do
@@ -111,15 +126,19 @@ function M.display(lenses, bufnr, client_id)
local ns = namespaces[client_id]
local num_lines = api.nvim_buf_line_count(bufnr)
for i = 0, num_lines do
- local line_lenses = lenses_by_lnum[i]
+ local line_lenses = lenses_by_lnum[i] or {}
api.nvim_buf_clear_namespace(bufnr, ns, i, i + 1)
local chunks = {}
- for _, lens in pairs(line_lenses or {}) do
+ local num_line_lenses = #line_lenses
+ for j, lens in ipairs(line_lenses) do
local text = lens.command and lens.command.title or 'Unresolved lens ...'
table.insert(chunks, {text, 'LspCodeLens' })
+ if j < num_line_lenses then
+ table.insert(chunks, {' | ', 'LspCodeLensSeparator' })
+ end
end
if #chunks > 0 then
- api.nvim_buf_set_virtual_text(bufnr, ns, i, chunks, {})
+ api.nvim_buf_set_extmark(bufnr, ns, i, 0, { virt_text = chunks })
end
end
end
@@ -147,7 +166,7 @@ function M.save(lenses, bufnr, client_id)
end
---@private
+---@private
local function resolve_lenses(lenses, bufnr, client_id, callback)
lenses = lenses or {}
local num_lens = vim.tbl_count(lenses)
@@ -156,7 +175,7 @@ local function resolve_lenses(lenses, bufnr, client_id, callback)
return
end
- --@private
+ ---@private
local function countdown()
num_lens = num_lens - 1
if num_lens == 0 then
@@ -169,18 +188,18 @@ local function resolve_lenses(lenses, bufnr, client_id, callback)
if lens.command then
countdown()
else
- client.request('codeLens/resolve', lens, function(_, _, result)
+ client.request('codeLens/resolve', lens, function(_, result)
if result and result.command then
lens.command = result.command
-- Eager display to have some sort of incremental feedback
-- Once all lenses got resolved there will be a full redraw for all lenses
-- So that multiple lens per line are properly displayed
- api.nvim_buf_set_virtual_text(
+ api.nvim_buf_set_extmark(
bufnr,
ns,
lens.range.start.line,
- {{ lens.command.title, 'LspCodeLens' },},
- {}
+ 0,
+ { virt_text = {{ lens.command.title, 'LspCodeLens' }} }
)
end
countdown()
@@ -192,17 +211,17 @@ end
--- |lsp-handler| for the method `textDocument/codeLens`
---
-function M.on_codelens(err, _, result, client_id, bufnr)
+function M.on_codelens(err, result, ctx, _)
assert(not err, vim.inspect(err))
- M.save(result, bufnr, client_id)
+ M.save(result, ctx.bufnr, ctx.client_id)
-- Eager display for any resolved (and unresolved) lenses and refresh them
-- once resolved.
- M.display(result, bufnr, client_id)
- resolve_lenses(result, bufnr, client_id, function()
- M.display(result, bufnr, client_id)
- active_refreshes[bufnr] = nil
+ M.display(result, ctx.bufnr, ctx.client_id)
+ resolve_lenses(result, ctx.bufnr, ctx.client_id, function()
+ M.display(result, ctx.bufnr, ctx.client_id)
+ active_refreshes[ctx.bufnr] = nil
end)
end
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 64dde78f17..c6c08a15d3 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -1,126 +1,19 @@
-local api = vim.api
-local validate = vim.validate
-
-local highlight = vim.highlight
-local log = require('vim.lsp.log')
-local protocol = require('vim.lsp.protocol')
-local util = require('vim.lsp.util')
-
-local if_nil = vim.F.if_nil
-
---@class DiagnosticSeverity
-local DiagnosticSeverity = protocol.DiagnosticSeverity
-
-local to_severity = function(severity)
- if not severity then return nil end
- return type(severity) == 'string' and DiagnosticSeverity[severity] or severity
-end
-
-local filter_to_severity_limit = function(severity, diagnostics)
- local filter_level = to_severity(severity)
- if not filter_level then
- return diagnostics
- end
-
- return vim.tbl_filter(function(t) return t.severity == filter_level end, diagnostics)
-end
-
-local filter_by_severity_limit = function(severity_limit, diagnostics)
- local filter_level = to_severity(severity_limit)
- if not filter_level then
- return diagnostics
- end
-
- return vim.tbl_filter(function(t) return t.severity <= filter_level end, diagnostics)
-end
-
-local to_position = function(position, bufnr)
- vim.validate { position = {position, 't'} }
-
- return {
- position.line,
- util._get_line_byte_from_position(bufnr, position)
- }
-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 = {}
--- Diagnostic Highlights {{{
-
--- TODO(tjdevries): Determine how to generate documentation for these
--- and how to configure them to be easy for users.
---
--- For now, just use the following script. It should work pretty good.
---[[
-local levels = {"Error", "Warning", "Information", "Hint" }
-
-local all_info = {
- { "Default", "Used as the base highlight group, other highlight groups link to", },
- { "VirtualText", 'Used for "%s" diagnostic virtual text.\n See |vim.lsp.diagnostic.set_virtual_text()|', },
- { "Underline", 'Used to underline "%s" diagnostics.\n See |vim.lsp.diagnostic.set_underline()|', },
- { "Floating", 'Used to color "%s" diagnostic messages in diagnostics float.\n See |vim.lsp.diagnostic.show_line_diagnostics()|', },
- { "Sign", 'Used for "%s" signs in sing column.\n See |vim.lsp.diagnostic.set_signs()|', },
-}
-
-local results = {}
-for _, info in ipairs(all_info) do
- for _, level in ipairs(levels) do
- local name = info[1]
- local description = info[2]
- local fullname = string.format("Lsp%s%s", name, level)
- table.insert(results, string.format(
- "%78s", string.format("*hl-%s*", fullname))
- )
-
- table.insert(results, fullname)
- table.insert(results, string.format(" %s", description))
- table.insert(results, "")
- end
-end
-
--- print(table.concat(results, '\n'))
-vim.fn.setreg("*", table.concat(results, '\n'))
---]]
-
-local diagnostic_severities = {
- [DiagnosticSeverity.Error] = { guifg = "Red" };
- [DiagnosticSeverity.Warning] = { guifg = "Orange" };
- [DiagnosticSeverity.Information] = { guifg = "LightBlue" };
- [DiagnosticSeverity.Hint] = { guifg = "LightGrey" };
-}
-
--- Make a map from DiagnosticSeverity -> Highlight Name
-local make_highlight_map = function(base_name)
- local result = {}
- for k, _ in pairs(diagnostic_severities) do
- result[k] = "LspDiagnostics" .. base_name .. DiagnosticSeverity[k]
- end
-
- return result
-end
-
-local default_highlight_map = make_highlight_map("Default")
-local virtual_text_highlight_map = make_highlight_map("VirtualText")
-local underline_highlight_map = make_highlight_map("Underline")
-local floating_highlight_map = make_highlight_map("Floating")
-local sign_highlight_map = make_highlight_map("Sign")
-
--- }}}
--- Diagnostic Namespaces {{{
local DEFAULT_CLIENT_ID = -1
-local get_client_id = function(client_id)
+---@private
+local function get_client_id(client_id)
if client_id == nil then
client_id = DEFAULT_CLIENT_ID
end
@@ -128,472 +21,414 @@ local get_client_id = function(client_id)
return client_id
end
-local get_bufnr = function(bufnr)
+---@private
+local function get_bufnr(bufnr)
if not bufnr then
- return api.nvim_get_current_buf()
+ return vim.api.nvim_get_current_buf()
elseif bufnr == 0 then
- return api.nvim_get_current_buf()
+ return vim.api.nvim_get_current_buf()
end
return bufnr
end
-
---- Create a namespace table, used to track a client's buffer local items
-local _make_namespace_table = function(namespace, api_namespace)
- vim.validate { namespace = { namespace, 's' } }
-
- return setmetatable({
- [DEFAULT_CLIENT_ID] = api.nvim_create_namespace(namespace)
- }, {
- __index = function(t, client_id)
- client_id = get_client_id(client_id)
-
- if rawget(t, client_id) == nil then
- local value = string.format("%s:%s", namespace, client_id)
-
- if api_namespace then
- value = api.nvim_create_namespace(value)
- end
-
- rawset(t, client_id, value)
- end
-
- return rawget(t, client_id)
- end
- })
+---@private
+local function severity_lsp_to_vim(severity)
+ if type(severity) == 'string' then
+ severity = vim.lsp.protocol.DiagnosticSeverity[severity]
+ end
+ return severity
end
-local _diagnostic_namespaces = _make_namespace_table("vim_lsp_diagnostics", true)
-local _sign_namespaces = _make_namespace_table("vim_lsp_signs", false)
-
---@private
-function M._get_diagnostic_namespace(client_id)
- return _diagnostic_namespaces[client_id]
+---@private
+local function severity_vim_to_lsp(severity)
+ if type(severity) == 'string' then
+ severity = vim.diagnostic.severity[severity]
+ end
+ return severity
end
---@private
-function M._get_sign_namespace(client_id)
- return _sign_namespaces[client_id]
-end
--- }}}
--- Diagnostic Buffer & Client metatables {{{
-local bufnr_and_client_cacher_mt = {
- __index = function(t, bufnr)
- if bufnr == 0 or bufnr == nil then
- bufnr = vim.api.nvim_get_current_buf()
- end
+---@private
+local function line_byte_from_position(lines, lnum, col, offset_encoding)
+ if not lines or offset_encoding == "utf-8" then
+ return col
+ end
- if rawget(t, bufnr) == nil then
- rawset(t, bufnr, {})
- end
+ local line = lines[lnum + 1]
+ local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == "utf-16")
+ if ok then
+ return result
+ end
- return rawget(t, bufnr)
- end,
+ return col
+end
- __newindex = function(t, bufnr, v)
- if bufnr == 0 or bufnr == nil then
- bufnr = vim.api.nvim_get_current_buf()
- end
+---@private
+local function get_buf_lines(bufnr)
+ if vim.api.nvim_buf_is_loaded(bufnr) then
+ return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
+ end
- rawset(t, bufnr, v)
- end,
-}
--- }}}
--- Diagnostic Saving & Caching {{{
-local _diagnostic_cleanup = setmetatable({}, bufnr_and_client_cacher_mt)
-local diagnostic_cache = 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 filename = vim.api.nvim_buf_get_name(bufnr)
+ local f = io.open(filename)
+ if not f then
+ return
+ end
-local _bufs_waiting_to_update = setmetatable({}, bufnr_and_client_cacher_mt)
+ local content = f:read("*a")
+ if not content then
+ -- Some LSP servers report diagnostics at a directory level, in which case
+ -- io.read() returns nil
+ f:close()
+ return
+ end
---- Store Diagnostic[] by line
----
----@param diagnostics Diagnostic[]
----@return table<number, Diagnostic[]>
-local _diagnostic_lines = function(diagnostics)
- if not diagnostics then return end
+ local lines = vim.split(content, "\n")
+ f:close()
+ return lines
+end
- local diagnostics_by_line = {}
- for _, diagnostic in ipairs(diagnostics) do
+---@private
+local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
+ local buf_lines = get_buf_lines(bufnr)
+ local client = vim.lsp.get_client_by_id(client_id)
+ local offset_encoding = client and client.offset_encoding or "utf-16"
+ return vim.tbl_map(function(diagnostic)
local start = diagnostic.range.start
- local line_diagnostics = diagnostics_by_line[start.line]
- if not line_diagnostics then
- line_diagnostics = {}
- diagnostics_by_line[start.line] = line_diagnostics
- end
- table.insert(line_diagnostics, diagnostic)
+ local _end = diagnostic.range["end"]
+ return {
+ lnum = start.line,
+ col = line_byte_from_position(buf_lines, start.line, start.character, offset_encoding),
+ end_lnum = _end.line,
+ end_col = line_byte_from_position(buf_lines, _end.line, _end.character, offset_encoding),
+ severity = severity_lsp_to_vim(diagnostic.severity),
+ message = diagnostic.message,
+ source = diagnostic.source,
+ user_data = {
+ lsp = {
+ code = diagnostic.code,
+ codeDescription = diagnostic.codeDescription,
+ tags = diagnostic.tags,
+ relatedInformation = diagnostic.relatedInformation,
+ data = diagnostic.data,
+ },
+ },
+ }
+ end, diagnostics)
+end
+
+---@private
+local function diagnostic_vim_to_lsp(diagnostics)
+ return vim.tbl_map(function(diagnostic)
+ return vim.tbl_extend("error", {
+ range = {
+ start = {
+ line = diagnostic.lnum,
+ character = diagnostic.col,
+ },
+ ["end"] = {
+ line = diagnostic.end_lnum,
+ character = diagnostic.end_col,
+ },
+ },
+ severity = severity_vim_to_lsp(diagnostic.severity),
+ message = diagnostic.message,
+ source = diagnostic.source,
+ }, diagnostic.user_data and (diagnostic.user_data.lsp or {}) or {})
+ end, diagnostics)
+end
+
+local _client_namespaces = {}
+
+--- Get the diagnostic namespace associated with an LSP client |vim.diagnostic|.
+---
+---@param client_id number The id of the LSP client
+function M.get_namespace(client_id)
+ vim.validate { client_id = { client_id, 'n' } }
+ if not _client_namespaces[client_id] then
+ local name = string.format("vim.lsp.client-%d", client_id)
+ _client_namespaces[client_id] = vim.api.nvim_create_namespace(name)
end
- return diagnostics_by_line
+ return _client_namespaces[client_id]
end
---- Get the count of M by Severity
+--- Save diagnostics to the current buffer.
---
+--- Handles saving diagnostics from multiple clients in the same buffer.
---@param diagnostics Diagnostic[]
----@return table<DiagnosticSeverity, number>
-local _diagnostic_counts = function(diagnostics)
- if not diagnostics then return end
+---@param bufnr number
+---@param client_id number
+---@private
+function M.save(diagnostics, bufnr, client_id)
+ local namespace = M.get_namespace(client_id)
+ vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
+end
+-- }}}
- local counts = {}
- for _, diagnostic in pairs(diagnostics) do
- if diagnostic.severity then
- local val = counts[diagnostic.severity]
- if val == nil then
- val = 0
- end
+--- |lsp-handler| for the method "textDocument/publishDiagnostics"
+---
+--- See |vim.diagnostic.config()| for configuration options. Handler-specific
+--- configuration can be set using |vim.lsp.with()|:
+--- <pre>
+--- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
+--- vim.lsp.diagnostic.on_publish_diagnostics, {
+--- -- Enable underline, use default values
+--- underline = true,
+--- -- Enable virtual text, override spacing to 4
+--- virtual_text = {
+--- spacing = 4,
+--- },
+--- -- Use a function to dynamically turn signs off
+--- -- and on, using buffer local variables
+--- signs = function(bufnr, client_id)
+--- return vim.bo[bufnr].show_signs == false
+--- end,
+--- -- Disable a feature
+--- update_in_insert = false,
+--- }
+--- )
+--- </pre>
+---
+---@param config table Configuration table (see |vim.diagnostic.config()|).
+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)
- counts[diagnostic.severity] = val + 1
- end
+ if not bufnr then
+ return
end
- return counts
-end
-
---@private
---- Set the different diagnostic cache after `textDocument/publishDiagnostics`
----@param diagnostics Diagnostic[]
----@param bufnr number
----@param client_id number
----@return nil
-local function set_diagnostic_cache(diagnostics, bufnr, client_id)
client_id = get_client_id(client_id)
-
- -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
- --
- -- The diagnostic's severity. Can be omitted. If omitted it is up to the
- -- client to interpret diagnostics as error, warning, info or hint.
- -- TODO: Replace this with server-specific heuristics to infer severity.
- local buf_line_count = vim.api.nvim_buf_line_count(bufnr)
- for _, diagnostic in ipairs(diagnostics) do
- if diagnostic.severity == nil then
- diagnostic.severity = DiagnosticSeverity.Error
- end
- -- Account for servers that place diagnostics on terminating newline
- if buf_line_count > 0 then
- 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)
+ local namespace = M.get_namespace(client_id)
+ local diagnostics = result.diagnostics
+
+ if config then
+ for _, opt in pairs(config) do
+ if type(opt) == 'table' then
+ if not opt.severity and opt.severity_limit then
+ opt.severity = {min=severity_lsp_to_vim(opt.severity_limit)}
+ end
+ end
end
end
- diagnostic_cache[bufnr][client_id] = diagnostics
- diagnostic_cache_lines[bufnr][client_id] = _diagnostic_lines(diagnostics)
- diagnostic_cache_counts[bufnr][client_id] = _diagnostic_counts(diagnostics)
-end
-
+ vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), config)
---@private
---- Clear the cached diagnostics
----@param bufnr number
----@param client_id number
-local function clear_diagnostic_cache(bufnr, client_id)
- client_id = get_client_id(client_id)
-
- diagnostic_cache[bufnr][client_id] = nil
- diagnostic_cache_lines[bufnr][client_id] = nil
- diagnostic_cache_counts[bufnr][client_id] = nil
+ -- Keep old autocmd for back compat. This should eventually be removed.
+ vim.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged")
end
---- Save diagnostics to the current buffer.
+--- Clear diagnotics and diagnostic cache.
+---
+--- Diagnostic producers should prefer |vim.diagnostic.reset()|. However,
+--- this method signature is still used internally in some parts of the LSP
+--- implementation so it's simply marked @private rather than @deprecated.
---
---- Handles saving diagnostics from multiple clients in the same buffer.
----@param diagnostics Diagnostic[]
----@param bufnr number
---@param client_id number
-function M.save(diagnostics, bufnr, client_id)
- validate {
- diagnostics = {diagnostics, 't'},
- bufnr = {bufnr, 'n'},
- client_id = {client_id, 'n', true},
- }
-
- if not diagnostics then return end
-
- bufnr = get_bufnr(bufnr)
- client_id = get_client_id(client_id)
-
- if not _diagnostic_cleanup[bufnr][client_id] then
- _diagnostic_cleanup[bufnr][client_id] = true
-
- -- Clean up our data when the buffer unloads.
- api.nvim_buf_attach(bufnr, false, {
- on_detach = function(_, b)
- clear_diagnostic_cache(b, client_id)
- _diagnostic_cleanup[b][client_id] = nil
+---@param buffer_client_map table map of buffers to active clients
+---@private
+function M.reset(client_id, buffer_client_map)
+ buffer_client_map = vim.deepcopy(buffer_client_map)
+ vim.schedule(function()
+ for bufnr, client_ids in pairs(buffer_client_map) do
+ if client_ids[client_id] then
+ local namespace = M.get_namespace(client_id)
+ vim.diagnostic.reset(namespace, bufnr)
end
- })
- end
-
- set_diagnostic_cache(diagnostics, bufnr, client_id)
+ end
+ end)
end
--- }}}
--- Diagnostic Retrieval {{{
+-- Deprecated Functions {{{
--- Get all diagnostics for clients
---
+---@deprecated Prefer |vim.diagnostic.get()|
+---
---@param client_id number Restrict included diagnostics to the client
--- If nil, diagnostics of all clients are included.
---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[])
function M.get_all(client_id)
- local diagnostics_by_bufnr = {}
- for bufnr, buf_diagnostics in pairs(diagnostic_cache) do
- diagnostics_by_bufnr[bufnr] = {}
- for cid, client_diagnostics in pairs(buf_diagnostics) do
- if client_id == nil or cid == client_id then
- vim.list_extend(diagnostics_by_bufnr[bufnr], client_diagnostics)
- end
- end
+ local result = {}
+ local namespace
+ if client_id then
+ namespace = M.get_namespace(client_id)
end
- return diagnostics_by_bufnr
+ for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
+ local diagnostics = diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, {namespace = namespace}))
+ result[bufnr] = diagnostics
+ end
+ return result
end
--- Return associated diagnostics for bufnr
---
+---@deprecated Prefer |vim.diagnostic.get()|
+---
---@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)
+ predicate = predicate or function() return true end
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)
-
+ vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _)
+ local iter_diagnostics = vim.tbl_filter(predicate, M.get(bufnr, iter_client_id))
for _, diagnostic in ipairs(iter_diagnostics) do
table.insert(all_diagnostics, diagnostic)
end
- end
-
+ end)
return all_diagnostics
end
- return diagnostic_cache[bufnr][client_id] or {}
+ local namespace = M.get_namespace(client_id)
+ return diagnostic_vim_to_lsp(vim.tbl_filter(predicate, vim.diagnostic.get(bufnr, {namespace=namespace})))
end
--- Get the diagnostics by line
---
----@param bufnr number The buffer number
----@param line_nr number The line number
+--- Marked private as this is used internally by the LSP subsystem, but
+--- most users should instead prefer |vim.diagnostic.get()|.
+---
+---@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] = {.... } }
+--- Structured: { [1] = {...}, [5] = {.... } }
+---@private
function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
opts = opts or {}
-
- bufnr = bufnr or vim.api.nvim_get_current_buf()
- line_nr = line_nr or vim.api.nvim_win_get_cursor(0)[1] - 1
-
- local client_get_diags = function(iter_client_id)
- return (diagnostic_cache_lines[bufnr][iter_client_id] or {})[line_nr] or {}
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
- local line_diagnostics
- if client_id == nil then
- line_diagnostics = {}
- for iter_client_id, _ in pairs(diagnostic_cache_lines[bufnr]) do
- for _, diagnostic in ipairs(client_get_diags(iter_client_id)) do
- table.insert(line_diagnostics, diagnostic)
- end
- end
- else
- line_diagnostics = vim.deepcopy(client_get_diags(client_id))
+ if client_id then
+ opts.namespace = M.get_namespace(client_id)
end
- if opts.severity then
- line_diagnostics = filter_to_severity_limit(opts.severity, line_diagnostics)
- elseif opts.severity_limit then
- line_diagnostics = filter_by_severity_limit(opts.severity_limit, line_diagnostics)
+ if not line_nr then
+ line_nr = vim.api.nvim_win_get_cursor(0)[1] - 1
end
- table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end)
+ opts.lnum = line_nr
- return line_diagnostics
+ return diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, opts))
end
--- Get the counts for a particular severity
---
---- Useful for showing diagnostic counts in statusline. eg:
----
---- <pre>
---- function! LspStatus() abort
---- let sl = ''
---- if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))')
---- let sl.='%#MyStatuslineLSP#E:'
---- let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.diagnostic.get_count(0, [[Error]])")}'
---- let sl.='%#MyStatuslineLSP# W:'
---- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.diagnostic.get_count(0, [[Warning]])")}'
---- else
---- let sl.='%#MyStatuslineLSPErrors#off'
---- endif
---- return sl
---- endfunction
---- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
---- </pre>
+---@deprecated Prefer |vim.diagnostic.get_count()|
---
---@param bufnr number The buffer number
---@param severity DiagnosticSeverity
---@param client_id number the client id
function M.get_count(bufnr, severity, client_id)
- if client_id == nil then
- local total = 0
- for iter_client_id, _ in pairs(diagnostic_cache_counts[bufnr]) do
- total = total + M.get_count(bufnr, severity, iter_client_id)
- end
-
- return total
+ severity = severity_lsp_to_vim(severity)
+ local opts = { severity = severity }
+ if client_id ~= nil then
+ opts.namespace = M.get_namespace(client_id)
end
- return (diagnostic_cache_counts[bufnr][client_id] or {})[DiagnosticSeverity[severity]] or 0
-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
-
- 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
- end
- 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)
- end
- end
-
- return result
-end
-
---@private
---- Helper function to ierate through diagnostic lines and return a position
----
----@return table {row, col}
-local function _iter_diagnostic_lines_pos(opts, line_diagnostics)
- 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
-
- local iter_diagnostic = line_diagnostics[1]
- return to_position(iter_diagnostic.range.start, bufnr)
-end
-
---@private
--- Move to the diagnostic position
-local function _iter_diagnostic_move_pos(name, opts, pos)
- opts = opts or {}
-
- local enable_popup = if_nil(opts.enable_popup, true)
- local win_id = opts.win_id or vim.api.nvim_get_current_win()
-
- if not pos then
- print(string.format("%s: No more valid diagnostics to move to.", name))
- return
- end
-
- vim.api.nvim_win_set_cursor(win_id, {pos[1] + 1, pos[2]})
-
- 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))
- end)
- end
+ return #vim.diagnostic.get(bufnr, opts)
end
--- Get the previous diagnostic closest to the cursor_position
---
+---@deprecated Prefer |vim.diagnostic.get_prev()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic
function M.get_prev(opts)
- 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)
- 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)
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return diagnostic_vim_to_lsp({vim.diagnostic.get_prev(opts)})[1]
end
--- Return the pos, {row, col}, for the prev diagnostic in the current buffer.
+---
+---@deprecated Prefer |vim.diagnostic.get_prev_pos()|
+---
---@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(
- opts,
- M.get_prev(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.get_prev_pos(opts)
end
--- Move to the previous diagnostic
+---
+---@deprecated Prefer |vim.diagnostic.goto_prev()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
function M.goto_prev(opts)
- return _iter_diagnostic_move_pos(
- "DiagnosticPrevious",
- opts,
- M.get_prev_pos(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.goto_prev(opts)
end
--- Get the next diagnostic closest to the cursor_position
+---
+---@deprecated Prefer |vim.diagnostic.get_next()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic
function M.get_next(opts)
- 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)
- 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)
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return diagnostic_vim_to_lsp({vim.diagnostic.get_next(opts)})[1]
end
--- Return the pos, {row, col}, for the next diagnostic in the current buffer.
+---
+---@deprecated Prefer |vim.diagnostic.get_next_pos()|
+---
---@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(
- opts,
- M.get_next(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.get_next_pos(opts)
end
--- Move to the next diagnostic
+---
+---@deprecated Prefer |vim.diagnostic.goto_next()|
+---
---@param opts table|nil Configuration table. Keys:
--- - {client_id}: (number)
--- - If nil, will consider all clients attached to buffer.
@@ -612,25 +447,20 @@ end
--- - {win_id}: (number, default 0)
--- - Window ID
function M.goto_next(opts)
- return _iter_diagnostic_move_pos(
- "DiagnosticNext",
- opts,
- M.get_next_pos(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.goto_next(opts)
end
--- }}}
--- Diagnostic Setters {{{
--- Set signs for given diagnostics
---
---- Sign characters can be customized with the following commands:
+---@deprecated Prefer |vim.diagnostic._set_signs()|
---
---- <pre>
---- sign define LspDiagnosticsSignError text=E texthl=LspDiagnosticsSignError linehl= numhl=
---- sign define LspDiagnosticsSignWarning text=W texthl=LspDiagnosticsSignWarning linehl= numhl=
---- sign define LspDiagnosticsSignInformation text=I texthl=LspDiagnosticsSignInformation linehl= numhl=
---- sign define LspDiagnosticsSignHint text=H texthl=LspDiagnosticsSignHint linehl= numhl=
---- </pre>
---@param diagnostics Diagnostic[]
---@param bufnr number The buffer number
---@param client_id number the client id
@@ -639,51 +469,18 @@ end
--- - priority: Set the priority of the signs.
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
-function M.set_signs(diagnostics, bufnr, client_id, sign_ns, opts)
- opts = opts or {}
- sign_ns = sign_ns or M._get_sign_namespace(client_id)
-
- if not diagnostics then
- diagnostics = diagnostic_cache[bufnr][client_id]
- end
-
- if not diagnostics then
- return
+function M.set_signs(diagnostics, bufnr, client_id, _, opts)
+ local namespace = M.get_namespace(client_id)
+ if opts and not opts.severity and opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
- bufnr = get_bufnr(bufnr)
- diagnostics = filter_by_severity_limit(opts.severity_limit, diagnostics)
-
- local ok = true
- for _, diagnostic in ipairs(diagnostics) do
-
- ok = ok and pcall(vim.fn.sign_place,
- 0,
- sign_ns,
- sign_highlight_map[diagnostic.severity],
- bufnr,
- {
- priority = opts.priority,
- lnum = diagnostic.range.start.line + 1
- }
- )
- end
-
- if not ok then
- log.debug("Failed to place signs:", diagnostics)
- end
+ vim.diagnostic._set_signs(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
end
--- Set underline for given diagnostics
---
---- Underline highlights can be customized by changing the following |:highlight| groups.
----
---- <pre>
---- LspDiagnosticsUnderlineError
---- LspDiagnosticsUnderlineWarning
---- LspDiagnosticsUnderlineInformation
---- LspDiagnosticsUnderlineHint
---- </pre>
+---@deprecated Prefer |vim.diagnostic._set_underline()|
---
---@param diagnostics Diagnostic[]
---@param bufnr number: The buffer number
@@ -692,43 +489,17 @@ end
---@param opts table: Configuration table:
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
-function M.set_underline(diagnostics, bufnr, client_id, diagnostic_ns, opts)
- opts = opts or {}
-
- diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
- diagnostics = filter_by_severity_limit(opts.severity_limit, diagnostics)
-
- for _, diagnostic in ipairs(diagnostics) do
- local start = diagnostic.range["start"]
- local finish = diagnostic.range["end"]
- local higroup = underline_highlight_map[diagnostic.severity]
-
- if higroup == nil then
- -- Default to error if we don't have a highlight associated
- higroup = underline_highlight_map[DiagnosticSeverity.Error]
- end
-
- highlight.range(
- bufnr,
- diagnostic_ns,
- higroup,
- to_position(start, bufnr),
- to_position(finish, bufnr)
- )
+function M.set_underline(diagnostics, bufnr, client_id, _, opts)
+ local namespace = M.get_namespace(client_id)
+ if opts and not opts.severity and opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
+ return vim.diagnostic._set_underline(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
end
--- Virtual Text {{{
--- Set virtual text given diagnostics
---
---- Virtual text highlights can be customized by changing the following |:highlight| groups.
----
---- <pre>
---- LspDiagnosticsVirtualTextError
---- LspDiagnosticsVirtualTextWarning
---- LspDiagnosticsVirtualTextInformation
---- LspDiagnosticsVirtualTextHint
---- </pre>
+---@deprecated Prefer |vim.diagnostic._set_virtual_text()|
---
---@param diagnostics Diagnostic[]
---@param bufnr number
@@ -739,460 +510,133 @@ end
--- - spacing (number): Number of spaces to insert before virtual text
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
-function M.set_virtual_text(diagnostics, bufnr, client_id, diagnostic_ns, opts)
- opts = opts or {}
-
- client_id = get_client_id(client_id)
- diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
-
- local buffer_line_diagnostics
- if diagnostics then
- buffer_line_diagnostics = _diagnostic_lines(diagnostics)
- else
- buffer_line_diagnostics = diagnostic_cache_lines[bufnr][client_id]
- end
-
- if not buffer_line_diagnostics then
- return nil
- end
-
- for line, line_diagnostics in pairs(buffer_line_diagnostics) do
- line_diagnostics = filter_by_severity_limit(opts.severity_limit, line_diagnostics)
- 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, {})
- end
+function M.set_virtual_text(diagnostics, bufnr, client_id, _, opts)
+ local namespace = M.get_namespace(client_id)
+ if opts and not opts.severity and opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
+ return vim.diagnostic._set_virtual_text(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
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()|.
+---
+---@deprecated Prefer |vim.diagnostic.get_virt_text_chunks()|
+---
---@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()|
-function M.get_virtual_text_chunks_for_line(bufnr, line, line_diags, opts)
- assert(bufnr or line)
-
- if #line_diags == 0 then
- return nil
- end
-
- opts = opts or {}
- local prefix = opts.prefix or "■"
- local spacing = opts.spacing or 4
-
- -- Create a little more space between virtual text and contents
- local virt_texts = {{string.rep(" ", spacing)}}
-
- for i = 1, #line_diags - 1 do
- table.insert(virt_texts, {prefix, virtual_text_highlight_map[line_diags[i].severity]})
- end
- local last = line_diags[#line_diags]
-
- -- TODO(tjdevries): Allow different servers to be shown first somehow?
- -- TODO(tjdevries): Display server name associated with these?
- if last.message then
- table.insert(
- virt_texts,
- {
- string.format("%s %s", prefix, last.message:gsub("\r", ""):gsub("\n", " ")),
- virtual_text_highlight_map[last.severity]
- }
- )
-
- return virt_texts
- end
+---@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_diags, opts)
+ return vim.diagnostic._get_virt_text_chunks(diagnostic_lsp_to_vim(line_diags, bufnr), opts)
end
--- }}}
--- }}}
--- Diagnostic Clear {{{
---- Clears the currently displayed diagnostics
----@param bufnr number The buffer number
----@param client_id number the client id
----@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
-
- if client_id == nil then
- return vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _)
- return M.clear(bufnr, iter_client_id)
- end)
- end
-
- diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
- sign_ns = sign_ns or M._get_sign_namespace(client_id)
-
- assert(bufnr, "bufnr is required")
- assert(diagnostic_ns, "Need diagnostic_ns, got nil")
- assert(sign_ns, string.format("Need sign_ns, got nil %s", sign_ns))
- -- clear sign group
- vim.fn.sign_unplace(sign_ns, {buffer=bufnr})
-
- -- clear virtual text namespace
- api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1)
-end
--- }}}
--- Diagnostic Insert Leave Handler {{{
-
---- Callback scheduled for after leaving insert mode
+--- Open a floating window with the diagnostics from {position}
---
---- Used to handle
---@private
-function M._execute_scheduled_display(bufnr, client_id)
- local args = _bufs_waiting_to_update[bufnr][client_id]
- if not args then
- return
- end
-
- -- Clear the args so we don't display unnecessarily.
- _bufs_waiting_to_update[bufnr][client_id] = nil
-
- M.display(nil, bufnr, client_id, args)
-end
-
-local registered = {}
-
-local make_augroup_key = function(bufnr, client_id)
- return string.format("LspDiagnosticInsertLeave:%s:%s", bufnr, client_id)
-end
-
---- Table of autocmd events to fire the update for displaying new diagnostic information
-M.insert_leave_auto_cmds = { "InsertLeave", "CursorHoldI" }
-
---- Used to schedule diagnostic updates upon leaving insert mode.
+---@deprecated Prefer |vim.diagnostic.show_position_diagnostics()|
---
---- For parameter description, see |M.display()|
-function M._schedule_display(bufnr, client_id, args)
- _bufs_waiting_to_update[bufnr][client_id] = args
-
- local key = make_augroup_key(bufnr, client_id)
- if not registered[key] then
- vim.cmd(string.format("augroup %s", key))
- vim.cmd(" au!")
- vim.cmd(
- string.format(
- [[autocmd %s <buffer=%s> :lua vim.lsp.diagnostic._execute_scheduled_display(%s, %s)]],
- table.concat(M.insert_leave_auto_cmds, ","),
- bufnr,
- bufnr,
- client_id
- )
- )
- vim.cmd("augroup END")
-
- registered[key] = true
- end
-end
-
-
---- Used in tandem with
----
---- For parameter description, see |M.display()|
-function M._clear_scheduled_display(bufnr, client_id)
- local key = make_augroup_key(bufnr, client_id)
-
- if registered[key] then
- vim.cmd(string.format("augroup %s", key))
- vim.cmd(" au!")
- vim.cmd("augroup END")
-
- registered[key] = nil
- end
-end
--- }}}
-
--- Diagnostic Private Highlight Utilies {{{
---- Get the severity highlight name
---@private
-function M._get_severity_highlight_name(severity)
- return virtual_text_highlight_map[severity]
-end
-
---- Get floating severity highlight name
---@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
- local function define_default_sign(name, properties)
- if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then
- vim.fn.sign_define(name, properties)
+---@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)
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
end
-
- -- Initialize default diagnostic highlights
- for severity, hi_info in pairs(diagnostic_severities) do
- local default_highlight_name = default_highlight_map[severity]
- highlight.create(default_highlight_name, hi_info, true)
-
- -- Default link all corresponding highlights to the default highlight
- highlight.link(virtual_text_highlight_map[severity], default_highlight_name, false)
- highlight.link(floating_highlight_map[severity], default_highlight_name, false)
- highlight.link(sign_highlight_map[severity], default_highlight_name, false)
- end
-
- -- Create all signs
- for severity, sign_hl_name in pairs(sign_highlight_map) do
- local severity_name = DiagnosticSeverity[severity]
-
- define_default_sign(sign_hl_name, {
- text = (severity_name or 'U'):sub(1, 1),
- texthl = sign_hl_name,
- linehl = '',
- numhl = '',
- })
- end
-
- -- Initialize Underline highlights
- for severity, underline_highlight_name in pairs(underline_highlight_map) do
- highlight.create(underline_highlight_name, {
- cterm = 'underline',
- gui = 'underline',
- guisp = diagnostic_severities[severity].guifg
- }, true)
- end
+ return vim.diagnostic.show_position_diagnostics(opts, buf_nr, position)
end
--- }}}
--- Diagnostic Display {{{
---- |lsp-handler| for the method "textDocument/publishDiagnostics"
+--- Open a floating window with the diagnostics from {line_nr}
---
----@note Each of the configuration options accepts:
---- - `false`: Disable this feature
---- - `true`: Enable this feature, use default settings.
---- - `table`: Enable this feature, use overrides.
---- - `function`: Function with signature (bufnr, client_id) that returns any of the above.
---- <pre>
---- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
---- vim.lsp.diagnostic.on_publish_diagnostics, {
---- -- Enable underline, use default values
---- underline = true,
---- -- Enable virtual text, override spacing to 4
---- virtual_text = {
---- spacing = 4,
---- },
---- -- Use a function to dynamically turn signs off
---- -- and on, using buffer local variables
---- signs = function(bufnr, client_id)
---- return vim.bo[bufnr].show_signs == false
---- end,
---- -- Disable a feature
---- update_in_insert = false,
---- }
---- )
---- </pre>
+---@deprecated Prefer |vim.diagnostic.show_line_diagnostics()|
---
----@param config table Configuration table.
---- - underline: (default=true)
---- - Apply underlines to diagnostics.
---- - See |vim.lsp.diagnostic.set_underline()|
---- - virtual_text: (default=true)
---- - Apply virtual text to line endings.
---- - See |vim.lsp.diagnostic.set_virtual_text()|
---- - signs: (default=true)
---- - Apply signs for diagnostics.
---- - See |vim.lsp.diagnostic.set_signs()|
---- - update_in_insert: (default=false)
---- - 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
- local bufnr = vim.uri_to_bufnr(uri)
-
- if not bufnr then
- return
- end
-
- local diagnostics = params.diagnostics
-
- if config and if_nil(config.severity_sort, false) then
- table.sort(diagnostics, function(a, b) return a.severity > b.severity end)
- end
-
- -- Always save the diagnostics, even if the buf is not loaded.
- -- Language servers may report compile or build errors via diagnostics
- -- Users should be able to find these, even if they're in files which
- -- are not loaded.
- M.save(diagnostics, bufnr, client_id)
-
- -- Unloaded buffers should not handle diagnostics.
- -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen.
- -- This should trigger another publish of the diagnostics.
- --
- -- In particular, this stops a ton of spam when first starting a server for current
- -- unloaded buffers.
- if not api.nvim_buf_is_loaded(bufnr) then
- return
+---@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)
+ if client_id then
+ opts = opts or {}
+ opts.namespace = M.get_namespace(client_id)
end
-
- M.display(diagnostics, bufnr, client_id, config)
+ return vim.diagnostic.show_line_diagnostics(opts, buf_nr, line_nr)
end
---@private
---- Display diagnostics for the buffer, given a configuration.
-function M.display(diagnostics, bufnr, client_id, config)
- config = vim.lsp._with_extend('vim.lsp.diagnostic.on_publish_diagnostics', {
- signs = true,
- underline = true,
- virtual_text = true,
- update_in_insert = false,
- severity_sort = false,
- }, config)
-
- -- TODO(tjdevries): Consider how we can make this a "standardized" kind of thing for |lsp-handlers|.
- -- It seems like we would probably want to do this more often as we expose more of them.
- -- It provides a very nice functional interface for people to override configuration.
- local resolve_optional_value = function(option)
- local enabled_val = {}
-
- if not option then
- return false
- elseif option == true then
- return enabled_val
- elseif type(option) == 'function' then
- local val = option(bufnr, client_id)
- if val == true then
- return enabled_val
- else
- return val
- end
- elseif type(option) == 'table' then
- return option
- else
- error("Unexpected option type: " .. vim.inspect(option))
- end
- end
-
- if resolve_optional_value(config.update_in_insert) then
- M._clear_scheduled_display(bufnr, client_id)
- else
- local mode = vim.api.nvim_get_mode()
-
- if string.sub(mode.mode, 1, 1) == 'i' then
- M._schedule_display(bufnr, client_id, config)
- return
- end
- end
-
- M.clear(bufnr, client_id)
-
- diagnostics = diagnostics or M.get(bufnr, client_id)
-
- vim.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged")
-
- if not diagnostics or vim.tbl_isempty(diagnostics) then
- return
- end
-
- local underline_opts = resolve_optional_value(config.underline)
- if underline_opts then
- M.set_underline(diagnostics, bufnr, client_id, nil, underline_opts)
- end
-
- local virtual_text_opts = resolve_optional_value(config.virtual_text)
- if virtual_text_opts then
- M.set_virtual_text(diagnostics, bufnr, client_id, nil, virtual_text_opts)
+--- Redraw diagnostics for the given buffer and client
+---
+---@deprecated Prefer |vim.diagnostic.redraw()|
+---
+--- 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
- local signs_opts = resolve_optional_value(config.signs)
- if signs_opts then
- M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts)
- end
+ local namespace = M.get_namespace(client_id)
+ return vim.diagnostic.show(namespace, bufnr)
end
--- }}}
--- Diagnostic User Functions {{{
---- Open a floating window with the diagnostics from {line_nr}
+--- Sets the quickfix list
---
---- The floating window can be customized with the following highlight groups:
---- <pre>
---- LspDiagnosticsFloatingError
---- LspDiagnosticsFloatingWarning
---- LspDiagnosticsFloatingInformation
---- LspDiagnosticsFloatingHint
---- </pre>
----@param opts table Configuration table
---- - show_header (boolean, default true): Show "Diagnostics:" header.
---- - Plus all the opts for |vim.lsp.diagnostic.get_line_diagnostics()|
---- and |vim.lsp.util.open_floating_preview()| can be used here.
----@param bufnr number The buffer number
----@param line_nr number The line number
----@param client_id number|nil the client id
----@return table {popup_bufnr, win_id}
-function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
+---@deprecated Prefer |vim.diagnostic.setqflist()|
+---
+---@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 {}
-
- 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 lines = {}
- local highlights = {}
- 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
- local prefix = string.format("%d. ", i)
- local hiname = M._get_floating_severity_highlight_name(diagnostic.severity)
- assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity))
-
- local message_lines = vim.split(diagnostic.message, '\n', true)
- 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(highlights, {0, hiname})
- end
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
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)
- -- Start highlight after the prefix
- api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1)
+ if opts.client_id then
+ opts.client_id = nil
+ opts.namespace = M.get_namespace(opts.client_id)
end
-
- return popup_bufnr, winnr
-end
-
-
---- Clear diagnotics and diagnostic cache
----
---- Handles saving diagnostics from multiple clients in the same buffer.
----@param client_id number
----@param buffer_client_map table map of buffers to active clients
-function M.reset(client_id, buffer_client_map)
- buffer_client_map = vim.deepcopy(buffer_client_map)
- vim.schedule(function()
- for bufnr, client_ids in pairs(buffer_client_map) do
- if client_ids[client_id] then
- clear_diagnostic_cache(bufnr, client_id)
- M.clear(bufnr, client_id)
- end
- end
- end)
+ local workspace = vim.F.if_nil(opts.workspace, true)
+ opts.bufnr = not workspace and 0
+ return vim.diagnostic.setqflist(opts)
end
--- Sets the location list
+---
+---@deprecated Prefer |vim.diagnostic.setloclist()|
+---
---@param opts table|nil Configuration table. Keys:
---- - {open_loclist}: (boolean, default true)
+--- - {open}: (boolean, default true)
--- - Open loclist after set
--- - {client_id}: (number)
--- - If nil, will consider all clients attached to buffer.
@@ -1204,29 +648,65 @@ end
--- - Set the list with workspace diagnostics
function M.set_loclist(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)
- }
- local predicate = function(d)
- local severity = to_severity(opts.severity)
- if severity then
- return d.severity == severity
- end
- local severity_limit = to_severity(opts.severity_limit)
- if severity_limit then
- return d.severity <= severity_limit
- end
- return true
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
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]]
+ if opts.client_id then
+ opts.client_id = nil
+ opts.namespace = M.get_namespace(opts.client_id)
end
+ local workspace = vim.F.if_nil(opts.workspace, false)
+ opts.bufnr = not workspace and 0
+ return vim.diagnostic.setloclist(opts)
end
+
+--- Disable diagnostics for the given buffer and client
+---
+---@deprecated Prefer |vim.diagnostic.disable()|
+---
+---@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
+
+ bufnr = get_bufnr(bufnr)
+ local namespace = M.get_namespace(client_id)
+ return vim.diagnostic.disable(bufnr, namespace)
+end
+
+--- Enable diagnostics for the given buffer and client
+---
+---@deprecated Prefer |vim.diagnostic.enable()|
+---
+---@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
+
+ bufnr = get_bufnr(bufnr)
+ local namespace = M.get_namespace(client_id)
+ return vim.diagnostic.enable(bufnr, namespace)
+end
+
-- }}}
return M
+
+-- vim: fdm=marker
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 41852b9d88..eff27807be 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -3,37 +3,34 @@ local protocol = require 'vim.lsp.protocol'
local util = require 'vim.lsp.util'
local vim = vim
local api = vim.api
-local buf = require 'vim.lsp.buf'
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 +58,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 +74,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 +95,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,42 +108,8 @@ 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
- print("No code actions available")
- return
- end
-
- local option_strings = {"Code Actions:"}
- for i, action in ipairs(actions) 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
- return
- end
- local action_chosen = actions[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
- if action_chosen.edit or type(action_chosen.command) == "table" then
- if action_chosen.edit then
- util.apply_workspace_edit(action_chosen.edit)
- end
- if type(action_chosen.command) == "table" then
- buf.execute_command(action_chosen.command)
- end
- else
- buf.execute_command(action_chosen)
- 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,30 +122,30 @@ 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(...)
@@ -191,51 +156,71 @@ M['textDocument/codeLens'] = function(...)
return require('vim.lsp.codelens').on_codelens(...)
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
-M['textDocument/references'] = function(_, _, result)
- if not result then return end
- util.set_qflist(util.locations_to_items(result))
- api.nvim_command("copen")
-end
---@private
---- Prints given list of symbols to the quickfix list.
---@param _ (not used)
---@param _ (not used)
---@param result (list of Symbols) LSP method name
---@param result (table) result of LSP method; a location or a list of locations.
----(`textDocument/definition` can return `Location` or `Location[]`
-local symbol_handler = function(_, _, result, _, bufnr)
- if not result or vim.tbl_isempty(result) then return end
- util.set_qflist(util.symbols_to_items(result, bufnr))
- api.nvim_command("copen")
+---@private
+--- Return a function that converts LSP responses to list items and opens the list
+---
+--- The returned function has an optional {config} parameter that accepts a table
+--- with the following keys:
+---
+--- loclist: (boolean) use the location list (default is to use the quickfix list)
+---
+---@param map_result function `((resp, bufnr) -> list)` to convert the response
+---@param entity name of the resource used in a `not found` error message
+local function response_to_list(map_result, entity)
+ return function(_,result, ctx, config)
+ if not result or vim.tbl_isempty(result) then
+ vim.notify('No ' .. entity .. ' found')
+ else
+ config = config or {}
+ if config.loclist then
+ vim.fn.setloclist(0, {}, ' ', {
+ title = 'Language Server';
+ items = map_result(result, ctx.bufnr);
+ })
+ api.nvim_command("lopen")
+ else
+ vim.fn.setqflist({}, ' ', {
+ title = 'Language Server';
+ items = 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])
@@ -260,9 +245,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
@@ -276,18 +261,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
@@ -306,16 +291,17 @@ local function location_handler(_, method, result)
end
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration
M['textDocument/declaration'] = location_handler
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
M['textDocument/definition'] = location_handler
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition
M['textDocument/typeDefinition'] = location_handler
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
M['textDocument/implementation'] = location_handler
---- |lsp-handler| for the method "textDocument/signatureHelp"
+--- |lsp-handler| for the method "textDocument/signatureHelp".
+--- The active parameter is highlighted with |hl-LspSignatureActiveParameter|.
--- <pre>
--- vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
--- vim.lsp.handlers.signature_help, {
@@ -328,43 +314,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
@@ -383,16 +379,17 @@ local make_call_hierarchy_handler = function(direction)
end
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/incomingCalls
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_incomingCalls
M['callHierarchy/incomingCalls'] = make_call_hierarchy_handler('from')
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/outgoingCalls
+--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_outgoingCalls
M['callHierarchy/outgoingCalls'] = make_call_hierarchy_handler('to')
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/logMessage
-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
@@ -402,7 +399,7 @@ M['window/logMessage'] = function(_, _, result, client_id)
log.error(message)
elseif message_type == protocol.MessageType.Warning then
log.warn(message)
- elseif message_type == protocol.MessageType.Info then
+ elseif message_type == protocol.MessageType.Info or message_type == protocol.MessageType.Log then
log.info(message)
else
log.debug(message)
@@ -410,10 +407,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 +428,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.trace() and log.trace('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..4597f1919a 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -14,26 +14,38 @@ log.levels = vim.deepcopy(vim.log.levels)
-- Default log level is warn.
local current_log_level = log.levels.WARN
-local log_date_format = "%FT%H:%M:%S%z"
+local log_date_format = "%F %H:%M:%S"
+local format_func = function(arg) return vim.inspect(arg, {newline=''}) end
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)))
+ logfile:write(string.format("[START][%s] LSP logging initiated\n", os.date(log_date_format)))
for level, levelnr in pairs(log.levels) do
-- Also export the log level on the root object.
log[level] = levelnr
@@ -56,14 +68,14 @@ do
if levelnr < current_log_level then return false end
if argc == 0 then return true end
local info = debug.getinfo(2, "Sl")
- local fileinfo = string.format("%s:%s", info.short_src, info.currentline)
- local parts = { table.concat({"[", level, "]", os.date(log_date_format), "]", fileinfo, "]"}, " ") }
+ local header = string.format("[%s][%s] ...%s:%s", level, os.date(log_date_format), string.sub(info.short_src, #info.short_src - 15), info.currentline)
+ local parts = { header }
for i = 1, argc do
local arg = select(i, ...)
if arg == nil then
table.insert(parts, "nil")
else
- table.insert(parts, vim.inspect(arg, {newline=''}))
+ table.insert(parts, format_func(arg))
end
end
logfile:write(table.concat(parts, '\t'), "\n")
@@ -77,7 +89,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 +100,21 @@ function log.set_level(level)
end
end
+--- Gets the current log level.
+function log.get_level()
+ return current_log_level
+end
+
+--- Sets formatting function used to format logs
+---@param handle function function to apply to logging arguments, pass vim.inspect for multi-line formatting
+function log.set_format_func(handle)
+ assert(handle == vim.inspect or type(handle) == 'function', "handle must be a function")
+ format_func = handle
+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..b3aa8b934f 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\+//]]
@@ -645,6 +645,10 @@ function protocol.make_client_capabilities()
end)();
};
};
+ dataSupport = true;
+ resolveSupport = {
+ properties = { 'edit', }
+ };
};
completion = {
dynamicRegistration = false;
@@ -691,10 +695,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 +1007,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 +1017,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 +1027,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 4c5f02af9d..255eb65dfe 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -4,38 +4,10 @@ local log = require('vim.lsp.log')
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
---- Encodes to JSON.
----
---@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
- return result
- else
- return nil, result
- end
-end
---@private
---- Decodes from JSON.
----
---@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
- return result
- else
- return nil, result
- 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,7 +15,7 @@ end
local NIL = vim.NIL
---@private
+---@private
local recursive_convert_NIL
recursive_convert_NIL = function(v, tbl_processed)
if v == NIL then
@@ -63,15 +35,15 @@ recursive_convert_NIL = function(v, tbl_processed)
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.
---
@@ -81,8 +53,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
@@ -97,11 +69,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';
@@ -109,11 +81,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
@@ -141,7 +113,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
@@ -203,8 +175,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' };
@@ -233,9 +205,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')
@@ -250,38 +222,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)
@@ -290,25 +262,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|.
@@ -358,10 +330,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()
@@ -385,20 +357,17 @@ 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
+ ---@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)
+ local _ = log.debug() and log.debug("rpc.send", payload)
if handle == nil or handle:is_closing() then return false end
- -- TODO(ashkan) remove this once we have a Lua json_encode
- schedule(function()
- local encoded = assert(json_encode(payload))
- stdin:write(format_message_with_content_length(encoded))
- end)
+ local encoded = vim.json.encode(payload)
+ stdin:write(format_message_with_content_length(encoded))
return true
end
@@ -406,9 +375,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";
@@ -417,7 +386,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 {
@@ -433,10 +402,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' };
@@ -463,13 +432,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, ...)
@@ -477,7 +446,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
@@ -486,16 +455,17 @@ 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
- -- on_error(client_errors.INVALID_SERVER_JSON, err)
+ local ok, decoded = pcall(vim.json.decode, body)
+ if not ok then
+ on_error(client_errors.INVALID_SERVER_JSON, decoded)
return
end
- local _ = log.debug() and log.debug("decoded", decoded)
+ local _ = log.debug() and log.debug("rpc.receive", decoded)
if type(decoded.method) == 'string' and decoded.id then
+ local err
-- Server Request
decoded.params = convert_NIL(decoded.params)
-- Schedule here so that the users functions don't trigger an error and
@@ -582,8 +552,6 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
end
end
- -- TODO(ashkan) remove this once we have a Lua json_decode
- handle_body = schedule_wrap(handle_body)
local request_parser = coroutine.wrap(request_parser_loop)
request_parser()
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 195e3a0e65..e95f170427 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -1,4 +1,5 @@
local protocol = require 'vim.lsp.protocol'
+local snippet = require 'vim.lsp._snippet'
local vim = vim
local validate = vim.validate
local api = vim.api
@@ -30,20 +31,12 @@ local default_border = {
{" ", "NormalFloat"},
}
-
-local DiagnosticSeverity = protocol.DiagnosticSeverity
-local loclist_type_map = {
- [DiagnosticSeverity.Error] = 'E',
- [DiagnosticSeverity.Warning] = 'W',
- [DiagnosticSeverity.Information] = 'I',
- [DiagnosticSeverity.Hint] = 'I',
-}
-
-
---@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
@@ -52,12 +45,16 @@ local function get_border_size(opts)
if type(border) == 'string' then
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 +62,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 +74,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 +85,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 +94,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 +132,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 +146,8 @@ local function sort_by_key(fn)
return false
end
end
---@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
@@ -175,6 +170,7 @@ local function get_line_byte_from_position(bufnr, position)
if ok then
return result
end
+ return math.min(#lines[1], col)
end
end
return col
@@ -238,53 +234,119 @@ 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 bufnr 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
vim.fn.bufload(bufnr)
end
api.nvim_buf_set_option(bufnr, 'buflisted', true)
- local start_line, finish_line = math.huge, -1
- local cleaned = {}
- for i, e in ipairs(text_edits) do
- -- adjust start and end column for UTF-16 encoding of non-ASCII characters
- local start_row = e.range.start.line
- local start_col = get_line_byte_from_position(bufnr, e.range.start)
- local end_row = e.range["end"].line
- local end_col = get_line_byte_from_position(bufnr, e.range['end'])
- start_line = math.min(e.range.start.line, start_line)
- finish_line = math.max(e.range["end"].line, finish_line)
- -- TODO(ashkan) sanity check ranges for overlap.
- table.insert(cleaned, {
- i = i;
- A = {start_row; start_col};
- B = {end_row; end_col};
- lines = vim.split(e.newText, '\n', true);
- })
- end
- -- Reverse sort the orders so we can apply them without interfering with
- -- eachother. Also add i as a sort key to mimic a stable sort.
- table.sort(cleaned, edit_sort_key)
- local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false)
- local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol')
- local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) <= finish_line + 1
- if set_eol and (#lines == 0 or #lines[#lines] ~= 0) then
- table.insert(lines, '')
+ -- Fix reversed range and indexing each text_edits
+ local index = 0
+ text_edits = vim.tbl_map(function(text_edit)
+ index = index + 1
+ text_edit._index = index
+
+ if text_edit.range.start.line > text_edit.range['end'].line or text_edit.range.start.line == text_edit.range['end'].line and text_edit.range.start.character > text_edit.range['end'].character then
+ local start = text_edit.range.start
+ text_edit.range.start = text_edit.range['end']
+ text_edit.range['end'] = start
+ end
+ return text_edit
+ end, text_edits)
+
+ -- Sort text_edits
+ table.sort(text_edits, function(a, b)
+ if a.range.start.line ~= b.range.start.line then
+ return a.range.start.line > b.range.start.line
+ end
+ if a.range.start.character ~= b.range.start.character then
+ return a.range.start.character > b.range.start.character
+ end
+ if a._index ~= b._index then
+ return a._index > b._index
+ end
+ end)
+
+ -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
+ local has_eol_text_edit = false
+ local max = vim.api.nvim_buf_line_count(bufnr)
+ local len = vim.str_utfindex(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '')
+ text_edits = vim.tbl_map(function(text_edit)
+ if max <= text_edit.range.start.line then
+ text_edit.range.start.line = max - 1
+ text_edit.range.start.character = len
+ text_edit.newText = '\n' .. text_edit.newText
+ has_eol_text_edit = true
+ end
+ if max <= text_edit.range['end'].line then
+ text_edit.range['end'].line = max - 1
+ text_edit.range['end'].character = len
+ has_eol_text_edit = true
+ end
+ return text_edit
+ end, text_edits)
+
+ -- Some LSP servers are depending on the VSCode behavior.
+ -- The VSCode will re-locate the cursor position after applying TextEdit so we also do it.
+ local is_current_buf = vim.api.nvim_get_current_buf() == bufnr
+ local cursor = (function()
+ if not is_current_buf then
+ return {
+ row = -1,
+ col = -1,
+ }
+ end
+ local cursor = vim.api.nvim_win_get_cursor(0)
+ return {
+ row = cursor[1] - 1,
+ col = cursor[2],
+ }
+ end)()
+
+ -- Apply text edits.
+ local is_cursor_fixed = false
+ for _, text_edit in ipairs(text_edits) do
+ local e = {
+ start_row = text_edit.range.start.line,
+ start_col = get_line_byte_from_position(bufnr, text_edit.range.start),
+ end_row = text_edit.range['end'].line,
+ end_col = get_line_byte_from_position(bufnr, text_edit.range['end']),
+ text = vim.split(text_edit.newText, '\n', true),
+ }
+ vim.api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
+
+ local row_count = (e.end_row - e.start_row) + 1
+ if e.end_row < cursor.row then
+ cursor.row = cursor.row + (#e.text - row_count)
+ is_cursor_fixed = true
+ elseif e.end_row == cursor.row and e.end_col <= cursor.col then
+ cursor.row = cursor.row + (#e.text - row_count)
+ cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
+ if #e.text == 1 then
+ cursor.col = cursor.col + e.start_col
+ end
+ is_cursor_fixed = true
+ end
end
- for i = #cleaned, 1, -1 do
- local e = cleaned[i]
- local A = {e.A[1] - start_line, e.A[2]}
- local B = {e.B[1] - start_line, e.B[2]}
- lines = M.set_lines(lines, A, B, e.lines)
+ if is_cursor_fixed then
+ vim.api.nvim_win_set_cursor(0, {
+ cursor.row + 1,
+ math.min(cursor.col, #(vim.api.nvim_buf_get_lines(bufnr, cursor.row, cursor.row + 1, false)[1] or ''))
+ })
end
- if set_eol and #lines[#lines] == 0 then
- table.remove(lines)
+
+ -- Remove final line if needed
+ local fix_eol = has_eol_text_edit
+ fix_eol = fix_eol and api.nvim_buf_get_option(bufnr, 'fixeol')
+ fix_eol = fix_eol and (vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') == ''
+ if fix_eol then
+ vim.api.nvim_buf_set_lines(bufnr, -2, -1, false, {})
end
- api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines)
end
-- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
@@ -294,11 +356,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 +386,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 +430,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 +457,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 +485,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 +530,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,90 +576,34 @@ function M.apply_text_document_edit(text_document_edit, index)
M.apply_text_edits(text_document_edit.edits, bufnr)
end
---@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
----unparsed rest of the input
-local function parse_snippet_rec(input, inner)
- local res = ""
-
- local close, closeend = nil, nil
- if inner then
- close, closeend = input:find("}", 1, true)
- while close ~= nil and input:sub(close-1,close-1) == "\\" do
- close, closeend = input:find("}", closeend+1, true)
- end
- end
-
- local didx = input:find('$', 1, true)
- if didx == nil and close == nil then
- return input, ""
- elseif close ~=nil and (didx == nil or close < didx) then
- -- No inner placeholders
- return input:sub(0, close-1), input:sub(closeend+1)
- end
-
- res = res .. input:sub(0, didx-1)
- input = input:sub(didx+1)
-
- local tabstop, tabstopend = input:find('^%d+')
- local placeholder, placeholderend = input:find('^{%d+:')
- local choice, choiceend = input:find('^{%d+|')
-
- if tabstop then
- input = input:sub(tabstopend+1)
- elseif choice then
- input = input:sub(choiceend+1)
- close, closeend = input:find("|}", 1, true)
-
- res = res .. input:sub(0, close-1)
- input = input:sub(closeend+1)
- elseif placeholder then
- -- TODO: add support for variables
- input = input:sub(placeholderend+1)
-
- -- placeholders and variables are recursive
- while input ~= "" do
- local r, tail = parse_snippet_rec(input, true)
- r = r:gsub("\\}", "}")
-
- res = res .. r
- input = tail
- end
- else
- res = res .. "$"
- end
-
- return res, input
-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
+ local ok, parsed = pcall(function()
+ return tostring(snippet.parse(input))
+ end)
+ if not ok then
+ return input
+ end
+ return parsed
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 +623,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 +638,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 +648,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 +703,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 +759,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 +799,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
@@ -810,16 +816,16 @@ function M.convert_input_to_markdown_lines(input, contents)
-- If it's plaintext, then wrap it in a <text></text> block
-- Some servers send input.value as empty, so let's ignore this :(
- input.value = input.value or ''
+ local value = input.value or ''
if input.kind == "plaintext" then
-- wrap this in a <text></text> block so that stylize_markdown
-- can properly process it as plaintext
- input.value = string.format("<text>\n%s\n</text>", input.value or "")
+ value = string.format("<text>\n%s\n</text>", value)
end
- -- assert(type(input.value) == 'string')
- list_extend(contents, split_lines(input.value))
+ -- assert(type(value) == 'string')
+ list_extend(contents, split_lines(value))
-- MarkupString variation 2
elseif input.language then
-- Some servers send input.value as empty, so let's ignore this :(
@@ -843,11 +849,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
@@ -856,6 +863,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft)
--=== 0`. Whenever possible implementors should make an active decision about
--the active signature and shouldn't rely on a default value.
local contents = {}
+ local active_hl
local active_signature = signature_help.activeSignature or 0
-- If the activeSignature is not inside the valid range, then clip it.
if active_signature >= #signature_help.signatures then
@@ -875,11 +883,17 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft)
M.convert_input_to_markdown_lines(signature.documentation, contents)
end
if signature.parameters and #signature.parameters > 0 then
- local active_parameter = signature_help.activeParameter or 0
- -- If the activeParameter is not inside the valid range, then clip it.
+ local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0)
+ if active_parameter < 0
+ then active_parameter = 0
+ end
+
+ -- If the activeParameter is > #parameters, then set it to the last
+ -- NOTE: this is not fully according to the spec, but a client-side interpretation
if active_parameter >= #signature.parameters then
- active_parameter = 0
+ active_parameter = #signature.parameters - 1
end
+
local parameter = signature.parameters[active_parameter + 1]
if parameter then
--[=[
@@ -900,22 +914,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 };
@@ -942,7 +978,7 @@ function M.make_floating_popup_options(width, height, opts)
row = -get_border_size(opts).height
end
- if vim.fn.wincol() + width <= api.nvim_get_option('columns') then
+ if vim.fn.wincol() + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then
anchor = anchor..'W'
col = 0
else
@@ -960,13 +996,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
@@ -996,8 +1033,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
@@ -1020,7 +1057,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
@@ -1056,10 +1093,10 @@ function M._trim(contents, opts)
return contents
end
--- 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
+--- 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
@@ -1129,6 +1166,8 @@ function M.stylize_markdown(bufnr, contents, opts)
-- Clean up
contents = M._trim(contents, opts)
+ -- Insert blank line separator after code block?
+ local add_sep = opts.separator == nil and true or opts.separator
local stripped = {}
local highlights = {}
-- keep track of lnums that contain markdown
@@ -1155,9 +1194,24 @@ function M.stylize_markdown(bufnr, contents, opts)
start = start + 1;
finish = #stripped;
})
+ -- add a separator, but not on the last line
+ if add_sep and i < #contents then
+ table.insert(stripped, "---")
+ markdown_lines[#stripped] = true
+ end
else
- table.insert(stripped, line)
- markdown_lines[#stripped] = true
+ -- strip any emty lines or separators prior to this separator in actual markdown
+ if line:match("^---+$") then
+ while markdown_lines[#stripped] and (stripped[#stripped]:match("^%s*$") or stripped[#stripped]:match("^---+$")) do
+ markdown_lines[#stripped] = false
+ table.remove(stripped, #stripped)
+ end
+ end
+ -- add the line if its not an empty line following a separator
+ if not (line:match("^%s*$") and markdown_lines[#stripped] and stripped[#stripped]:match("^---+$")) then
+ table.insert(stripped, line)
+ markdown_lines[#stripped] = true
+ end
i = i + 1
end
end
@@ -1165,7 +1219,7 @@ function M.stylize_markdown(bufnr, contents, opts)
-- Compute size of float needed to show (wrapped) lines
opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
- local width, height = M._make_floating_popup_size(stripped, opts)
+ local width = M._make_floating_popup_size(stripped, opts)
local sep_line = string.rep("─", math.min(width, opts.wrap_at or width))
@@ -1175,30 +1229,10 @@ function M.stylize_markdown(bufnr, contents, opts)
end
end
- -- Insert blank line separator after code block
- local insert_separator = opts.separator
- if insert_separator == nil then insert_separator = true end
- if insert_separator then
- local offset = 0
- for _, h in ipairs(highlights) do
- h.start = h.start + offset
- h.finish = h.finish + offset
- -- check if a seperator already exists and use that one instead of creating a new one
- if h.finish + 1 <= #stripped then
- if stripped[h.finish + 1] ~= sep_line then
- table.insert(stripped, h.finish + 1, sep_line)
- offset = offset + 1
- height = height + 1
- end
- end
- end
- end
-
-
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
local idx = 1
- --@private
+ ---@private
-- keep track of syntaxes we already inlcuded.
-- no need to include the same syntax more than once
local langs = {}
@@ -1247,26 +1281,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' };
@@ -1333,9 +1367,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)
@@ -1349,7 +1383,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 {
@@ -1445,7 +1479,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)
@@ -1453,8 +1487,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
@@ -1475,24 +1510,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 }
@@ -1560,8 +1595,11 @@ 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
+--- The result can be passed to the {list} argument of |setqflist()| or
+--- |setloclist()|.
+---
+---@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({}, {
@@ -1618,7 +1656,9 @@ end
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
--- Defaults to current window.
---
---@param items (table) list of items
+---@deprecated Use |setloclist()|
+---
+---@param items (table) list of items
function M.set_loclist(items, win_id)
vim.fn.setloclist(win_id or 0, {}, ' ', {
title = 'Language Server';
@@ -1629,7 +1669,9 @@ 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
+---@deprecated Use |setqflist()|
+---
+---@param items (table) list of items
function M.set_qflist(items)
vim.fn.setqflist({}, ' ', {
title = 'Language Server';
@@ -1646,9 +1688,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
@@ -1684,19 +1726,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
@@ -1709,8 +1751,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
@@ -1733,7 +1775,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
@@ -1747,8 +1789,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();
@@ -1761,7 +1803,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()
@@ -1774,11 +1816,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 {
@@ -1814,23 +1856,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
@@ -1838,11 +1880,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 {}, {
@@ -1857,10 +1899,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)
@@ -1873,9 +1915,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]
@@ -1886,40 +1928,6 @@ function M.lookup_section(settings, section)
return settings
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)
-function M.diagnostics_to_items(diagnostics_by_bufnr, predicate)
- local items = {}
- for bufnr, diagnostics in pairs(diagnostics_by_bufnr or {}) do
- for _, d in pairs(diagnostics) do
- if not predicate or predicate(d) then
- table.insert(items, {
- bufnr = bufnr,
- lnum = d.range.start.line + 1,
- col = d.range.start.character + 1,
- text = d.message,
- type = loclist_type_map[d.severity or DiagnosticSeverity.Error] or 'E'
- })
- end
- end
- end
- table.sort(items, function(a, b)
- if a.bufnr == b.bufnr then
- return a.lnum < b.lnum
- else
- return a.bufnr < b.bufnr
- end
- end)
- return items
-end
-
-
M._get_line_byte_from_position = get_line_byte_from_position
M._warn_once = warn_once