aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r--runtime/lua/vim/lsp/buf.lua15
-rw-r--r--runtime/lua/vim/lsp/callbacks.lua345
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua1195
-rw-r--r--runtime/lua/vim/lsp/handlers.lua310
-rw-r--r--runtime/lua/vim/lsp/protocol.lua25
-rw-r--r--runtime/lua/vim/lsp/rpc.lua68
-rw-r--r--runtime/lua/vim/lsp/util.lua402
7 files changed, 1657 insertions, 703 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 0b8e08f36c..fa62905c0a 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -30,9 +30,7 @@ end
---
--@param method (string) LSP method name
--@param params (optional, table) Parameters to send to the server
---@param callback (optional, functionnil) Handler
--- `function(err, method, params, client_id)` for this request. Defaults
--- to the client callback in `client.callbacks`. See |lsp-callbacks|.
+--@param handler (optional, functionnil) See |lsp-handler|. Follows |lsp-handler-resolution|
--
--@returns 2-tuple:
--- - Map of client-id:request-id pairs for all successful requests.
@@ -40,12 +38,12 @@ end
--- iterate all clients and call their `cancel_request()` methods.
---
--@see |vim.lsp.buf_request()|
-local function request(method, params, callback)
+local function request(method, params, handler)
validate {
method = {method, 's'};
- callback = {callback, 'f', true};
+ handler = {handler, 'f', true};
}
- return vim.lsp.buf_request(0, method, params, callback)
+ return vim.lsp.buf_request(0, method, params, handler)
end
--- Checks whether the language servers attached to the current buffer are
@@ -64,6 +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.
---
function M.declaration()
local params = util.make_position_params()
@@ -279,7 +278,7 @@ end
--@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 = util.get_line_diagnostics() }
+ context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
local params = util.make_range_params()
params.context = context
request('textDocument/codeAction', params)
@@ -294,7 +293,7 @@ end
---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 = util.get_line_diagnostics() }
+ context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
local params = util.make_given_range_params(start_pos, end_pos)
params.context = context
request('textDocument/codeAction', params)
diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua
index 3270d1d2a9..1da92b900d 100644
--- a/runtime/lua/vim/lsp/callbacks.lua
+++ b/runtime/lua/vim/lsp/callbacks.lua
@@ -1,345 +1,4 @@
-local log = require 'vim.lsp.log'
-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
---- Writes to error buffer.
---@param ... (table of strings) Will be concatenated before being written
-local function err_message(...)
- api.nvim_err_writeln(table.concat(vim.tbl_flatten{...}))
- 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
-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)
- if not workspace_edit then return end
- -- TODO(ashkan) Do something more with label?
- if workspace_edit.label then
- print("Workspace edit", workspace_edit.label)
- end
- local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit)
- return {
- applied = status;
- failureReason = result;
- }
-end
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_publishDiagnostics
-M['textDocument/publishDiagnostics'] = function(_, _, result)
- if not result then return end
- local uri = result.uri
- local bufnr = vim.uri_to_bufnr(uri)
- if not bufnr then
- err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri)
- return
- end
-
- -- 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.
- for _, diagnostic in ipairs(result.diagnostics) do
- if diagnostic.severity == nil then
- diagnostic.severity = protocol.DiagnosticSeverity.Error
- end
- end
-
- util.buf_clear_diagnostics(bufnr)
-
- -- 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.
- util.buf_diagnostics_save_positions(bufnr, result.diagnostics)
-
- -- 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
- end
- util.buf_diagnostics_underline(bufnr, result.diagnostics)
- util.buf_diagnostics_virtual_text(bufnr, result.diagnostics)
- util.buf_diagnostics_signs(bufnr, result.diagnostics)
- vim.api.nvim_command("doautocmd User LspDiagnosticsChanged")
-end
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
-M['textDocument/references'] = function(_, _, result)
- if not result then return end
- util.set_qflist(util.locations_to_items(result))
- api.nvim_command("copen")
- api.nvim_command("wincmd p")
-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_callback = function(_, _, result, _, bufnr)
- if not result or vim.tbl_isempty(result) then return end
-
- util.set_qflist(util.symbols_to_items(result, bufnr))
- api.nvim_command("copen")
- api.nvim_command("wincmd p")
-end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
-M['textDocument/documentSymbol'] = symbol_callback
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
-M['workspace/symbol'] = symbol_callback
-
---@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)
- if not result then return end
- util.apply_text_edits(result)
-end
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
-M['textDocument/formatting'] = function(_, _, result)
- if not result then return end
- util.apply_text_edits(result)
-end
-
---@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])
- local line_to_cursor = line:sub(col+1)
- local textMatch = vim.fn.match(line_to_cursor, '\\k*$')
- local prefix = line_to_cursor:sub(textMatch+1)
-
- local matches = util.text_document_completion_list_to_complete_items(result, prefix)
- vim.fn.complete(textMatch+1, matches)
-end
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
-M['textDocument/hover'] = function(_, method, result)
- util.focusable_float(method, function()
- if not (result and result.contents) then
- -- return { 'No information available' }
- return
- end
- local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
- markdown_lines = util.trim_empty_lines(markdown_lines)
- if vim.tbl_isempty(markdown_lines) then
- -- return { 'No information available' }
- return
- end
- local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, {
- pad_left = 1; pad_right = 1;
- })
- util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr)
- return bufnr, winnr
- end)
-end
-
---@private
---- Jumps to a location. Used as a callback 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.
----(`textDocument/definition` can return `Location` or `Location[]`
-local function location_callback(_, method, result)
- if result == nil or vim.tbl_isempty(result) then
- local _ = log.info() and log.info(method, 'No location found')
- return nil
- end
-
- -- textDocument/definition can return Location or Location[]
- -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
-
- if vim.tbl_islist(result) then
- util.jump_to_location(result[1])
-
- if #result > 1 then
- util.set_qflist(util.locations_to_items(result))
- api.nvim_command("copen")
- api.nvim_command("wincmd p")
- end
- else
- util.jump_to_location(result)
- end
-end
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration
-M['textDocument/declaration'] = location_callback
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
-M['textDocument/definition'] = location_callback
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition
-M['textDocument/typeDefinition'] = location_callback
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
-M['textDocument/implementation'] = location_callback
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
-M['textDocument/signatureHelp'] = function(_, method, result)
- -- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp callback
- -- 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')
- return
- end
- local lines = util.convert_signature_help_to_markdown_lines(result)
- lines = util.trim_empty_lines(lines)
- if vim.tbl_isempty(lines) then
- print('No signature help available')
- return
- end
- util.focusable_preview(method, function()
- return lines, util.try_trim_markdown_code_blocks(lines)
- end)
-end
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
-M['textDocument/documentHighlight'] = function(_, _, result, _)
- if not result then return end
- local bufnr = api.nvim_get_current_buf()
- util.buf_highlight_references(bufnr, result)
-end
-
---@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"`,
-local make_call_hierarchy_callback = function(direction)
- return function(_, _, result)
- if not result then return end
- local items = {}
- for _, call_hierarchy_call in pairs(result) do
- local call_hierarchy_item = call_hierarchy_call[direction]
- for _, range in pairs(call_hierarchy_call.fromRanges) do
- table.insert(items, {
- filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)),
- text = call_hierarchy_item.name,
- lnum = range.start.line + 1,
- col = range.start.character + 1,
- })
- end
- end
- util.set_qflist(items)
- api.nvim_command("copen")
- api.nvim_command("wincmd p")
- end
-end
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/incomingCalls
-M['callHierarchy/incomingCalls'] = make_call_hierarchy_callback('from')
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/outgoingCalls
-M['callHierarchy/outgoingCalls'] = make_call_hierarchy_callback('to')
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/logMessage
-M['window/logMessage'] = function(_, _, result, client_id)
- local message_type = result.type
- local message = result.message
- 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
- if message_type == protocol.MessageType.Error then
- log.error(message)
- elseif message_type == protocol.MessageType.Warning then
- log.warn(message)
- elseif message_type == protocol.MessageType.Info then
- log.info(message)
- else
- log.debug(message)
- end
- return result
-end
-
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/showMessage
-M['window/showMessage'] = function(_, _, result, client_id)
- local message_type = result.type
- local message = result.message
- 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
- if message_type == protocol.MessageType.Error then
- err_message("LSP[", client_name, "] ", message)
- else
- local message_type_name = protocol.MessageType[message_type]
- api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message))
- end
- return result
-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)
- log.debug('default_callback', method, { params = params, client_id = client_id, err = err, bufnr = bufnr })
- if err then
- error(tostring(err))
- end
- return fn(err, method, params, client_id, bufnr)
- end
-end
-
-return M
--- vim:sw=2 ts=2 et
+util._warn_once("require('vim.lsp.callbacks') is deprecated. Use vim.lsp.handlers instead.")
+return require('vim.lsp.handlers')
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
new file mode 100644
index 0000000000..45e717d16c
--- /dev/null
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -0,0 +1,1195 @@
+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 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[]
+
+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)
+ if client_id == nil then
+ client_id = DEFAULT_CLIENT_ID
+ end
+
+ return client_id
+end
+
+local get_bufnr = function(bufnr)
+ if not bufnr then
+ return api.nvim_get_current_buf()
+ elseif bufnr == 0 then
+ return 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
+ })
+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]
+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
+
+ if rawget(t, bufnr) == nil then
+ rawset(t, bufnr, {})
+ end
+
+ return rawget(t, bufnr)
+ end,
+
+ __newindex = function(t, bufnr, v)
+ if bufnr == 0 or bufnr == nil then
+ bufnr = vim.api.nvim_get_current_buf()
+ 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 _bufs_waiting_to_update = setmetatable({}, bufnr_and_client_cacher_mt)
+
+--- Store Diagnostic[] by line
+---
+---@param diagnostics Diagnostic[]
+---@return table<number, Diagnostic[]>
+local _diagnostic_lines = function(diagnostics)
+ if not diagnostics then return end
+
+ local diagnostics_by_line = {}
+ for _, diagnostic in ipairs(diagnostics) do
+ 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)
+ end
+ return diagnostics_by_line
+end
+
+--- Get the count of M by Severity
+---
+---@param diagnostics Diagnostic[]
+---@return table<DiagnosticSeverity, number>
+local _diagnostic_counts = function(diagnostics)
+ if not diagnostics then return 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
+
+ counts[diagnostic.severity] = val + 1
+ end
+ 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.
+ for _, diagnostic in ipairs(diagnostics) do
+ if diagnostic.severity == nil then
+ diagnostic.severity = DiagnosticSeverity.Error
+ 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
+
+
+--@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
+end
+
+--- Save diagnostics to the current buffer.
+---
+--- 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[bufnr][client_id] = nil
+ end
+ })
+ end
+
+ set_diagnostic_cache(diagnostics, bufnr, client_id)
+end
+-- }}}
+-- Diagnostic Retrieval {{{
+
+--- Return associated diagnostics for bufnr
+---
+---@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)
+ 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)
+
+ for _, diagnostic in ipairs(iter_diagnostics) do
+ table.insert(all_diagnostics, diagnostic)
+ end
+ end
+
+ return all_diagnostics
+ end
+
+ return diagnostic_cache[bufnr][client_id] or {}
+end
+
+--- Get the diagnostics by line
+---
+---@param bufnr number The buffer number
+---@param line_nr number 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
+---@return table Table with map of line number to list of diagnostics.
+-- Structured: { [1] = {...}, [5] = {.... } }
+function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
+ 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 {}
+ 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))
+ end
+
+ if opts.severity then
+ local filter_level = to_severity(opts.severity)
+ line_diagnostics = vim.tbl_filter(function(t) return t.severity == filter_level end, line_diagnostics)
+ elseif opts.severity_limit then
+ local filter_level = to_severity(opts.severity_limit)
+ line_diagnostics = vim.tbl_filter(function(t) return t.severity <= filter_level end, line_diagnostics)
+ end
+
+ if opts.severity_sort then
+ table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end)
+ end
+
+ return line_diagnostics
+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([[Error]])")}'
+--- let sl.='%#MyStatuslineLSP# W:'
+--- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.diagnostic.get_count([[Warning]])")}'
+--- else
+--- let sl.='%#MyStatuslineLSPErrors#off'
+--- endif
+--- return sl
+--- endfunction
+--- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
+--- </pre>
+---
+---@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
+ 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
+end
+
+--- Get the previous diagnostic closest to the cursor_position
+---
+---@param opts table See |vim.lsp.diagnostics.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)
+end
+
+--- Return the pos, {row, col}, for the prev diagnostic in the current buffer.
+---@param opts table See |vim.lsp.diagnostics.goto_next()|
+---@return table Previous diagnostic position
+function M.get_prev_pos(opts)
+ return _iter_diagnostic_lines_pos(
+ opts,
+ M.get_prev(opts)
+ )
+end
+
+--- Move to the previous diagnostic
+---@param opts table See |vim.lsp.diagnostics.goto_next()|
+function M.goto_prev(opts)
+ return _iter_diagnostic_move_pos(
+ "DiagnosticPrevious",
+ opts,
+ M.get_prev_pos(opts)
+ )
+end
+
+--- Get the previous diagnostic closest to the cursor_position
+---@param opts table See |vim.lsp.diagnostics.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)
+end
+
+--- Return the pos, {row, col}, for the next diagnostic in the current buffer.
+---@param opts table See |vim.lsp.diagnostics.goto_next()|
+---@return table Next diagnostic position
+function M.get_next_pos(opts)
+ return _iter_diagnostic_lines_pos(
+ opts,
+ M.get_next(opts)
+ )
+end
+
+--- Move to the next diagnostic
+---@param opts table|nil Configuration table. Keys:
+--- - {client_id}: (number)
+--- - If nil, will consider all clients attached to buffer.
+--- - {cursor_position}: (Position, default current position)
+--- - See |nvim_win_get_cursor()|
+--- - {wrap}: (boolean, default true)
+--- - Whether to loop around file or not. Similar to 'wrapscan'
+--- - {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.
+--- - {enable_popup}: (boolean, default true)
+--- - Call |vim.lsp.diagnostic.show_line_diagnostics()| on jump
+--- - {popup_opts}: (table)
+--- - Table to pass as {opts} parameter to |vim.lsp.diagnostic.show_line_diagnostics()|
+--- - {win_id}: (number, default 0)
+--- - Window ID
+function M.goto_next(opts)
+ return _iter_diagnostic_move_pos(
+ "DiagnosticNext",
+ opts,
+ M.get_next_pos(opts)
+ )
+end
+-- }}}
+-- Diagnostic Setters {{{
+
+--- Set signs for given diagnostics
+---
+--- Sign characters can be customized with the following commands:
+---
+--- <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
+---@param sign_ns number|nil
+---@param opts table Configuration for signs. Keys:
+--- - priority: Set the priority of the signs.
+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
+ end
+
+ bufnr = get_bufnr(bufnr)
+
+ 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
+end
+
+--- Set underline for given diagnostics
+---
+--- Underline highlights can be customized by changing the following |:highlight| groups.
+---
+--- <pre>
+--- LspDiagnosticsUnderlineError
+--- LspDiagnosticsUnderlineWarning
+--- LspDiagnosticsUnderlineInformation
+--- LspDiagnosticsUnderlineHint
+--- </pre>
+---
+---@param diagnostics Diagnostic[]
+---@param bufnr number The buffer number
+---@param client_id number the client id
+---@param diagnostic_ns number|nil
+---@param opts table Currently unused.
+function M.set_underline(diagnostics, bufnr, client_id, diagnostic_ns, opts)
+ opts = opts or {}
+ assert(opts) -- lint
+
+ diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
+
+ 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)
+ )
+ end
+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>
+---
+---@param diagnostics Diagnostic[]
+---@param bufnr number
+---@param client_id number
+---@param diagnostic_ns number
+---@param opts table Options on how to display virtual text. Keys:
+--- - prefix (string): Prefix to display before virtual text on line
+--- - spacing (number): Number of spaces to insert before virtual text
+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
+ 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
+ end
+end
+
+--- Default function to get text chunks to display using `nvim_buf_set_virtual_text`.
+---@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
+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
+---
+--- 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.
+---
+--- 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)
+ 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
+end
+-- }}}
+-- Diagnostic Display {{{
+
+--- |lsp-handler| for the method "textDocument/publishDiagnostics"
+---
+---@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>
+---
+---@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
+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
+
+ -- 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
+ end
+
+ M.display(diagnostics, bufnr, client_id, config)
+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,
+ }, config)
+
+ if diagnostics == nil then
+ diagnostics = M.get(bufnr, client_id)
+ end
+
+ -- 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 diagnostic_cache[bufnr][client_id]
+
+ 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)
+ end
+
+ local signs_opts = resolve_optional_value(config.signs)
+ if signs_opts then
+ M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts)
+ end
+
+ vim.api.nvim_command("doautocmd User LspDiagnosticsChanged")
+end
+-- }}}
+-- Diagnostic User Functions {{{
+
+--- Open a floating window with the diagnostics from {line_nr}
+---
+--- 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.
+---@param bufnr number The buffer number
+---@param line_nr number The line number
+---@param client_id number|nil the client id
+---@return {popup_bufnr, win_id}
+function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
+ opts = opts or {}
+ opts.severity_sort = if_nil(opts.severity_sort, true)
+
+ 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 + 1, hiname})
+ for j = 2, #message_lines do
+ table.insert(lines, message_lines[j])
+ table.insert(highlights, {0, hiname})
+ end
+ end
+
+ local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext')
+ 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)
+ end
+
+ return popup_bufnr, winnr
+end
+
+local loclist_type_map = {
+ [DiagnosticSeverity.Error] = 'E',
+ [DiagnosticSeverity.Warning] = 'W',
+ [DiagnosticSeverity.Information] = 'I',
+ [DiagnosticSeverity.Hint] = 'I',
+}
+
+--- Sets the location list
+---@param opts table|nil Configuration table. Keys:
+--- - {open_loclist}: (boolean, default true)
+--- - Open loclist after set
+--- - {client_id}: (number)
+--- - If nil, will consider all clients attached to buffer.
+--- - {severity}: (DiagnosticSeverity)
+--- - Exclusive severity to consider. Overrides {severity_limit}
+--- - {severity_limit}: (DiagnosticSeverity)
+--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
+function M.set_loclist(opts)
+ opts = opts or {}
+
+ local open_loclist = if_nil(opts.open_loclist, true)
+
+ local bufnr = vim.api.nvim_get_current_buf()
+ local buffer_diags = M.get(bufnr, opts.client_id)
+
+ local severity = to_severity(opts.severity)
+ local severity_limit = to_severity(opts.severity_limit)
+
+ local items = {}
+ local insert_diag = function(diag)
+ if severity then
+ -- Handle missing severities
+ if not diag.severity then
+ return
+ end
+
+ if severity ~= diag.severity then
+ return
+ end
+ elseif severity_limit then
+ if not diag.severity then
+ return
+ end
+
+ if severity_limit < diag.severity then
+ return
+ end
+ end
+
+ local pos = diag.range.start
+ local row = pos.line
+ local col = util.character_offset(bufnr, row, pos.character)
+
+ local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1]
+
+ table.insert(items, {
+ bufnr = bufnr,
+ lnum = row + 1,
+ col = col + 1,
+ text = line .. " | " .. diag.message,
+ type = loclist_type_map[diag.severity or DiagnosticSeverity.Error] or 'E',
+ })
+ end
+
+ for _, diag in ipairs(buffer_diags) do
+ insert_diag(diag)
+ end
+
+ table.sort(items, function(a, b) return a.lnum < b.lnum end)
+
+ util.set_loclist(items)
+ if open_loclist then
+ vim.cmd [[lopen]]
+ end
+end
+-- }}}
+
+return M
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
new file mode 100644
index 0000000000..e034923afb
--- /dev/null
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -0,0 +1,310 @@
+local log = require 'vim.lsp.log'
+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
+--- Writes to error buffer.
+--@param ... (table of strings) Will be concatenated before being written
+local function err_message(...)
+ api.nvim_err_writeln(table.concat(vim.tbl_flatten{...}))
+ 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
+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)
+ if not workspace_edit then return end
+ -- TODO(ashkan) Do something more with label?
+ if workspace_edit.label then
+ print("Workspace edit", workspace_edit.label)
+ end
+ local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit)
+ return {
+ applied = status;
+ failureReason = result;
+ }
+end
+
+M['textDocument/publishDiagnostics'] = function(...)
+ return require('vim.lsp.diagnostic').on_publish_diagnostics(...)
+end
+
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
+M['textDocument/references'] = function(_, _, result)
+ if not result then return end
+ util.set_qflist(util.locations_to_items(result))
+ api.nvim_command("copen")
+ api.nvim_command("wincmd p")
+end
+
+--@private
+--- Prints given list of symbols to the quickfix list.
+--@param _ (not used)
+--@param _ (not used)
+--@param result (list of Symbols) LSP method name
+--@param result (table) result of LSP method; a location or a list of locations.
+---(`textDocument/definition` can return `Location` or `Location[]`
+local symbol_handler = function(_, _, result, _, bufnr)
+ if not result or vim.tbl_isempty(result) then return end
+
+ util.set_qflist(util.symbols_to_items(result, bufnr))
+ api.nvim_command("copen")
+ api.nvim_command("wincmd p")
+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)
+ 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)
+ if not result then return end
+ util.apply_text_edits(result)
+end
+
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
+M['textDocument/formatting'] = function(_, _, result)
+ if not result then return end
+ util.apply_text_edits(result)
+end
+
+--@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])
+ local line_to_cursor = line:sub(col+1)
+ local textMatch = vim.fn.match(line_to_cursor, '\\k*$')
+ local prefix = line_to_cursor:sub(textMatch+1)
+
+ local matches = util.text_document_completion_list_to_complete_items(result, prefix)
+ vim.fn.complete(textMatch+1, matches)
+end
+
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
+M['textDocument/hover'] = function(_, method, result)
+ util.focusable_float(method, function()
+ if not (result and result.contents) then
+ -- return { 'No information available' }
+ return
+ end
+ local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
+ markdown_lines = util.trim_empty_lines(markdown_lines)
+ if vim.tbl_isempty(markdown_lines) then
+ -- return { 'No information available' }
+ return
+ end
+ local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, {
+ pad_left = 1; pad_right = 1;
+ })
+ util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr)
+ return bufnr, winnr
+ end)
+end
+
+--@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.
+---(`textDocument/definition` can return `Location` or `Location[]`
+local function location_handler(_, method, result)
+ if result == nil or vim.tbl_isempty(result) then
+ local _ = log.info() and log.info(method, 'No location found')
+ return nil
+ end
+
+ -- textDocument/definition can return Location or Location[]
+ -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
+
+ if vim.tbl_islist(result) then
+ util.jump_to_location(result[1])
+
+ if #result > 1 then
+ util.set_qflist(util.locations_to_items(result))
+ api.nvim_command("copen")
+ api.nvim_command("wincmd p")
+ end
+ else
+ util.jump_to_location(result)
+ end
+end
+
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration
+M['textDocument/declaration'] = location_handler
+--@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
+M['textDocument/typeDefinition'] = location_handler
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
+M['textDocument/implementation'] = location_handler
+
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
+M['textDocument/signatureHelp'] = function(_, method, result)
+ -- 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')
+ return
+ end
+ local lines = util.convert_signature_help_to_markdown_lines(result)
+ lines = util.trim_empty_lines(lines)
+ if vim.tbl_isempty(lines) then
+ print('No signature help available')
+ return
+ end
+ util.focusable_preview(method, function()
+ return lines, util.try_trim_markdown_code_blocks(lines)
+ end)
+end
+
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
+M['textDocument/documentHighlight'] = function(_, _, result, _)
+ if not result then return end
+ local bufnr = api.nvim_get_current_buf()
+ util.buf_highlight_references(bufnr, result)
+end
+
+--@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"`,
+local make_call_hierarchy_handler = function(direction)
+ return function(_, _, result)
+ if not result then return end
+ local items = {}
+ for _, call_hierarchy_call in pairs(result) do
+ local call_hierarchy_item = call_hierarchy_call[direction]
+ for _, range in pairs(call_hierarchy_call.fromRanges) do
+ table.insert(items, {
+ filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)),
+ text = call_hierarchy_item.name,
+ lnum = range.start.line + 1,
+ col = range.start.character + 1,
+ })
+ end
+ end
+ util.set_qflist(items)
+ api.nvim_command("copen")
+ api.nvim_command("wincmd p")
+ end
+end
+
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/incomingCalls
+M['callHierarchy/incomingCalls'] = make_call_hierarchy_handler('from')
+
+--@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)
+ local message_type = result.type
+ local message = result.message
+ 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
+ if message_type == protocol.MessageType.Error then
+ log.error(message)
+ elseif message_type == protocol.MessageType.Warning then
+ log.warn(message)
+ elseif message_type == protocol.MessageType.Info then
+ log.info(message)
+ else
+ log.debug(message)
+ end
+ return result
+end
+
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/showMessage
+M['window/showMessage'] = function(_, _, result, client_id)
+ local message_type = result.type
+ local message = result.message
+ 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
+ if message_type == protocol.MessageType.Error then
+ err_message("LSP[", client_name, "] ", message)
+ else
+ local message_type_name = protocol.MessageType[message_type]
+ api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message))
+ end
+ return result
+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
+ })
+
+ if err then
+ error(tostring(err))
+ end
+
+ return fn(err, method, params, client_id, bufnr, config)
+ end
+end
+
+return M
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 70862320c5..07b4e8b926 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -1,17 +1,8 @@
-- Protocol for the Microsoft Language Server Protocol (mslsp)
-local protocol = {}
-
---@private
---- Returns {a} if it is not nil, otherwise returns {b}.
----
---@param a
---@param b
-local function ifnil(a, b)
- if a == nil then return b end
- return a
-end
+local if_nil = vim.F.if_nil
+local protocol = {}
--[=[
--@private
@@ -909,12 +900,12 @@ function protocol.resolve_capabilities(server_capabilities)
}
elseif type(textDocumentSync) == 'table' then
text_document_sync_properties = {
- text_document_open_close = ifnil(textDocumentSync.openClose, false);
- text_document_did_change = ifnil(textDocumentSync.change, TextDocumentSyncKind.None);
- text_document_will_save = ifnil(textDocumentSync.willSave, false);
- text_document_will_save_wait_until = ifnil(textDocumentSync.willSaveWaitUntil, false);
- text_document_save = ifnil(textDocumentSync.save, false);
- text_document_save_include_text = ifnil(type(textDocumentSync.save) == 'table'
+ text_document_open_close = if_nil(textDocumentSync.openClose, false);
+ text_document_did_change = if_nil(textDocumentSync.change, TextDocumentSyncKind.None);
+ text_document_will_save = if_nil(textDocumentSync.willSave, false);
+ text_document_will_save_wait_until = if_nil(textDocumentSync.willSaveWaitUntil, false);
+ text_document_save = if_nil(textDocumentSync.save, false);
+ text_document_save_include_text = if_nil(type(textDocumentSync.save) == 'table'
and textDocumentSync.save.includeText, false);
}
else
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 17c411f952..bbcc8ea6f9 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -231,41 +231,42 @@ local function rpc_response_error(code, message, data)
})
end
-local default_handlers = {}
+local default_dispatchers = {}
+
--@private
---- Default handler for notifications sent to an LSP server.
+--- 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
-function default_handlers.notification(method, params)
+function default_dispatchers.notification(method, params)
local _ = log.debug() and log.debug('notification', method, params)
end
--@private
---- Default handler for requests sent to an LSP server.
+--- 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`.
-function default_handlers.server_request(method, params)
+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
---- Default handler for when a client exits.
+--- Default dispatcher for when a client exits.
---
--@param code (number): Exit code
--@param signal (number): Number describing the signal used to terminate (if
---any)
-function default_handlers.on_exit(code, signal)
+function default_dispatchers.on_exit(code, signal)
local _ = log.info() and log.info("client_exit", { code = code, signal = signal })
end
--@private
---- Default handler for client errors.
+--- Default dispatcher for client errors.
---
--@param code (number): Error code
--@param err (any): Details about the error
---any)
-function default_handlers.on_error(code, err)
+function default_dispatchers.on_error(code, err)
local _ = log.error() and log.error('client_error:', client_errors[code], err)
end
@@ -274,8 +275,8 @@ end
---
--@param cmd (string) Command to start the LSP server.
--@param cmd_args (table) List of additional string arguments to pass to {cmd}.
---@param handlers (table, optional) Handlers for LSP message types. Valid
----handler names are:
+--@param dispatchers (table, optional) Dispatchers for LSP message types. Valid
+---dispatcher names are:
--- - `"notification"`
--- - `"server_request"`
--- - `"on_error"`
@@ -294,39 +295,39 @@ end
--- - {pid} (number) The LSP server's PID.
--- - {handle} A handle for low-level interaction with the LSP server process
--- |vim.loop|.
-local function start(cmd, cmd_args, handlers, extra_spawn_params)
+local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params})
validate {
cmd = { cmd, 's' };
cmd_args = { cmd_args, 't' };
- handlers = { handlers, 't', true };
+ dispatchers = { dispatchers, 't', true };
}
if not (vim.fn.executable(cmd) == 1) then
error(string.format("The given command %q is not executable.", cmd))
end
- if handlers then
- local user_handlers = handlers
- handlers = {}
- for handle_name, default_handler in pairs(default_handlers) do
- local user_handler = user_handlers[handle_name]
- if user_handler then
- if type(user_handler) ~= 'function' then
- error(string.format("handler.%s must be a function", handle_name))
+ if dispatchers then
+ local user_dispatchers = dispatchers
+ dispatchers = {}
+ for dispatch_name, default_dispatch in pairs(default_dispatchers) do
+ local user_dispatcher = user_dispatchers[dispatch_name]
+ if user_dispatcher then
+ if type(user_dispatcher) ~= 'function' then
+ error(string.format("dispatcher.%s must be a function", dispatch_name))
end
-- server_request is wrapped elsewhere.
- if not (handle_name == 'server_request'
- or handle_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
+ if not (dispatch_name == 'server_request'
+ or dispatch_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
then
- user_handler = schedule_wrap(user_handler)
+ user_dispatcher = schedule_wrap(user_dispatcher)
end
- handlers[handle_name] = user_handler
+ dispatchers[dispatch_name] = user_dispatcher
else
- handlers[handle_name] = default_handler
+ dispatchers[dispatch_name] = default_dispatch
end
end
else
- handlers = default_handlers
+ dispatchers = default_dispatchers
end
local stdin = uv.new_pipe(false)
@@ -339,8 +340,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
local handle, pid
do
--@private
- --- Callback for |vim.loop.spawn()| Closes all streams and runs the
- --- `on_exit` handler.
+ --- 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)
local function onexit(code, signal)
@@ -350,7 +350,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
handle:close()
-- Make sure that message_callbacks can be gc'd.
message_callbacks = nil
- handlers.on_exit(code, signal)
+ dispatchers.on_exit(code, signal)
end
local spawn_params = {
args = cmd_args;
@@ -448,7 +448,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
local function on_error(errkind, ...)
assert(client_errors[errkind])
-- TODO what to do if this fails?
- pcall(handlers.on_error, errkind, ...)
+ pcall(dispatchers.on_error, errkind, ...)
end
--@private
local function pcall_handler(errkind, status, head, ...)
@@ -471,7 +471,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
local function handle_body(body)
local decoded, err = json_decode(body)
if not decoded then
- on_error(client_errors.INVALID_SERVER_JSON, err)
+ -- on_error(client_errors.INVALID_SERVER_JSON, err)
return
end
local _ = log.debug() and log.debug("decoded", decoded)
@@ -484,7 +484,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
schedule(function()
local status, result
status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR,
- handlers.server_request, decoded.method, decoded.params)
+ dispatchers.server_request, decoded.method, decoded.params)
local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err })
if status then
if not (result or err) then
@@ -551,7 +551,7 @@ local function start(cmd, cmd_args, handlers, extra_spawn_params)
-- Notification
decoded.params = convert_NIL(decoded.params)
try_call(client_errors.NOTIFICATION_HANDLER_ERROR,
- handlers.notification, decoded.method, decoded.params)
+ dispatchers.notification, decoded.method, decoded.params)
else
-- Invalid server message
on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 9ed19b938d..3deec6d74e 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -5,50 +5,32 @@ local api = vim.api
local list_extend = vim.list_extend
local highlight = require 'vim.highlight'
+local npcall = vim.F.npcall
+local split = vim.split
+
+local _warned = {}
+local warn_once = function(message)
+ if not _warned[message] then
+ vim.api.nvim_err_writeln(message)
+ _warned[message] = true
+ end
+end
+
local M = {}
--- FIXME: DOC: Expose in vimdocs
---- Diagnostics received from the server via `textDocument/publishDiagnostics`
--- by buffer.
---
--- {<bufnr>: {diagnostics}}
---
--- This contains only entries for active buffers. Entries for detached buffers
--- are discarded.
---
--- If you override the `textDocument/publishDiagnostic` callback,
--- this will be empty unless you call `buf_diagnostics_save_positions`.
---
---
--- Diagnostic is:
---
--- {
--- range: Range
--- message: string
--- severity?: DiagnosticSeverity
--- code?: number | string
--- source?: string
--- tags?: DiagnosticTag[]
--- relatedInformation?: DiagnosticRelatedInformation[]
--- }
-M.diagnostics_by_buf = {}
+-- TODO(remove-callbacks)
+M.diagnostics_by_buf = setmetatable({}, {
+ __index = function(_, bufnr)
+ warn_once("diagnostics_by_buf is deprecated. Use 'vim.lsp.diagnostic.get'")
+ return vim.lsp.diagnostic.get(bufnr)
+ end
+})
-local split = vim.split
--@private
local function split_lines(value)
return split(value, '\n', true)
end
---@private
-local function ok_or_nil(status, ...)
- if not status then return end
- return ...
-end
---@private
-local function npcall(fn, ...)
- return ok_or_nil(pcall(fn, ...))
-end
-
--- Replaces text in a range with new text.
---
--- CAUTION: Changes in-place!
@@ -121,10 +103,18 @@ local function get_line_byte_from_position(bufnr, position)
-- When on the first character, we can ignore the difference between byte and
-- character
if col > 0 then
+ if not api.nvim_buf_is_loaded(bufnr) then
+ vim.fn.bufload(bufnr)
+ end
+
local line = position.line
local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false)
if #lines > 0 then
- return vim.str_byteindex(lines[1], col)
+ local ok, result = pcall(vim.str_byteindex, lines[1], col)
+
+ if ok then
+ return result
+ end
end
end
return col
@@ -700,13 +690,13 @@ end
--- Trims empty lines from input and pad left and right with spaces
---
---@param contents table of lines to trim and pad
---@param opts dictionary with optional fields
--- - pad_left number of columns to pad contents at left (default 1)
--- - pad_right number of columns to pad contents at right (default 1)
--- - pad_top number of lines to pad contents at top (default 0)
--- - pad_bottom number of lines to pad contents at bottom (default 0)
---@returns contents table of trimmed and padded lines
+---@param contents table of lines to trim and pad
+---@param opts dictionary with optional fields
+--- - pad_left number of columns to pad contents at left (default 1)
+--- - pad_right number of columns to pad contents at right (default 1)
+--- - pad_top number of lines to pad contents at top (default 0)
+--- - pad_bottom number of lines to pad contents at bottom (default 0)
+---@return contents table of trimmed and padded lines
function M._trim_and_pad(contents, opts)
validate {
contents = { contents, 't' };
@@ -742,19 +732,19 @@ end
--- regions to improve readability.
--- The result is shown in a floating preview.
---
---@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
--- - pad_left number of columns to pad contents at left
--- - pad_right number of columns to pad contents at right
--- - pad_top number of lines to pad contents at top
--- - pad_bottom number of lines to pad contents at bottom
--- - separator insert separator after code block
---@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
+--- - pad_left number of columns to pad contents at left
+--- - pad_right number of columns to pad contents at right
+--- - pad_top number of lines to pad contents at top
+--- - pad_bottom number of lines to pad contents at bottom
+--- - separator insert separator after code block
+---@returns width,height size of float
function M.fancy_floating_markdown(contents, opts)
validate {
contents = { contents, 't' };
@@ -971,170 +961,80 @@ function M.open_floating_preview(contents, filetype, opts)
return floating_bufnr, floating_winnr
end
+-- TODO(remove-callbacks)
do
- local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics")
- local reference_ns = api.nvim_create_namespace("vim_lsp_references")
- local sign_ns = 'vim_lsp_signs'
- local underline_highlight_name = "LspDiagnosticsUnderline"
- vim.cmd(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name))
- for kind, _ in pairs(protocol.DiagnosticSeverity) do
- if type(kind) == 'string' then
- vim.cmd(string.format("highlight default link %s%s %s", underline_highlight_name, kind, underline_highlight_name))
- end
+ --@deprecated
+ function M.get_severity_highlight_name(severity)
+ warn_once("vim.lsp.util.get_severity_highlight_name is deprecated.")
+ return vim.lsp.diagnostic._get_severity_highlight_name(severity)
end
- local severity_highlights = {}
+ --@deprecated
+ function M.buf_clear_diagnostics(bufnr, client_id)
+ warn_once("buf_clear_diagnostics is deprecated. Use vim.lsp.diagnostic.clear")
+ return vim.lsp.diagnostic.clear(bufnr, client_id)
+ end
- local severity_floating_highlights = {}
+ --@deprecated
+ function M.get_line_diagnostics()
+ warn_once("get_line_diagnostics is deprecated. Use vim.lsp.diagnostic.get_line_diagnostics")
- local default_severity_highlight = {
- [protocol.DiagnosticSeverity.Error] = { guifg = "Red" };
- [protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" };
- [protocol.DiagnosticSeverity.Information] = { guifg = "LightBlue" };
- [protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" };
- }
+ local bufnr = api.nvim_get_current_buf()
+ local line_nr = api.nvim_win_get_cursor(0)[1] - 1
- -- Initialize default severity highlights
- for severity, hi_info in pairs(default_severity_highlight) do
- local severity_name = protocol.DiagnosticSeverity[severity]
- local highlight_name = "LspDiagnostics"..severity_name
- local floating_highlight_name = highlight_name.."Floating"
- -- Try to fill in the foreground color with a sane default.
- local cmd_parts = {"highlight", "default", highlight_name}
- for k, v in pairs(hi_info) do
- table.insert(cmd_parts, k.."="..v)
- end
- api.nvim_command(table.concat(cmd_parts, ' '))
- api.nvim_command('highlight link ' .. highlight_name .. 'Sign ' .. highlight_name)
- api.nvim_command('highlight link ' .. highlight_name .. 'Floating ' .. highlight_name)
- severity_highlights[severity] = highlight_name
- severity_floating_highlights[severity] = floating_highlight_name
+ return vim.lsp.diagnostic.get_line_diagnostics(bufnr, line_nr)
end
- --- Clears diagnostics for a buffer.
- ---
- --@param bufnr (number) buffer id
- function M.buf_clear_diagnostics(bufnr)
- validate { bufnr = {bufnr, 'n', true} }
- bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
+ --@deprecated
+ function M.show_line_diagnostics()
+ warn_once("show_line_diagnostics is deprecated. Use vim.lsp.diagnostic.show_line_diagnostics")
- -- clear sign group
- vim.fn.sign_unplace(sign_ns, {buffer=bufnr})
+ local bufnr = api.nvim_get_current_buf()
+ local line_nr = api.nvim_win_get_cursor(0)[1] - 1
- -- clear virtual text namespace
- api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1)
+ return vim.lsp.diagnostic.show_line_diagnostics(bufnr, line_nr)
end
- --- Gets the name of a severity's highlight group.
- ---
- --@param severity A member of `vim.lsp.protocol.DiagnosticSeverity`
- --@returns (string) Highlight group name
- function M.get_severity_highlight_name(severity)
- return severity_highlights[severity]
+ --@deprecated
+ function M.buf_diagnostics_save_positions(bufnr, diagnostics, client_id)
+ warn_once("buf_diagnostics_save_positions is deprecated. Use vim.lsp.diagnostic.save")
+ return vim.lsp.diagnostic.save(diagnostics, bufnr, client_id)
end
- --- Gets list of diagnostics for the current line.
- ---
- --@returns (table) list of `Diagnostic` tables
- --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
- function M.get_line_diagnostics()
- local bufnr = api.nvim_get_current_buf()
- local linenr = api.nvim_win_get_cursor(0)[1] - 1
-
- local buffer_diagnostics = M.diagnostics_by_buf[bufnr]
+ --@deprecated
+ function M.buf_diagnostics_get_positions(bufnr, client_id)
+ warn_once("buf_diagnostics_get_positions is deprecated. Use vim.lsp.diagnostic.get")
+ return vim.lsp.diagnostic.get(bufnr, client_id)
+ end
- if not buffer_diagnostics then
- return {}
- end
+ --@deprecated
+ function M.buf_diagnostics_underline(bufnr, diagnostics, client_id)
+ warn_once("buf_diagnostics_underline is deprecated. Use 'vim.lsp.diagnostic.set_underline'")
+ return vim.lsp.diagnostic.set_underline(diagnostics, bufnr, client_id)
+ end
- local diagnostics_by_line = M.diagnostics_group_by_line(buffer_diagnostics)
- return diagnostics_by_line[linenr] or {}
+ --@deprecated
+ function M.buf_diagnostics_virtual_text(bufnr, diagnostics, client_id)
+ warn_once("buf_diagnostics_virtual_text is deprecated. Use 'vim.lsp.diagnostic.set_virtual_text'")
+ return vim.lsp.diagnostic.set_virtual_text(diagnostics, bufnr, client_id)
end
- --- Displays the diagnostics for the current line in a floating hover
- --- window.
- function M.show_line_diagnostics()
- -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {})
- -- if #marks == 0 then
- -- return
- -- end
- local lines = {"Diagnostics:"}
- local highlights = {{0, "Bold"}}
- local line_diagnostics = M.get_line_diagnostics()
- if vim.tbl_isempty(line_diagnostics) then return end
-
- for i, diagnostic in ipairs(line_diagnostics) do
- -- for i, mark in ipairs(marks) do
- -- local mark_id = mark[1]
- -- local diagnostic = buffer_diagnostics[mark_id]
-
- -- TODO(ashkan) make format configurable?
- local prefix = string.format("%d. ", i)
- local hiname = severity_floating_highlights[diagnostic.severity]
- assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity))
- local message_lines = split_lines(diagnostic.message)
- table.insert(lines, prefix..message_lines[1])
- table.insert(highlights, {#prefix + 1, hiname})
- for j = 2, #message_lines do
- table.insert(lines, message_lines[j])
- table.insert(highlights, {0, hiname})
- end
- end
- local popup_bufnr, winnr = M.open_floating_preview(lines, 'plaintext')
- 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)
- end
- return popup_bufnr, winnr
+ --@deprecated
+ function M.buf_diagnostics_signs(bufnr, diagnostics, client_id)
+ warn_once("buf_diagnostics_signs is deprecated. Use 'vim.lsp.diagnostics.set_signs'")
+ return vim.lsp.diagnostic.set_signs(diagnostics, bufnr, client_id)
end
- --- Saves diagnostics into vim.lsp.util.diagnostics_by_buf[{bufnr}].
- ---
- --@param bufnr (number) buffer id for which the diagnostics are for
- --@param diagnostics list of `Diagnostic`s received from the LSP server
- function M.buf_diagnostics_save_positions(bufnr, diagnostics)
- validate {
- bufnr = {bufnr, 'n', true};
- diagnostics = {diagnostics, 't', true};
- }
- if not diagnostics then return end
- bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
-
- if not M.diagnostics_by_buf[bufnr] then
- -- Clean up our data when the buffer unloads.
- api.nvim_buf_attach(bufnr, false, {
- on_detach = function(b)
- M.diagnostics_by_buf[b] = nil
- end
- })
- end
- M.diagnostics_by_buf[bufnr] = diagnostics
+ --@deprecated
+ function M.buf_diagnostics_count(kind, client_id)
+ warn_once("buf_diagnostics_count is deprecated. Use 'vim.lsp.diagnostic.get_count'")
+ return vim.lsp.diagnostic.get_count(vim.api.nvim_get_current_buf(), client_id, kind)
end
- --- Highlights a list of diagnostics in a buffer by underlining them.
- ---
- --@param bufnr (number) buffer id
- --@param diagnostics (list of `Diagnostic`s)
- function M.buf_diagnostics_underline(bufnr, diagnostics)
- for _, diagnostic in ipairs(diagnostics) do
- local start = diagnostic.range["start"]
- local finish = diagnostic.range["end"]
-
- local hlmap = {
- [protocol.DiagnosticSeverity.Error]='Error',
- [protocol.DiagnosticSeverity.Warning]='Warning',
- [protocol.DiagnosticSeverity.Information]='Information',
- [protocol.DiagnosticSeverity.Hint]='Hint',
- }
+end
- highlight.range(bufnr, diagnostic_ns,
- underline_highlight_name..hlmap[diagnostic.severity],
- {start.line, start.character},
- {finish.line, finish.character}
- )
- end
- end
+do --[[ References ]]
+ local reference_ns = api.nvim_create_namespace("vim_lsp_references")
--- Removes document highlights from a buffer.
---
@@ -1162,109 +1062,6 @@ do
highlight.range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos)
end
end
-
- --- Groups a list of diagnostics by line.
- ---
- --@param diagnostics (table) list of `Diagnostic`s
- --@returns (table) dictionary mapping lines to lists of diagnostics valid on
- ---those lines
- --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
- function M.diagnostics_group_by_line(diagnostics)
- if not diagnostics then return end
- local diagnostics_by_line = {}
- for _, diagnostic in ipairs(diagnostics) do
- local start = diagnostic.range.start
- -- TODO: Are diagnostics only valid for a single line? I don't understand
- -- why this would be okay otherwise
- 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)
- end
- return diagnostics_by_line
- end
-
- --- Given a list of diagnostics, sets the corresponding virtual text for a
- --- buffer.
- ---
- --@param bufnr buffer id
- --@param diagnostics (table) list of `Diagnostic`s
- function M.buf_diagnostics_virtual_text(bufnr, diagnostics)
- if not diagnostics then
- return
- end
- local buffer_line_diagnostics = M.diagnostics_group_by_line(diagnostics)
- for line, line_diags in pairs(buffer_line_diagnostics) do
- local virt_texts = {}
- for i = 1, #line_diags - 1 do
- table.insert(virt_texts, {"■", severity_highlights[line_diags[i].severity]})
- end
- local last = line_diags[#line_diags]
- -- TODO(ashkan) use first line instead of subbing 2 spaces?
- table.insert(virt_texts, {"■ "..last.message:gsub("\r", ""):gsub("\n", " "), severity_highlights[last.severity]})
- api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {})
- end
- end
-
- --- Returns the number of diagnostics of given kind for current buffer.
- ---
- --- 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.util.buf_diagnostics_count([[Error]])")}'
- --- let sl.='%#MyStatuslineLSP# W:'
- --- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Warning]])")}'
- --- else
- --- let sl.='%#MyStatuslineLSPErrors#off'
- --- endif
- --- return sl
- --- endfunction
- --- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
- --- </pre>
- ---
- --@param kind Diagnostic severity kind: See |vim.lsp.protocol.DiagnosticSeverity|
- --@returns Count of diagnostics
- function M.buf_diagnostics_count(kind)
- local bufnr = vim.api.nvim_get_current_buf()
- local diagnostics = M.diagnostics_by_buf[bufnr]
- if not diagnostics then return end
- local count = 0
- for _, diagnostic in pairs(diagnostics) do
- if protocol.DiagnosticSeverity[kind] == diagnostic.severity then
- count = count + 1
- end
- end
- return count
- end
-
- local diagnostic_severity_map = {
- [protocol.DiagnosticSeverity.Error] = "LspDiagnosticsErrorSign";
- [protocol.DiagnosticSeverity.Warning] = "LspDiagnosticsWarningSign";
- [protocol.DiagnosticSeverity.Information] = "LspDiagnosticsInformationSign";
- [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign";
- }
-
- --- Places signs for each diagnostic in the sign column.
- ---
- --- Sign characters can be customized with the following commands:
- ---
- --- <pre>
- --- sign define LspDiagnosticsErrorSign text=E texthl=LspDiagnosticsError linehl= numhl=
- --- sign define LspDiagnosticsWarningSign text=W texthl=LspDiagnosticsWarning linehl= numhl=
- --- sign define LspDiagnosticsInformationSign text=I texthl=LspDiagnosticsInformation linehl= numhl=
- --- sign define LspDiagnosticsHintSign text=H texthl=LspDiagnosticsHint linehl= numhl=
- --- </pre>
- function M.buf_diagnostics_signs(bufnr, diagnostics)
- for _, diagnostic in ipairs(diagnostics) do
- vim.fn.sign_place(0, sign_ns, diagnostic_severity_map[diagnostic.severity], bufnr, {lnum=(diagnostic.range.start.line+1)})
- end
- end
end
local position_sort = sort_by_key(function(v)
@@ -1561,6 +1358,9 @@ function M.character_offset(buf, row, col)
return str_utfindex(line, col)
end
+M._get_line_byte_from_position = get_line_byte_from_position
+M._warn_once = warn_once
+
M.buf_versions = {}
return M