aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2021-09-16 14:23:42 -0700
committerGitHub <noreply@github.com>2021-09-16 14:23:42 -0700
commit2e8103475e18d1e4aa0d6355107f393815a886a6 (patch)
tree3bca29fcd412cd4ce942a662229aa541977a63d6 /runtime/lua/vim
parent7d21b958691c06ed6b40aa1909cd81c37a67844e (diff)
parent4fca63dbf722f60f9096ef32c0dbecc2055c4a9a (diff)
downloadrneovim-2e8103475e18d1e4aa0d6355107f393815a886a6.tar.gz
rneovim-2e8103475e18d1e4aa0d6355107f393815a886a6.tar.bz2
rneovim-2e8103475e18d1e4aa0d6355107f393815a886a6.zip
Merge #15585 refactor: move vim.lsp.diagnostic to vim.diagnostic
## Overview - Move vim.lsp.diagnostic to vim.diagnostic - Refactor client ids to diagnostic namespaces - Update tests - Write/update documentation and function signatures Currently, non-LSP diagnostics in Neovim must hook into the LSP subsystem. This is what e.g. null-ls and nvim-lint do. This is necessary because none of the diagnostic API is exposed separately from the LSP subsystem. This commit addresses this by generalizing the diagnostic subsystem beyond the scope of LSP. The `vim.lsp.diagnostic` module is now simply a specific diagnostic producer and primarily maintains the interface between LSP clients and the broader diagnostic API. The current diagnostic API uses "client ids" which only makes sense in the context of LSP. We replace "client ids" with standard API namespaces generated from `nvim_create_namespace`. This PR is *mostly* backward compatible (so long as plugins are only using the publicly documented API): LSP diagnostics will continue to work as usual, as will pseudo-LSP clients like null-ls and nvim-lint. However, the latter can now use the new interface, which looks something like this: ```lua -- The namespace *must* be given a name. Anonymous namespaces will not work with diagnostics local ns = vim.api.nvim_create_namespace("foo") -- Generate diagnostics local diagnostics = generate_diagnostics() -- Set diagnostics for the current buffer vim.diagnostic.set(ns, diagnostics, bufnr) ``` Some public facing API utility methods were removed and internalized directly in `vim.diagnostic`: * `vim.lsp.util.diagnostics_to_items` ## API Design `vim.diagnostic` contains most of the same API as `vim.lsp.diagnostic` with `client_id` simply replaced with `namespace`, with some differences: * Generally speaking, functions that modify or add diagnostics require a namespace as their first argument, e.g. ```lua vim.diagnostic.set({namespace}, {bufnr}, {diagnostics}[, {opts}]) ``` while functions that read or query diagnostics do not (although in many cases one may be supplied optionally): ```lua vim.diagnostic.get({bufnr}[, {namespace}]) ``` * We use our own severity levels to decouple `vim.diagnostic` from LSP. These are designed similarly to `vim.log.levels` and currently include: ```lua vim.diagnostic.severity.ERROR vim.diagnostic.severity.WARN vim.diagnostic.severity.INFO vim.diagnostic.severity.HINT ``` In practice, these match the LSP diagnostic severity levels exactly, but we should treat this as an interface and not assume that they are the same. The "translation" between the two severity types is handled transparently in `vim.lsp.diagnostic`. * The actual "diagnostic" data structure is: (**EDIT:** Updated 2021-09-09): ```lua { lnum = <number>, col = <number>, end_lnum = <number>, end_col = <number>, severity = <vim.diagnostic.severity>, message = <string> } ``` This differs from the LSP definition of a diagnostic, so we transform them in the handler functions in vim.lsp.diagnostic. ## Configuration The `vim.lsp.with` paradigm still works for configuring how LSP diagnostics are displayed, but this is a specific use-case for the `publishDiagnostics` handler. Configuration with `vim.diagnostic` is instead done with the `vim.diagnostic.config` function: ```lua vim.diagnostic.config({ virtual_text = true, signs = false, underline = true, update_in_insert = true, severity_sort = false, }[, namespace]) ``` (or alternatively passed directly to `set()` or `show()`.) When the `namespace` argument is `nil`, settings are set globally (i.e. for *all* diagnostic namespaces). This is what user's will typically use for their local configuration. Diagnostic producers can also set configuration options for their specific namespace, although this is generally discouraged in order to respect the user's global settings. All of the values in the table passed to `vim.diagnostic.config()` are resolved in the same way that they are in `on_publish_diagnostics`; that is, the value can be a boolean, a table, or a function: ```lua vim.diagnostic.config({ virtual_text = function(namespace, bufnr) -- Only enable virtual text in buffer 3 return bufnr == 3 end, }) ``` ## Misc Notes * `vim.diagnostic` currently depends on `vim.lsp.util` for floating window previews. I think this is okay for now, although ideally we'd want to decouple these completely.
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/diagnostic.lua1150
-rw-r--r--runtime/lua/vim/lsp.lua3
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua1477
-rw-r--r--runtime/lua/vim/lsp/handlers.lua10
-rw-r--r--runtime/lua/vim/lsp/util.lua51
5 files changed, 1537 insertions, 1154 deletions
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
new file mode 100644
index 0000000000..33fa07ef4c
--- /dev/null
+++ b/runtime/lua/vim/diagnostic.lua
@@ -0,0 +1,1150 @@
+local M = {}
+
+M.severity = {
+ ERROR = 1,
+ WARN = 2,
+ INFO = 3,
+ HINT = 4,
+}
+
+vim.tbl_add_reverse_lookup(M.severity)
+
+local global_diagnostic_options = {
+ signs = true,
+ underline = true,
+ virtual_text = true,
+ update_in_insert = false,
+ severity_sort = false,
+}
+
+-- Local functions {{{
+
+---@private
+local function to_severity(severity)
+ return type(severity) == 'string' and M.severity[string.upper(severity)] or severity
+end
+
+---@private
+local function filter_by_severity(severity, diagnostics)
+ if not severity then
+ return diagnostics
+ end
+
+ if type(severity) ~= "table" then
+ severity = to_severity(severity)
+ return vim.tbl_filter(function(t) return t.severity == severity end, diagnostics)
+ end
+
+ local min_severity = to_severity(severity.min) or M.severity.HINT
+ local max_severity = to_severity(severity.max) or M.severity.ERROR
+
+ return vim.tbl_filter(function(t) return t.severity <= min_severity and t.severity >= max_severity end, diagnostics)
+end
+
+---@private
+local function resolve_optional_value(option, namespace, bufnr)
+ local enabled_val = {}
+
+ if not option then
+ return false
+ elseif option == true then
+ return enabled_val
+ elseif type(option) == 'function' then
+ local val = option(namespace, bufnr)
+ 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
+
+local all_namespaces = {}
+
+---@private
+local function get_namespace(ns)
+ if not all_namespaces[ns] then
+ local name
+ for k, v in pairs(vim.api.nvim_get_namespaces()) do
+ if ns == v then
+ name = k
+ break
+ end
+ end
+
+ if not name then
+ return vim.notify("namespace does not exist or is anonymous", vim.log.levels.ERROR)
+ end
+
+ all_namespaces[ns] = {
+ name = name,
+ sign_group = string.format("vim.diagnostic.%s", name),
+ opts = {}
+ }
+ end
+ return all_namespaces[ns]
+end
+
+---@private
+local function get_resolved_options(opts, namespace, bufnr)
+ local ns = get_namespace(namespace)
+ local resolved = vim.tbl_extend('keep', opts or {}, ns.opts, global_diagnostic_options)
+ for k in pairs(global_diagnostic_options) do
+ if resolved[k] ~= nil then
+ resolved[k] = resolve_optional_value(resolved[k], namespace, bufnr)
+ end
+ end
+ return resolved
+end
+
+-- Default diagnostic highlights
+local diagnostic_severities = {
+ [M.severity.ERROR] = { ctermfg = 1, guifg = "Red" };
+ [M.severity.WARN] = { ctermfg = 3, guifg = "Orange" };
+ [M.severity.INFO] = { ctermfg = 4, guifg = "LightBlue" };
+ [M.severity.HINT] = { ctermfg = 7, guifg = "LightGrey" };
+}
+
+-- Make a map from DiagnosticSeverity -> Highlight Name
+---@private
+local function make_highlight_map(base_name)
+ local result = {}
+ for k in pairs(diagnostic_severities) do
+ local name = M.severity[k]
+ name = name:sub(1, 1) .. name:sub(2):lower()
+ result[k] = "Diagnostic" .. base_name .. name
+ end
+
+ return result
+end
+
+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")
+
+---@private
+local define_default_signs = (function()
+ local signs_defined = false
+ return function()
+ if signs_defined then
+ return
+ end
+
+ for severity, sign_hl_name in pairs(sign_highlight_map) do
+ local severity_name = M.severity[severity]
+ vim.fn.sign_define(sign_hl_name, {
+ text = (severity_name or 'U'):sub(1, 1),
+ texthl = sign_hl_name,
+ linehl = '',
+ numhl = '',
+ })
+ end
+
+ signs_defined = true
+ end
+end)()
+
+---@private
+local function get_bufnr(bufnr)
+ if not bufnr or bufnr == 0 then
+ return vim.api.nvim_get_current_buf()
+ end
+ return bufnr
+end
+
+-- Metatable that automatically creates an empty table when assigning to a missing key
+local bufnr_and_namespace_cacher_mt = {
+ __index = function(t, bufnr)
+ if not bufnr or bufnr == 0 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 not bufnr or bufnr == 0 then
+ bufnr = vim.api.nvim_get_current_buf()
+ end
+
+ rawset(t, bufnr, v)
+ end,
+}
+
+local diagnostic_cleanup = setmetatable({}, bufnr_and_namespace_cacher_mt)
+local diagnostic_cache = setmetatable({}, bufnr_and_namespace_cacher_mt)
+local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt)
+local diagnostic_attached_buffers = {}
+local diagnostic_disabled = {}
+local bufs_waiting_to_update = setmetatable({}, bufnr_and_namespace_cacher_mt)
+
+---@private
+local function is_disabled(namespace, bufnr)
+ if type(diagnostic_disabled[bufnr]) == "table" then
+ return diagnostic_disabled[bufnr][namespace]
+ end
+ return diagnostic_disabled[bufnr]
+end
+
+---@private
+local function diagnostic_lines(diagnostics)
+ if not diagnostics then
+ return
+ end
+
+ local diagnostics_by_line = {}
+ for _, diagnostic in ipairs(diagnostics) do
+ local line_diagnostics = diagnostics_by_line[diagnostic.lnum]
+ if not line_diagnostics then
+ line_diagnostics = {}
+ diagnostics_by_line[diagnostic.lnum] = line_diagnostics
+ end
+ table.insert(line_diagnostics, diagnostic)
+ end
+ return diagnostics_by_line
+end
+
+---@private
+local function set_diagnostic_cache(namespace, diagnostics, bufnr)
+ local buf_line_count = vim.api.nvim_buf_line_count(bufnr)
+ for _, diagnostic in ipairs(diagnostics) do
+ if diagnostic.severity == nil then
+ diagnostic.severity = M.severity.ERROR
+ end
+
+ diagnostic.namespace = namespace
+ diagnostic.bufnr = bufnr
+
+ if buf_line_count > 0 then
+ diagnostic.lnum = math.max(math.min(
+ diagnostic.lnum, buf_line_count - 1
+ ), 0)
+ diagnostic.end_lnum = math.max(math.min(
+ diagnostic.end_lnum, buf_line_count - 1
+ ), 0)
+ end
+ end
+
+ diagnostic_cache[bufnr][namespace] = diagnostics
+end
+
+---@private
+local function clear_diagnostic_cache(namespace, bufnr)
+ diagnostic_cache[bufnr][namespace] = nil
+end
+
+---@private
+local function restore_extmarks(bufnr, last)
+ for ns, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do
+ local extmarks_current = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true})
+ local found = {}
+ for _, extmark in ipairs(extmarks_current) do
+ -- nvim_buf_set_lines will move any extmark to the line after the last
+ -- nvim_buf_set_text will move any extmark to the last line
+ if extmark[2] ~= last + 1 then
+ found[extmark[1]] = true
+ end
+ end
+ for _, extmark in ipairs(extmarks) do
+ if not found[extmark[1]] then
+ local opts = extmark[4]
+ opts.id = extmark[1]
+ -- HACK: end_row should be end_line
+ if opts.end_row then
+ opts.end_line = opts.end_row
+ opts.end_row = nil
+ end
+ pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
+ end
+ end
+ end
+end
+
+---@private
+local function save_extmarks(namespace, bufnr)
+ bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr
+ if not diagnostic_attached_buffers[bufnr] then
+ vim.api.nvim_buf_attach(bufnr, false, {
+ on_lines = function(_, _, _, _, _, last)
+ restore_extmarks(bufnr, last - 1)
+ end,
+ on_detach = function()
+ diagnostic_cache_extmarks[bufnr] = nil
+ end})
+ diagnostic_attached_buffers[bufnr] = true
+ end
+ diagnostic_cache_extmarks[bufnr][namespace] = vim.api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, {details = true})
+end
+
+local registered_autocmds = {}
+
+---@private
+local function make_augroup_key(namespace, bufnr)
+ local ns = get_namespace(namespace)
+ return string.format("DiagnosticInsertLeave:%s:%s", bufnr, ns.name)
+end
+
+--- Table of autocmd events to fire the update for displaying new diagnostic information
+local insert_leave_auto_cmds = { "InsertLeave", "CursorHoldI" }
+
+---@private
+local function schedule_display(namespace, bufnr, args)
+ bufs_waiting_to_update[bufnr][namespace] = args
+
+ local key = make_augroup_key(namespace, bufnr)
+ if not registered_autocmds[key] then
+ vim.cmd(string.format("augroup %s", key))
+ vim.cmd(" au!")
+ vim.cmd(
+ string.format(
+ [[autocmd %s <buffer=%s> lua vim.diagnostic._execute_scheduled_display(%s, %s)]],
+ table.concat(insert_leave_auto_cmds, ","),
+ bufnr,
+ namespace,
+ bufnr
+ )
+ )
+ vim.cmd("augroup END")
+
+ registered_autocmds[key] = true
+ end
+end
+
+---@private
+local function clear_scheduled_display(namespace, bufnr)
+ local key = make_augroup_key(namespace, bufnr)
+
+ if registered_autocmds[key] then
+ vim.cmd(string.format("augroup %s", key))
+ vim.cmd(" au!")
+ vim.cmd("augroup END")
+
+ registered_autocmds[key] = nil
+ end
+end
+
+---@private
+--- Open a floating window with the provided diagnostics
+---@param opts table Configuration table
+--- - show_header (boolean, default true): Show "Diagnostics:" header
+--- - all opts for |vim.util.open_floating_preview()| can be used here
+---@param diagnostics table: The diagnostics to display
+---@return table {popup_bufnr, win_id}
+local function show_diagnostics(opts, diagnostics)
+ if vim.tbl_isempty(diagnostics) then return end
+ local lines = {}
+ local highlights = {}
+ local show_header = vim.F.if_nil(opts.show_header, true)
+ if show_header then
+ table.insert(lines, "Diagnostics:")
+ table.insert(highlights, {0, "Bold"})
+ end
+
+ for i, diagnostic in ipairs(diagnostics) do
+ local prefix = string.format("%d. ", i)
+ local hiname = floating_highlight_map[diagnostic.severity]
+ assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity))
+
+ local message_lines = vim.split(diagnostic.message, '\n', true)
+ table.insert(lines, prefix..message_lines[1])
+ table.insert(highlights, {#prefix, hiname})
+ for j = 2, #message_lines do
+ table.insert(lines, string.rep(' ', #prefix) .. message_lines[j])
+ table.insert(highlights, {0, hiname})
+ end
+ end
+
+ local popup_bufnr, winnr = require('vim.lsp.util').open_floating_preview(lines, 'plaintext', opts)
+ for i, hi in ipairs(highlights) do
+ local prefixlen, hiname = unpack(hi)
+ -- Start highlight after the prefix
+ vim.api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1)
+ end
+
+ return popup_bufnr, winnr
+end
+
+local errlist_type_map = {
+ [M.severity.ERROR] = 'E',
+ [M.severity.WARN] = 'W',
+ [M.severity.INFO] = 'I',
+ [M.severity.HINT] = 'I',
+}
+
+---@private
+local function diagnostics_to_list_items(diagnostics)
+ local items = {}
+ for _, d in pairs(diagnostics) do
+ table.insert(items, {
+ bufnr = d.bufnr,
+ lnum = d.lnum + 1,
+ col = d.col + 1,
+ text = d.message,
+ type = errlist_type_map[d.severity or M.severity.ERROR] or 'E'
+ })
+ end
+ table.sort(items, function(a, b)
+ if a.bufnr == b.bufnr then
+ return a.lnum < b.lnum
+ else
+ return a.bufnr < b.bufnr
+ end
+ end)
+ return items
+end
+
+---@private
+local function set_list(loclist, opts)
+ opts = opts or {}
+ local open = vim.F.if_nil(opts.open, true)
+ local title = opts.title or "Diagnostics"
+ local winnr = opts.winnr or 0
+ local diagnostics = M.get(loclist and vim.api.nvim_win_get_buf(winnr), opts)
+ local items = diagnostics_to_list_items(diagnostics)
+ if loclist then
+ vim.fn.setloclist(winnr, {}, ' ', { title = title, items = items })
+ else
+ vim.fn.setqflist({}, ' ', { title = title, items = items })
+ end
+ if open then
+ vim.api.nvim_command(loclist and "lopen" or "copen")
+ end
+end
+
+-- }}}
+
+-- Public API {{{
+
+--- Configure diagnostic options globally or for a specific diagnostic
+--- namespace.
+---
+---@note Each of the configuration options below accepts one of the following:
+--- - `false`: Disable this feature
+--- - `true`: Enable this feature, use default settings.
+--- - `table`: Enable this feature with overrides.
+--- - `function`: Function with signature (namespace, bufnr) that returns any of the above.
+---
+---@param opts table Configuration table with the following keys:
+--- - underline: (default true) Use underline for diagnostics
+--- - virtual_text: (default true) Use virtual text for diagnostics
+--- - signs: (default true) Use signs for diagnostics
+--- - update_in_insert: (default false) Update diagnostics in Insert mode (if false,
+--- diagnostics are updated on InsertLeave)
+--- - severity_sort: (default false) Sort diagnostics by severity. This affects the order in
+--- which signs and virtual text are displayed
+---@param namespace number|nil Update the options for the given namespace. When omitted, update the
+--- global diagnostic options.
+function M.config(opts, namespace)
+ vim.validate {
+ opts = { opts, 't' },
+ namespace = { namespace, 'n', true },
+ }
+
+ local t
+ if namespace then
+ local ns = get_namespace(namespace)
+ t = ns.opts
+ else
+ t = global_diagnostic_options
+ end
+
+ for opt in pairs(global_diagnostic_options) do
+ if opts[opt] ~= nil then
+ t[opt] = opts[opt]
+ end
+ end
+
+ if namespace then
+ for bufnr, v in pairs(diagnostic_cache) do
+ if v[namespace] then
+ M.show(namespace, bufnr)
+ end
+ end
+ else
+ for bufnr, v in pairs(diagnostic_cache) do
+ for ns in pairs(v) do
+ M.show(ns, bufnr)
+ end
+ end
+ end
+end
+
+--- Set diagnostics for the given namespace and buffer.
+---
+---@param namespace number The diagnostic namespace
+---@param bufnr number Buffer number
+---@param diagnostics table A list of diagnostic items |diagnostic-structure|
+---@param opts table|nil Display options to pass to |vim.diagnostic.show()|
+function M.set(namespace, bufnr, diagnostics, opts)
+ vim.validate {
+ namespace = {namespace, 'n'},
+ bufnr = {bufnr, 'n'},
+ diagnostics = {diagnostics, 't'},
+ opts = {opts, 't', true},
+ }
+
+ if vim.tbl_isempty(diagnostics) then
+ return M.reset(namespace, bufnr)
+ end
+
+ if not diagnostic_cleanup[bufnr][namespace] then
+ diagnostic_cleanup[bufnr][namespace] = true
+
+ -- Clean up our data when the buffer unloads.
+ vim.api.nvim_buf_attach(bufnr, false, {
+ on_detach = function(_, b)
+ clear_diagnostic_cache(b, namespace)
+ diagnostic_cleanup[b][namespace] = nil
+ end
+ })
+ end
+
+ set_diagnostic_cache(namespace, diagnostics, bufnr)
+
+ if opts then
+ M.config(opts, namespace)
+ end
+
+ if vim.api.nvim_buf_is_loaded(bufnr) then
+ M.show(namespace, bufnr)
+ end
+
+ vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged")
+end
+
+--- Get current diagnostics.
+---
+---@param bufnr number|nil Buffer number to get diagnistics from. Use 0 for
+--- current buffer or nil for all buffers.
+---@param opts table|nil A table with the following keys:
+--- - namespace: (number) Limit diagnostics to the given namespace.
+--- - lnum: (number) Limit diagnostics to the given line number.
+--- - severity: See |diagnostic-severity|.
+---@return table A list of diagnostic items |diagnostic-structure|.
+function M.get(bufnr, opts)
+ vim.validate {
+ bufnr = { bufnr, 'n', true },
+ opts = { opts, 't', true },
+ }
+
+ opts = opts or {}
+
+ local namespace = opts.namespace
+ local diagnostics = {}
+
+ ---@private
+ local function add(d)
+ if not opts.lnum or d.lnum == opts.lnum then
+ table.insert(diagnostics, d)
+ end
+ end
+
+ if namespace == nil and bufnr == nil then
+ for _, t in pairs(diagnostic_cache) do
+ for _, v in pairs(t) do
+ for _, diagnostic in pairs(v) do
+ add(diagnostic)
+ end
+ end
+ end
+ elseif namespace == nil then
+ for iter_namespace in pairs(diagnostic_cache[bufnr]) do
+ for _, diagnostic in pairs(diagnostic_cache[bufnr][iter_namespace]) do
+ add(diagnostic)
+ end
+ end
+ elseif bufnr == nil then
+ for _, t in pairs(diagnostic_cache) do
+ for _, diagnostic in pairs(t[namespace] or {}) do
+ add(diagnostic)
+ end
+ end
+ else
+ for _, diagnostic in pairs(diagnostic_cache[bufnr][namespace] or {}) do
+ add(diagnostic)
+ end
+ end
+
+ if opts.severity then
+ diagnostics = filter_by_severity(opts.severity, diagnostics)
+ end
+
+ return diagnostics
+end
+
+-- Diagnostic Movements {{{
+
+local next_diagnostic = function(position, search_forward, bufnr, opts, namespace)
+ position[1] = position[1] - 1
+ bufnr = bufnr or vim.api.nvim_get_current_buf()
+ local wrap = vim.F.if_nil(opts.wrap, true)
+ local line_count = vim.api.nvim_buf_line_count(bufnr)
+ opts.namespace = namespace
+ for i = 0, line_count do
+ local offset = i * (search_forward and 1 or -1)
+ local lnum = position[1] + offset
+ if lnum < 0 or lnum >= line_count then
+ if not wrap then
+ return
+ end
+ lnum = (lnum + line_count) % line_count
+ end
+ opts.lnum = lnum
+ local line_diagnostics = M.get(bufnr, opts)
+ if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then
+ local sort_diagnostics, is_next
+ if search_forward then
+ sort_diagnostics = function(a, b) return a.col < b.col end
+ is_next = function(diagnostic) return diagnostic.col > position[2] end
+ else
+ sort_diagnostics = function(a, b) return a.col > b.col end
+ is_next = function(diagnostic) return diagnostic.col < position[2] end
+ end
+ table.sort(line_diagnostics, sort_diagnostics)
+ if i == 0 then
+ for _, v in pairs(line_diagnostics) do
+ if is_next(v) then
+ return v
+ end
+ end
+ else
+ return line_diagnostics[1]
+ end
+ end
+ end
+end
+
+---@private
+local function diagnostic_move_pos(opts, pos)
+ opts = opts or {}
+
+ local enable_popup = vim.F.if_nil(opts.enable_popup, true)
+ local win_id = opts.win_id or vim.api.nvim_get_current_win()
+
+ if not pos then
+ vim.api.nvim_echo({"No more valid diagnostics to move to", "WarningMsg"})
+ 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_position_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.diagnostic.goto_next()|
+---@return table Previous diagnostic
+function M.get_prev(opts)
+ opts = opts or {}
+
+ local win_id = opts.win_id or vim.api.nvim_get_current_win()
+ local bufnr = vim.api.nvim_win_get_buf(win_id)
+ local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
+
+ return next_diagnostic(cursor_position, false, bufnr, opts, opts.namespace)
+end
+
+--- Return the position of the previous diagnostic in the current buffer.
+---
+---@param opts table See |vim.diagnostic.goto_next()|
+---@return table Previous diagnostic position as a (row, col) tuple.
+function M.get_prev_pos(opts)
+ local prev = M.get_prev(opts)
+ if not prev then
+ return false
+ end
+
+ return {prev.lnum, prev.col}
+end
+
+--- Move to the previous diagnostic in the current buffer.
+---@param opts table See |vim.diagnostic.goto_next()|
+function M.goto_prev(opts)
+ return diagnostic_move_pos(
+ opts,
+ M.get_prev_pos(opts)
+ )
+end
+
+--- Get the next diagnostic closest to the cursor position.
+---
+---@param opts table See |vim.diagnostic.goto_next()|
+---@return table Next diagnostic
+function M.get_next(opts)
+ opts = opts or {}
+
+ local win_id = opts.win_id or vim.api.nvim_get_current_win()
+ local bufnr = vim.api.nvim_win_get_buf(win_id)
+ local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
+
+ return next_diagnostic(cursor_position, true, bufnr, opts, opts.namespace)
+end
+
+--- Return the position of the next diagnostic in the current buffer.
+---
+---@param opts table See |vim.diagnostic.goto_next()|
+---@return table Next diagnostic position as a (row, col) tuple.
+function M.get_next_pos(opts)
+ local next = M.get_next(opts)
+ if not next then
+ return false
+ end
+
+ return {next.lnum, next.col}
+end
+
+--- Move to the next diagnostic.
+---
+---@param opts table|nil Configuration table with the following keys:
+--- - namespace: (number) Only consider diagnostics from the given namespace.
+--- - cursor_position: (cursor position) Cursor position as a (row, col) tuple. See
+--- |nvim_win_get_cursor()|. Defaults to the current cursor position.
+--- - wrap: (boolean, default true) Whether to loop around file or not. Similar to 'wrapscan'.
+--- - severity: See |diagnostic-severity|.
+--- - enable_popup: (boolean, default true) Call |vim.diagnostic.show_line_diagnostics()|
+--- on jump.
+--- - popup_opts: (table) Table to pass as {opts} parameter to
+--- |vim.diagnostic.show_line_diagnostics()|
+--- - win_id: (number, default 0) Window ID
+function M.goto_next(opts)
+ return diagnostic_move_pos(
+ opts,
+ M.get_next_pos(opts)
+ )
+end
+
+-- Diagnostic Setters {{{
+
+--- Set signs for given diagnostics.
+---
+---@param namespace number The diagnostic namespace
+---@param bufnr number Buffer number
+---@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the
+--- current diagnostics in the given buffer are used.
+---@param opts table Configuration table with the following keys:
+--- - priority: Set the priority of the signs |sign-priority|.
+---@private
+function M._set_signs(namespace, bufnr, diagnostics, opts)
+ vim.validate {
+ namespace = {namespace, 'n'},
+ bufnr = {bufnr, 'n'},
+ diagnostics = {diagnostics, 't'},
+ opts = {opts, 't', true},
+ }
+
+ bufnr = get_bufnr(bufnr)
+ opts = get_resolved_options({ signs = opts }, namespace, bufnr).signs
+
+ if opts and opts.severity then
+ diagnostics = filter_by_severity(opts.severity, diagnostics)
+ end
+
+ local ns = get_namespace(namespace)
+
+ define_default_signs()
+
+ for _, diagnostic in ipairs(diagnostics) do
+ vim.fn.sign_place(
+ 0,
+ ns.sign_group,
+ sign_highlight_map[diagnostic.severity],
+ bufnr,
+ {
+ priority = opts and opts.priority,
+ lnum = diagnostic.lnum + 1
+ }
+ )
+ end
+end
+
+--- Set underline for given diagnostics.
+---
+---@param namespace number The diagnostic namespace
+---@param bufnr number Buffer number
+---@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the
+--- current diagnostics in the given buffer are used.
+---@param opts table Configuration table. Currently unused.
+---@private
+function M._set_underline(namespace, bufnr, diagnostics, opts)
+ vim.validate {
+ namespace = {namespace, 'n'},
+ bufnr = {bufnr, 'n'},
+ diagnostics = {diagnostics, 't'},
+ opts = {opts, 't', true},
+ }
+
+ bufnr = get_bufnr(bufnr)
+ opts = get_resolved_options({ underline = opts }, namespace, bufnr).underline
+
+ if opts and opts.severity then
+ diagnostics = filter_by_severity(opts.severity, diagnostics)
+ end
+
+ for _, diagnostic in ipairs(diagnostics) do
+ 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.Error
+ end
+
+ vim.highlight.range(
+ bufnr,
+ namespace,
+ higroup,
+ { diagnostic.lnum, diagnostic.col },
+ { diagnostic.end_lnum, diagnostic.end_col }
+ )
+ end
+end
+
+--- Set virtual text for given diagnostics.
+---
+---@param namespace number The diagnostic namespace
+---@param bufnr number Buffer number
+---@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the
+--- current diagnostics in the given buffer are used.
+---@param opts table|nil Configuration table with the following keys:
+--- - prefix: (string) Prefix to display before virtual text on line.
+--- - spacing: (number) Number of spaces to insert before virtual text.
+---@private
+function M._set_virtual_text(namespace, bufnr, diagnostics, opts)
+ vim.validate {
+ namespace = {namespace, 'n'},
+ bufnr = {bufnr, 'n'},
+ diagnostics = {diagnostics, 't'},
+ opts = {opts, 't', true},
+ }
+
+ bufnr = get_bufnr(bufnr)
+ opts = get_resolved_options({ virtual_text = opts }, namespace, bufnr).virtual_text
+
+ local buffer_line_diagnostics = diagnostic_lines(diagnostics)
+ for line, line_diagnostics in pairs(buffer_line_diagnostics) do
+ if opts and opts.severity then
+ line_diagnostics = filter_by_severity(opts.severity, line_diagnostics)
+ end
+ local virt_texts = M.get_virt_text_chunks(line_diagnostics, opts)
+
+ if virt_texts then
+ vim.api.nvim_buf_set_extmark(bufnr, namespace, line, 0, {
+ hl_mode = "combine",
+ virt_text = virt_texts,
+ })
+ end
+ end
+end
+
+--- Get virtual text chunks to display using |nvim_buf_set_extmark()|.
+---
+---@param line_diags table The diagnostics associated with the line.
+---@param opts table|nil Configuration table with the following keys:
+--- - prefix: (string) Prefix to display before virtual text on line.
+--- - spacing: (number) Number of spaces to insert before virtual text.
+---@return an array of [text, hl_group] arrays. This can be passed directly to
+--- the {virt_text} option of |nvim_buf_set_extmark()|.
+function M.get_virt_text_chunks(line_diags, opts)
+ 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
+
+--- Callback scheduled when leaving Insert mode.
+---
+--- This function must be exported publicly so that it is available to be
+--- called from the Vimscript autocommand.
+---
+--- See @ref schedule_display()
+---
+---@private
+function M._execute_scheduled_display(namespace, bufnr)
+ local args = bufs_waiting_to_update[bufnr][namespace]
+ if not args then
+ return
+ end
+
+ -- Clear the args so we don't display unnecessarily.
+ bufs_waiting_to_update[bufnr][namespace] = nil
+
+ M.show(namespace, bufnr, nil, args)
+end
+
+--- Hide currently displayed diagnostics.
+---
+--- This only clears the decorations displayed in the buffer. Diagnostics can
+--- be redisplayed with |vim.diagnostic.show()|. To completely remove
+--- diagnostics, use |vim.diagnostic.reset()|.
+---
+--- To hide diagnostics and prevent them from re-displaying, use
+--- |vim.diagnostic.disable()|.
+---
+---@param namespace number The diagnostic namespace
+---@param bufnr number|nil Buffer number. Defaults to the current buffer.
+function M.hide(namespace, bufnr)
+ vim.validate {
+ namespace = { namespace, 'n' },
+ bufnr = { bufnr, 'n', true },
+ }
+
+ bufnr = get_bufnr(bufnr)
+ diagnostic_cache_extmarks[bufnr][namespace] = {}
+
+ local ns = get_namespace(namespace)
+
+ -- clear sign group
+ vim.fn.sign_unplace(ns.sign_group, {buffer=bufnr})
+
+ -- clear virtual text namespace
+ vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
+end
+
+
+--- Display diagnostics for the given namespace and buffer.
+---
+---@param namespace number Diagnostic namespace
+---@param bufnr number|nil Buffer number. Defaults to the current buffer.
+---@param diagnostics table|nil The diagnostics to display. When omitted, use the
+--- saved diagnostics for the given namespace and
+--- buffer. This can be used to display a list of diagnostics
+--- without saving them or to display only a subset of
+--- diagnostics.
+---@param opts table|nil Display options. See |vim.diagnostic.config()|.
+function M.show(namespace, bufnr, diagnostics, opts)
+ vim.validate {
+ namespace = { namespace, 'n' },
+ bufnr = { bufnr, 'n', true },
+ diagnostics = { diagnostics, 't', true },
+ opts = { opts, 't', true },
+ }
+
+ bufnr = get_bufnr(bufnr)
+ if is_disabled(namespace, bufnr) then
+ return
+ end
+
+ M.hide(namespace, bufnr)
+
+ diagnostics = diagnostics or M.get(bufnr, {namespace=namespace})
+
+ if not diagnostics or vim.tbl_isempty(diagnostics) then
+ return
+ end
+
+ opts = get_resolved_options(opts, namespace, bufnr)
+
+ if opts.update_in_insert then
+ clear_scheduled_display(namespace, bufnr)
+ else
+ local mode = vim.api.nvim_get_mode()
+ if string.sub(mode.mode, 1, 1) == 'i' then
+ schedule_display(namespace, bufnr, opts)
+ return
+ end
+ end
+
+ if opts.underline then
+ M._set_underline(namespace, bufnr, diagnostics, opts.underline)
+ end
+
+ if opts.virtual_text then
+ M._set_virtual_text(namespace, bufnr, diagnostics, opts.virtual_text)
+ end
+
+ if opts.signs then
+ M._set_signs(namespace, bufnr, diagnostics, opts.signs)
+ end
+
+ save_extmarks(namespace, bufnr)
+end
+
+--- Open a floating window with the diagnostics at the given position.
+---
+---@param opts table|nil Configuration table with the same keys as
+--- |vim.lsp.util.open_floatin_preview()| in addition to the following:
+--- - namespace: (number) Limit diagnostics to the given namespace
+--- - severity: See |diagnostic-severity|.
+--- - show_header: (boolean, default true) Show "Diagnostics:" header
+---@param bufnr number|nil Buffer number. Defaults to the current buffer.
+---@param position table|nil The (0,0)-indexed position. Defaults to the current cursor position.
+---@return A ({popup_bufnr}, {win_id}) tuple
+function M.show_position_diagnostics(opts, bufnr, position)
+ vim.validate {
+ opts = { opts, 't', true },
+ bufnr = { bufnr, 'n', true },
+ position = { position, 't', true },
+ }
+
+ opts = opts or {}
+
+ opts.focus_id = "position_diagnostics"
+ bufnr = get_bufnr(bufnr)
+ if not position then
+ local curr_position = vim.api.nvim_win_get_cursor(0)
+ curr_position[1] = curr_position[1] - 1
+ position = curr_position
+ end
+ local match_position_predicate = function(diag)
+ return position[1] == diag.lnum and
+ position[2] >= diag.col and
+ (position[2] <= diag.end_col or position[1] < diag.end_lnum)
+ end
+ local position_diagnostics = vim.tbl_filter(match_position_predicate, M.get(bufnr, opts))
+ table.sort(position_diagnostics, function(a, b) return a.severity < b.severity end)
+ return show_diagnostics(opts, position_diagnostics)
+end
+
+--- Open a floating window with the diagnostics from the given line.
+---
+---@param opts table Configuration table. See |vim.diagnostic.show_position_diagnostics()|.
+---@param bufnr number|nil Buffer number. Defaults to the current buffer.
+---@param lnum number|nil Line number. Defaults to line number of cursor.
+---@return A ({popup_bufnr}, {win_id}) tuple
+function M.show_line_diagnostics(opts, bufnr, lnum)
+ vim.validate {
+ opts = { opts, 't', true },
+ bufnr = { bufnr, 'n', true },
+ lnum = { lnum, 'n', true },
+ }
+
+ opts = opts or {}
+ opts.focus_id = "line_diagnostics"
+ opts.lnum = lnum
+ local line_diagnostics = M.get(bufnr, opts)
+ return show_diagnostics(opts, line_diagnostics)
+end
+
+--- Remove all diagnostics from the given namespace.
+---
+--- Unlike |vim.diagnostic.hide()|, this function removes all saved
+--- diagnostics. They cannot be redisplayed using |vim.diagnostic.show()|. To
+--- simply remove diagnostic decorations in a way that they can be
+--- re-displayed, use |vim.diagnostic.hide()|.
+---
+---@param namespace number
+---@param bufnr number|nil Remove diagnostics for the given buffer. When omitted,
+--- diagnostics are removed for all buffers.
+function M.reset(namespace, bufnr)
+ if bufnr == nil then
+ for iter_bufnr, namespaces in pairs(diagnostic_cache) do
+ if namespaces[namespace] then
+ M.reset(namespace, iter_bufnr)
+ end
+ end
+ else
+ clear_diagnostic_cache(namespace, bufnr)
+ M.hide(namespace, bufnr)
+ end
+
+ vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged")
+end
+
+--- Add all diagnostics to the quickfix list.
+---
+---@param opts table|nil Configuration table with the following keys:
+--- - namespace: (number) Only add diagnostics from the given namespace.
+--- - open: (boolean, default true) Open quickfix list after setting.
+--- - title: (string) Title of quickfix list. Defaults to "Diagnostics".
+--- - severity: See |diagnostic-severity|.
+function M.setqflist(opts)
+ set_list(false, opts)
+end
+
+--- Add buffer diagnostics to the location list.
+---
+---@param opts table|nil Configuration table with the following keys:
+--- - namespace: (number) Only add diagnostics from the given namespace.
+--- - winnr: (number, default 0) Window number to set location list for.
+--- - open: (boolean, default true) Open the location list after setting.
+--- - title: (string) Title of the location list. Defaults to "Diagnostics".
+--- - severity: See |diagnostic-severity|.
+function M.setloclist(opts)
+ set_list(true, opts)
+end
+
+--- Disable diagnostics in the given buffer.
+---
+---@param bufnr number|nil Buffer number. Defaults to the current buffer.
+---@param namespace number|nil Only disable diagnostics for the given namespace.
+function M.disable(bufnr, namespace)
+ vim.validate { bufnr = {bufnr, 'n', true}, namespace = {namespace, 'n', true} }
+ bufnr = get_bufnr(bufnr)
+ if namespace == nil then
+ diagnostic_disabled[bufnr] = true
+ for ns in pairs(diagnostic_cache[bufnr]) do
+ M.hide(ns, bufnr)
+ end
+ else
+ if type(diagnostic_disabled[bufnr]) ~= "table" then
+ diagnostic_disabled[bufnr] = {}
+ end
+ diagnostic_disabled[bufnr][namespace] = true
+ M.hide(namespace, bufnr)
+ end
+end
+
+--- Enable diagnostics in the given buffer.
+---
+---@param bufnr number|nil Buffer number. Defaults to the current buffer.
+---@param namespace number|nil Only enable diagnostics for the given namespace.
+function M.enable(bufnr, namespace)
+ vim.validate { bufnr = {bufnr, 'n', true}, namespace = {namespace, 'n', true} }
+ bufnr = get_bufnr(bufnr)
+ if namespace == nil then
+ diagnostic_disabled[bufnr] = nil
+ for ns in pairs(diagnostic_cache[bufnr]) do
+ M.show(ns, bufnr)
+ end
+ else
+ if type(diagnostic_disabled[bufnr]) ~= "table" then
+ return
+ end
+ diagnostic_disabled[bufnr][namespace] = nil
+ M.show(namespace, bufnr)
+ end
+end
+
+-- }}}
+
+
+return M
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 3276563439..90c5872f11 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -1534,8 +1534,5 @@ function lsp._with_extend(name, options, user_config)
return resulting_config
end
--- Define the LspDiagnostics signs if they're not defined already.
-require('vim.lsp.diagnostic')._define_default_signs_and_highlights()
-
return lsp
-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index ccd325b1ac..01c675ba77 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -1,48 +1,4 @@
-local api = vim.api
-local validate = vim.validate
-
-local highlight = vim.highlight
local log = require('vim.lsp.log')
-local protocol = require('vim.lsp.protocol')
-local util = require('vim.lsp.util')
-
-local if_nil = vim.F.if_nil
-
----@class DiagnosticSeverity
-local DiagnosticSeverity = protocol.DiagnosticSeverity
-
-local to_severity = function(severity)
- if not severity then return nil end
- return type(severity) == 'string' and DiagnosticSeverity[severity] or severity
-end
-
-local filter_to_severity_limit = function(severity, diagnostics)
- local filter_level = to_severity(severity)
- if not filter_level then
- return diagnostics
- end
-
- return vim.tbl_filter(function(t) return t.severity == filter_level end, diagnostics)
-end
-
-local filter_by_severity_limit = function(severity_limit, diagnostics)
- local filter_level = to_severity(severity_limit)
- if not filter_level then
- return diagnostics
- end
-
- return vim.tbl_filter(function(t) return t.severity <= filter_level end, diagnostics)
-end
-
-local to_position = function(position, bufnr)
- vim.validate { position = {position, 't'} }
-
- return {
- position.line,
- util._get_line_byte_from_position(bufnr, position)
- }
-end
-
---@brief lsp-diagnostic
---
@@ -57,70 +13,9 @@ end
local M = {}
--- Diagnostic Highlights {{{
-
--- TODO(tjdevries): Determine how to generate documentation for these
--- and how to configure them to be easy for users.
---
--- For now, just use the following script. It should work pretty good.
---[[
-local levels = {"Error", "Warning", "Information", "Hint" }
-
-local all_info = {
- { "Default", "Used as the base highlight group, other highlight groups link to", },
- { "VirtualText", 'Used for "%s" diagnostic virtual text.\n See |vim.lsp.diagnostic.set_virtual_text()|', },
- { "Underline", 'Used to underline "%s" diagnostics.\n See |vim.lsp.diagnostic.set_underline()|', },
- { "Floating", 'Used to color "%s" diagnostic messages in diagnostics float.\n See |vim.lsp.diagnostic.show_line_diagnostics()|', },
- { "Sign", 'Used for "%s" signs in sing column.\n See |vim.lsp.diagnostic.set_signs()|', },
-}
-
-local results = {}
-for _, info in ipairs(all_info) do
- for _, level in ipairs(levels) do
- local name = info[1]
- local description = info[2]
- local fullname = string.format("Lsp%s%s", name, level)
- table.insert(results, string.format(
- "%78s", string.format("*hl-%s*", fullname))
- )
-
- table.insert(results, fullname)
- table.insert(results, string.format(" %s", description))
- table.insert(results, "")
- end
-end
-
--- print(table.concat(results, '\n'))
-vim.fn.setreg("*", table.concat(results, '\n'))
---]]
-
-local diagnostic_severities = {
- [DiagnosticSeverity.Error] = { guifg = "Red" };
- [DiagnosticSeverity.Warning] = { guifg = "Orange" };
- [DiagnosticSeverity.Information] = { guifg = "LightBlue" };
- [DiagnosticSeverity.Hint] = { guifg = "LightGrey" };
-}
-
--- Make a map from DiagnosticSeverity -> Highlight Name
-local make_highlight_map = function(base_name)
- local result = {}
- for k, _ in pairs(diagnostic_severities) do
- result[k] = "LspDiagnostics" .. base_name .. DiagnosticSeverity[k]
- end
-
- return result
-end
-
-local default_highlight_map = make_highlight_map("Default")
-local virtual_text_highlight_map = make_highlight_map("VirtualText")
-local underline_highlight_map = make_highlight_map("Underline")
-local floating_highlight_map = make_highlight_map("Floating")
-local sign_highlight_map = make_highlight_map("Sign")
-
--- }}}
--- Diagnostic Namespaces {{{
local DEFAULT_CLIENT_ID = -1
-local get_client_id = function(client_id)
+---@private
+local function get_client_id(client_id)
if client_id == nil then
client_id = DEFAULT_CLIENT_ID
end
@@ -128,179 +23,112 @@ local get_client_id = function(client_id)
return client_id
end
-local get_bufnr = function(bufnr)
+---@private
+local function get_bufnr(bufnr)
if not bufnr then
- return api.nvim_get_current_buf()
+ return vim.api.nvim_get_current_buf()
elseif bufnr == 0 then
- return api.nvim_get_current_buf()
+ return vim.api.nvim_get_current_buf()
end
return bufnr
end
-
---- Create a namespace table, used to track a client's buffer local items
-local _make_namespace_table = function(namespace, api_namespace)
- vim.validate { namespace = { namespace, 's' } }
-
- return setmetatable({
- [DEFAULT_CLIENT_ID] = api.nvim_create_namespace(namespace)
- }, {
- __index = function(t, client_id)
- client_id = get_client_id(client_id)
-
- if rawget(t, client_id) == nil then
- local value = string.format("%s:%s", namespace, client_id)
-
- if api_namespace then
- value = api.nvim_create_namespace(value)
- end
-
- rawset(t, client_id, value)
- end
-
- return rawget(t, client_id)
- end
- })
-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]
+local function severity_lsp_to_vim(severity)
+ if type(severity) == 'string' then
+ severity = vim.lsp.protocol.DiagnosticSeverity[severity]
+ end
+ return severity
end
---@private
-function M._get_sign_namespace(client_id)
- return _sign_namespaces[client_id]
-end
--- }}}
--- Diagnostic Buffer & Client metatables {{{
-local bufnr_and_client_cacher_mt = {
- __index = function(t, bufnr)
- if bufnr == 0 or bufnr == nil then
- bufnr = vim.api.nvim_get_current_buf()
- end
-
- 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_extmarks = setmetatable({}, bufnr_and_client_cacher_mt)
-local diagnostic_cache_lines = setmetatable({}, bufnr_and_client_cacher_mt)
-local diagnostic_cache_counts = setmetatable({}, bufnr_and_client_cacher_mt)
-local diagnostic_attached_buffers = {}
-
--- Disabled buffers and clients
-local diagnostic_disabled = setmetatable({}, bufnr_and_client_cacher_mt)
-
-local _bufs_waiting_to_update = setmetatable({}, bufnr_and_client_cacher_mt)
-
---- 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)
+local function severity_vim_to_lsp(severity)
+ if type(severity) == 'string' then
+ severity = vim.diagnostic.severity[severity]
end
- return diagnostics_by_line
+ return severity
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
+---@private
+local function line_byte_from_position(lines, lnum, col, offset_encoding)
+ if offset_encoding == "utf-8" then
+ return col
+ end
- counts[diagnostic.severity] = val + 1
- end
+ local line = lines[lnum + 1]
+ local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == "utf-16")
+ if ok then
+ return result
end
- return counts
+ return col
end
---@private
---- Set the different diagnostic cache after `textDocument/publishDiagnostics`
----@param diagnostics Diagnostic[]
----@param bufnr number
----@param client_id number
----@return nil
-local function set_diagnostic_cache(diagnostics, bufnr, client_id)
- client_id = get_client_id(client_id)
-
- -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
- --
- -- The diagnostic's severity. Can be omitted. If omitted it is up to the
- -- client to interpret diagnostics as error, warning, info or hint.
- -- TODO: Replace this with server-specific heuristics to infer severity.
- local buf_line_count = vim.api.nvim_buf_line_count(bufnr)
- for _, diagnostic in ipairs(diagnostics) do
- if diagnostic.severity == nil then
- diagnostic.severity = DiagnosticSeverity.Error
- end
- -- Account for servers that place diagnostics on terminating newline
- if buf_line_count > 0 then
- diagnostic.range.start.line = math.max(math.min(
- diagnostic.range.start.line, buf_line_count - 1
- ), 0)
- diagnostic.range["end"].line = math.max(math.min(
- diagnostic.range["end"].line, buf_line_count - 1
- ), 0)
- end
+local function get_buf_lines(bufnr)
+ if vim.api.nvim_buf_is_loaded(bufnr) then
+ return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
end
- diagnostic_cache[bufnr][client_id] = diagnostics
- diagnostic_cache_lines[bufnr][client_id] = _diagnostic_lines(diagnostics)
- diagnostic_cache_counts[bufnr][client_id] = _diagnostic_counts(diagnostics)
+ local filename = vim.api.nvim_buf_get_name(bufnr)
+ local f = io.open(filename)
+ local lines = vim.split(f:read("*a"), "\n")
+ f:close()
+ return lines
end
+---@private
+local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
+ local buf_lines = get_buf_lines(bufnr)
+ local client = vim.lsp.get_client_by_id(client_id)
+ local offset_encoding = client and client.offset_encoding or "utf-16"
+ return vim.tbl_map(function(diagnostic)
+ local start = diagnostic.range.start
+ local _end = diagnostic.range["end"]
+ return {
+ lnum = start.line,
+ col = line_byte_from_position(buf_lines, start.line, start.character, offset_encoding),
+ end_lnum = _end.line,
+ end_col = line_byte_from_position(buf_lines, _end.line, _end.character, offset_encoding),
+ severity = severity_lsp_to_vim(diagnostic.severity),
+ message = diagnostic.message
+ }
+ end, 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)
+local function diagnostic_vim_to_lsp(diagnostics)
+ return vim.tbl_map(function(diagnostic)
+ return {
+ range = {
+ start = {
+ line = diagnostic.lnum,
+ character = diagnostic.col,
+ },
+ ["end"] = {
+ line = diagnostic.end_lnum,
+ character = diagnostic.end_col,
+ },
+ },
+ severity = severity_vim_to_lsp(diagnostic.severity),
+ message = diagnostic.message,
+ }
+ end, diagnostics)
+end
- diagnostic_cache[bufnr][client_id] = nil
- diagnostic_cache_lines[bufnr][client_id] = nil
- diagnostic_cache_counts[bufnr][client_id] = nil
+local _client_namespaces = {}
+
+--- Get the diagnostic namespace associated with an LSP client |vim.diagnostic|.
+---
+---@param client_id number The id of the LSP client
+function M.get_namespace(client_id)
+ vim.validate { client_id = { client_id, 'n' } }
+ if not _client_namespaces[client_id] then
+ local name = string.format("vim.lsp.client-%d", client_id)
+ _client_namespaces[client_id] = vim.api.nvim_create_namespace(name)
+ end
+ return _client_namespaces[client_id]
end
--- Save diagnostics to the current buffer.
@@ -309,86 +137,146 @@ end
---@param diagnostics Diagnostic[]
---@param bufnr number
---@param client_id number
+---@private
function M.save(diagnostics, bufnr, client_id)
- validate {
- diagnostics = {diagnostics, 't'},
- bufnr = {bufnr, 'n'},
- client_id = {client_id, 'n', true},
- }
+ local namespace = M.get_namespace(client_id)
+ vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
+end
+-- }}}
+
+--- |lsp-handler| for the method "textDocument/publishDiagnostics"
+---
+--- See |vim.diagnostic.config()| for configuration options. Handler-specific
+--- configuration can be set using |vim.lsp.with()|:
+--- <pre>
+--- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
+--- vim.lsp.diagnostic.on_publish_diagnostics, {
+--- -- Enable underline, use default values
+--- underline = true,
+--- -- Enable virtual text, override spacing to 4
+--- virtual_text = {
+--- spacing = 4,
+--- },
+--- -- Use a function to dynamically turn signs off
+--- -- and on, using buffer local variables
+--- signs = function(bufnr, client_id)
+--- return vim.bo[bufnr].show_signs == false
+--- end,
+--- -- Disable a feature
+--- update_in_insert = false,
+--- }
+--- )
+--- </pre>
+---
+---@param config table Configuration table (see |vim.diagnostic.config()|).
+function M.on_publish_diagnostics(_, result, ctx, config)
+ local client_id = ctx.client_id
+ local uri = result.uri
+ local bufnr = vim.uri_to_bufnr(uri)
- if not diagnostics then return end
+ if not bufnr then
+ return
+ end
- bufnr = get_bufnr(bufnr)
client_id = get_client_id(client_id)
+ local namespace = M.get_namespace(client_id)
+ local diagnostics = result.diagnostics
- if not _diagnostic_cleanup[bufnr][client_id] then
- _diagnostic_cleanup[bufnr][client_id] = true
+ if config then
+ if vim.F.if_nil(config.severity_sort, false) then
+ table.sort(diagnostics, function(a, b) return a.severity > b.severity end)
+ end
- -- Clean up our data when the buffer unloads.
- api.nvim_buf_attach(bufnr, false, {
- on_detach = function(_, b)
- clear_diagnostic_cache(b, client_id)
- _diagnostic_cleanup[b][client_id] = nil
+ for _, opt in pairs(config) do
+ if type(opt) == 'table' then
+ if not opt.severity and opt.severity_limit then
+ opt.severity = {min=severity_lsp_to_vim(opt.severity_limit)}
+ end
end
- })
+ end
+
+ vim.diagnostic.config(config, namespace)
end
- set_diagnostic_cache(diagnostics, bufnr, client_id)
+ vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
+
+ -- Keep old autocmd for back compat. This should eventually be removed.
+ vim.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged")
+end
+
+--- Clear diagnotics and diagnostic cache.
+---
+--- Diagnostic producers should prefer |vim.diagnostic.reset()|. However,
+--- this method signature is still used internally in some parts of the LSP
+--- implementation so it's simply marked @private rather than @deprecated.
+---
+---@param client_id number
+---@param buffer_client_map table map of buffers to active clients
+---@private
+function M.reset(client_id, buffer_client_map)
+ buffer_client_map = vim.deepcopy(buffer_client_map)
+ vim.schedule(function()
+ for bufnr, client_ids in pairs(buffer_client_map) do
+ if client_ids[client_id] then
+ local namespace = M.get_namespace(client_id)
+ vim.diagnostic.reset(namespace, bufnr)
+ end
+ end
+ end)
end
--- }}}
--- Diagnostic Retrieval {{{
+-- Deprecated Functions {{{
--- Get all diagnostics for clients
---
+---@deprecated Prefer |vim.diagnostic.get()|
+---
---@param client_id number Restrict included diagnostics to the client
--- If nil, diagnostics of all clients are included.
---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[])
function M.get_all(client_id)
- local diagnostics_by_bufnr = {}
- for bufnr, buf_diagnostics in pairs(diagnostic_cache) do
- diagnostics_by_bufnr[bufnr] = {}
- for cid, client_diagnostics in pairs(buf_diagnostics) do
- if client_id == nil or cid == client_id then
- vim.list_extend(diagnostics_by_bufnr[bufnr], client_diagnostics)
- end
- end
+ local result = {}
+ local namespace
+ if client_id then
+ namespace = M.get_namespace(client_id)
+ end
+ for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
+ local diagnostics = diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, {namespace = namespace}))
+ result[bufnr] = diagnostics
end
- return diagnostics_by_bufnr
+ return result
end
--- Return associated diagnostics for bufnr
---
+---@deprecated Prefer |vim.diagnostic.get()|
+---
---@param bufnr number
---@param client_id number|nil If nil, then return all of the diagnostics.
--- Else, return just the diagnostics associated with the client_id.
---@param predicate function|nil Optional function for filtering diagnostics
function M.get(bufnr, client_id, predicate)
+ predicate = predicate or function() return true end
if client_id == nil then
local all_diagnostics = {}
- for iter_client_id, _ in pairs(diagnostic_cache[bufnr]) do
- local iter_diagnostics = M.get(bufnr, iter_client_id, predicate)
-
+ vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _)
+ local iter_diagnostics = vim.tbl_filter(predicate, M.get(bufnr, iter_client_id))
for _, diagnostic in ipairs(iter_diagnostics) do
table.insert(all_diagnostics, diagnostic)
end
- end
-
- return all_diagnostics
+ end)
+ return diagnostic_vim_to_lsp(all_diagnostics)
end
- predicate = predicate or function(_) return true end
- local client_diagnostics = {}
- for _, diagnostic in ipairs(diagnostic_cache[bufnr][client_id] or {}) do
- if predicate(diagnostic) then
- table.insert(client_diagnostics, diagnostic)
- end
- end
- return client_diagnostics
+ local namespace = M.get_namespace(client_id)
+ return diagnostic_vim_to_lsp(vim.tbl_filter(predicate, vim.diagnostic.get(bufnr, {namespace=namespace})))
end
--- Get the diagnostics by line
---
+--- Marked private as this is used internally by the LSP subsystem, but
+--- most users should instead prefer |vim.diagnostic.get()|.
+---
---@param bufnr number|nil The buffer number
---@param line_nr number|nil The line number
---@param opts table|nil Configuration keys
@@ -398,216 +286,134 @@ end
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---@param client_id|nil number the client id
---@return table Table with map of line number to list of diagnostics.
--- Structured: { [1] = {...}, [5] = {.... } }
+--- Structured: { [1] = {...}, [5] = {.... } }
+---@private
function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
opts = opts or {}
-
- bufnr = bufnr or vim.api.nvim_get_current_buf()
- line_nr = line_nr or vim.api.nvim_win_get_cursor(0)[1] - 1
-
- local client_get_diags = function(iter_client_id)
- return (diagnostic_cache_lines[bufnr][iter_client_id] or {})[line_nr] or {}
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
- local line_diagnostics
- if client_id == nil then
- line_diagnostics = {}
- for iter_client_id, _ in pairs(diagnostic_cache_lines[bufnr]) do
- for _, diagnostic in ipairs(client_get_diags(iter_client_id)) do
- table.insert(line_diagnostics, diagnostic)
- end
- end
- else
- line_diagnostics = vim.deepcopy(client_get_diags(client_id))
+ if client_id then
+ opts.namespace = M.get_namespace(client_id)
end
- if opts.severity then
- line_diagnostics = filter_to_severity_limit(opts.severity, line_diagnostics)
- elseif opts.severity_limit then
- line_diagnostics = filter_by_severity_limit(opts.severity_limit, line_diagnostics)
+ if not line_nr then
+ line_nr = vim.api.nvim_win_get_cursor(0)[1] - 1
end
- table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end)
+ opts.lnum = line_nr
- return line_diagnostics
+ return diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, opts))
end
--- Get the counts for a particular severity
---
---- Useful for showing diagnostic counts in statusline. eg:
----
---- <pre>
---- function! LspStatus() abort
---- let sl = ''
---- if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))')
---- let sl.='%#MyStatuslineLSP#E:'
---- let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.diagnostic.get_count(0, [[Error]])")}'
---- let sl.='%#MyStatuslineLSP# W:'
---- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.diagnostic.get_count(0, [[Warning]])")}'
---- else
---- let sl.='%#MyStatuslineLSPErrors#off'
---- endif
---- return sl
---- endfunction
---- autocmd BufWinEnter * let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
---- </pre>
+---@deprecated Prefer |vim.diagnostic.get_count()|
---
---@param bufnr number The buffer number
---@param severity DiagnosticSeverity
---@param client_id number the client id
function M.get_count(bufnr, severity, client_id)
- if client_id == nil then
- local total = 0
- for iter_client_id, _ in pairs(diagnostic_cache_counts[bufnr]) do
- total = total + M.get_count(bufnr, severity, iter_client_id)
- end
-
- return total
- end
-
- return (diagnostic_cache_counts[bufnr][client_id] or {})[DiagnosticSeverity[severity]] or 0
-end
-
-
--- }}}
--- Diagnostic Movements {{{
-
---- Helper function to find the next diagnostic relative to a position
----@return table the next diagnostic if found
-local _next_diagnostic = function(position, search_forward, bufnr, opts, client_id)
- position[1] = position[1] - 1
- bufnr = bufnr or vim.api.nvim_get_current_buf()
- local wrap = if_nil(opts.wrap, true)
- local line_count = vim.api.nvim_buf_line_count(bufnr)
- for i = 0, line_count do
- local offset = i * (search_forward and 1 or -1)
- local line_nr = position[1] + offset
- if line_nr < 0 or line_nr >= line_count then
- if not wrap then
- return
- end
- line_nr = (line_nr + line_count) % line_count
- end
- local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
- if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then
- local sort_diagnostics, is_next
- if search_forward then
- sort_diagnostics = function(a, b) return a.range.start.character < b.range.start.character end
- is_next = function(diagnostic) return diagnostic.range.start.character > position[2] end
- else
- sort_diagnostics = function(a, b) return a.range.start.character > b.range.start.character end
- is_next = function(diagnostic) return diagnostic.range.start.character < position[2] end
- end
- table.sort(line_diagnostics, sort_diagnostics)
- if i == 0 then
- for _, v in pairs(line_diagnostics) do
- if is_next(v) then
- return v
- end
- end
- else
- return line_diagnostics[1]
- end
- end
- end
-end
-
----@private
---- Helper function to return a position from a diagnostic
----
----@return table {row, col}
-local function _diagnostic_pos(opts, diagnostic)
- opts = opts or {}
-
- local win_id = opts.win_id or vim.api.nvim_get_current_win()
- local bufnr = vim.api.nvim_win_get_buf(win_id)
-
- if not diagnostic then return false end
-
- return to_position(diagnostic.range.start, bufnr)
-end
-
----@private
--- Move to the diagnostic position
-local function _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
+ severity = severity_lsp_to_vim(severity)
+ local opts = { severity = severity }
+ if client_id ~= nil then
+ opts.namespace = M.get_namespace(client_id)
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_position_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id))
- end)
- end
+ return #vim.diagnostic.get(bufnr, opts)
end
--- Get the previous diagnostic closest to the cursor_position
---
+---@deprecated Prefer |vim.diagnostic.get_prev()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic
function M.get_prev(opts)
- opts = opts or {}
-
- local win_id = opts.win_id or vim.api.nvim_get_current_win()
- local bufnr = vim.api.nvim_win_get_buf(win_id)
- local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
-
- return _next_diagnostic(cursor_position, false, bufnr, opts, opts.client_id)
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return diagnostic_vim_to_lsp({vim.diagnostic.get_prev(opts)})[1]
end
--- Return the pos, {row, col}, for the prev diagnostic in the current buffer.
+---
+---@deprecated Prefer |vim.diagnostic.get_prev_pos()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic position
function M.get_prev_pos(opts)
- return _diagnostic_pos(
- opts,
- M.get_prev(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.get_prev_pos(opts)
end
--- Move to the previous diagnostic
+---
+---@deprecated Prefer |vim.diagnostic.goto_prev()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
function M.goto_prev(opts)
- return _diagnostic_move_pos(
- "DiagnosticPrevious",
- opts,
- M.get_prev_pos(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.goto_prev(opts)
end
--- Get the next diagnostic closest to the cursor_position
+---
+---@deprecated Prefer |vim.diagnostic.get_next()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic
function M.get_next(opts)
- opts = opts or {}
-
- local win_id = opts.win_id or vim.api.nvim_get_current_win()
- local bufnr = vim.api.nvim_win_get_buf(win_id)
- local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
-
- return _next_diagnostic(cursor_position, true, bufnr, opts, opts.client_id)
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return diagnostic_vim_to_lsp({vim.diagnostic.get_next(opts)})[1]
end
--- Return the pos, {row, col}, for the next diagnostic in the current buffer.
+---
+---@deprecated Prefer |vim.diagnostic.get_next_pos()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic position
function M.get_next_pos(opts)
- return _diagnostic_pos(
- opts,
- M.get_next(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.get_next_pos(opts)
end
--- Move to the next diagnostic
+---
+---@deprecated Prefer |vim.diagnostic.goto_next()|
+---
---@param opts table|nil Configuration table. Keys:
--- - {client_id}: (number)
--- - If nil, will consider all clients attached to buffer.
@@ -626,17 +432,20 @@ end
--- - {win_id}: (number, default 0)
--- - Window ID
function M.goto_next(opts)
- return _diagnostic_move_pos(
- "DiagnosticNext",
- opts,
- M.get_next_pos(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.goto_next(opts)
end
--- }}}
--- Diagnostic Setters {{{
--- Set signs for given diagnostics
---
+---@deprecated Prefer |vim.diagnostic._set_signs()|
+---
--- Sign characters can be customized with the following commands:
---
--- <pre>
@@ -653,36 +462,13 @@ end
--- - priority: Set the priority of the signs.
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
-function M.set_signs(diagnostics, bufnr, client_id, sign_ns, opts)
- opts = opts or {}
- sign_ns = sign_ns or M._get_sign_namespace(client_id)
-
- if not diagnostics then
- diagnostics = diagnostic_cache[bufnr][client_id]
- end
-
- if not diagnostics then
- return
- end
-
- bufnr = get_bufnr(bufnr)
- diagnostics = filter_by_severity_limit(opts.severity_limit, diagnostics)
-
- local ok = true
- for _, diagnostic in ipairs(diagnostics) do
-
- ok = ok and pcall(vim.fn.sign_place,
- 0,
- sign_ns,
- sign_highlight_map[diagnostic.severity],
- bufnr,
- {
- priority = opts.priority,
- lnum = diagnostic.range.start.line + 1
- }
- )
+function M.set_signs(diagnostics, bufnr, client_id, _, opts)
+ local namespace = M.get_namespace(client_id)
+ if opts and not opts.severity and opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
+ local ok = vim.diagnostic._set_signs(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
if not ok then
log.debug("Failed to place signs:", diagnostics)
end
@@ -690,6 +476,8 @@ end
--- Set underline for given diagnostics
---
+---@deprecated Prefer |vim.diagnostic._set_underline()|
+---
--- Underline highlights can be customized by changing the following |:highlight| groups.
---
--- <pre>
@@ -706,35 +494,18 @@ end
---@param opts table: Configuration table:
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
-function M.set_underline(diagnostics, bufnr, client_id, diagnostic_ns, opts)
- opts = opts or {}
-
- diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
- diagnostics = filter_by_severity_limit(opts.severity_limit, diagnostics)
-
- for _, diagnostic in ipairs(diagnostics) do
- local start = diagnostic.range["start"]
- local finish = diagnostic.range["end"]
- local higroup = underline_highlight_map[diagnostic.severity]
-
- if higroup == nil then
- -- Default to error if we don't have a highlight associated
- higroup = underline_highlight_map[DiagnosticSeverity.Error]
- end
-
- highlight.range(
- bufnr,
- diagnostic_ns,
- higroup,
- to_position(start, bufnr),
- to_position(finish, bufnr)
- )
+function M.set_underline(diagnostics, bufnr, client_id, _, opts)
+ local namespace = M.get_namespace(client_id)
+ if opts and not opts.severity and opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
+ return vim.diagnostic._set_underline(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
end
--- Virtual Text {{{
--- Set virtual text given diagnostics
---
+---@deprecated Prefer |vim.diagnostic._set_virtual_text()|
+---
--- Virtual text highlights can be customized by changing the following |:highlight| groups.
---
--- <pre>
@@ -753,436 +524,75 @@ end
--- - spacing (number): Number of spaces to insert before virtual text
--- - severity_limit (DiagnosticSeverity):
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
-function M.set_virtual_text(diagnostics, bufnr, client_id, diagnostic_ns, opts)
- opts = opts or {}
-
- client_id = get_client_id(client_id)
- diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
-
- local buffer_line_diagnostics
- if diagnostics then
- buffer_line_diagnostics = _diagnostic_lines(diagnostics)
- else
- buffer_line_diagnostics = diagnostic_cache_lines[bufnr][client_id]
- end
-
- if not buffer_line_diagnostics then
- return nil
- end
-
- for line, line_diagnostics in pairs(buffer_line_diagnostics) do
- line_diagnostics = filter_by_severity_limit(opts.severity_limit, line_diagnostics)
- local virt_texts = M.get_virtual_text_chunks_for_line(bufnr, line, line_diagnostics, opts)
-
- if virt_texts then
- api.nvim_buf_set_extmark(bufnr, diagnostic_ns, line, 0, {
- virt_text = virt_texts,
- })
- end
+function M.set_virtual_text(diagnostics, bufnr, client_id, _, opts)
+ local namespace = M.get_namespace(client_id)
+ if opts and not opts.severity and opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
+ return vim.diagnostic._set_virtual_text(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
end
--- Default function to get text chunks to display using |nvim_buf_set_extmark()|.
+---
+---@deprecated Prefer |vim.diagnostic.get_virt_text_chunks()|
+---
---@param bufnr number The buffer to display the virtual text in
---@param line number The line number to display the virtual text on
---@param line_diags Diagnostic[] The diagnostics associated with the line
---@param opts table See {opts} from |vim.lsp.diagnostic.set_virtual_text()|
---@return an array of [text, hl_group] arrays. This can be passed directly to
--- the {virt_text} option of |nvim_buf_set_extmark()|.
-function M.get_virtual_text_chunks_for_line(bufnr, line, line_diags, opts)
- assert(bufnr or line)
-
- 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)
- bufnr = get_bufnr(bufnr)
- if client_id == nil then
- return vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _)
- return M.clear(bufnr, iter_client_id)
- end)
- end
-
- diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
- sign_ns = sign_ns or M._get_sign_namespace(client_id)
- diagnostic_cache_extmarks[bufnr][client_id] = {}
-
- assert(bufnr, "bufnr is required")
- assert(diagnostic_ns, "Need diagnostic_ns, got nil")
- 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)
+function M.get_virtual_text_chunks_for_line(bufnr, _, line_diags, opts)
+ return vim.diagnostic.get_virt_text_chunks(diagnostic_lsp_to_vim(line_diags, bufnr), opts)
end
---- 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.
+--- Open a floating window with the diagnostics from {position}
---
---- 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
+---@deprecated Prefer |vim.diagnostic.show_position_diagnostics()|
---
---- For parameter description, see |M.display()|
-function M._clear_scheduled_display(bufnr, client_id)
- local key = make_augroup_key(bufnr, client_id)
-
- if registered[key] then
- vim.cmd(string.format("augroup %s", key))
- vim.cmd(" au!")
- vim.cmd("augroup END")
-
- registered[key] = nil
- end
-end
--- }}}
-
--- Diagnostic Private Highlight Utilies {{{
---- Get the severity highlight name
----@private
-function M._get_severity_highlight_name(severity)
- return virtual_text_highlight_map[severity]
-end
-
---- Get floating severity highlight name
----@private
-function M._get_floating_severity_highlight_name(severity)
- return floating_highlight_map[severity]
-end
-
---- This should be called to update the highlights for the LSP client.
-function M._define_default_signs_and_highlights()
- ---@private
- local function define_default_sign(name, properties)
- if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then
- vim.fn.sign_define(name, properties)
+---@param opts table|nil Configuration keys
+--- - severity: (DiagnosticSeverity, default nil)
+--- - Only return diagnostics with this severity. Overrides severity_limit
+--- - severity_limit: (DiagnosticSeverity, default nil)
+--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
+--- - all opts for |show_diagnostics()| can be used here
+---@param buf_nr number|nil The buffer number
+---@param position table|nil The (0,0)-indexed position
+---@return table {popup_bufnr, win_id}
+function M.show_position_diagnostics(opts, buf_nr, position)
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
end
-
- -- Initialize default diagnostic highlights
- for severity, hi_info in pairs(diagnostic_severities) do
- local default_highlight_name = default_highlight_map[severity]
- highlight.create(default_highlight_name, hi_info, true)
-
- -- Default link all corresponding highlights to the default highlight
- highlight.link(virtual_text_highlight_map[severity], default_highlight_name, false)
- highlight.link(floating_highlight_map[severity], default_highlight_name, false)
- highlight.link(sign_highlight_map[severity], default_highlight_name, false)
- end
-
- -- Create all signs
- for severity, sign_hl_name in pairs(sign_highlight_map) do
- local severity_name = DiagnosticSeverity[severity]
-
- define_default_sign(sign_hl_name, {
- text = (severity_name or 'U'):sub(1, 1),
- texthl = sign_hl_name,
- linehl = '',
- numhl = '',
- })
- end
-
- -- Initialize Underline highlights
- for severity, underline_highlight_name in pairs(underline_highlight_map) do
- highlight.create(underline_highlight_name, {
- cterm = 'underline',
- gui = 'underline',
- guisp = diagnostic_severities[severity].guifg
- }, true)
- end
+ return vim.diagnostic.show_position_diagnostics(opts, buf_nr, position)
end
--- }}}
--- Diagnostic Display {{{
---- |lsp-handler| for the method "textDocument/publishDiagnostics"
+--- Open a floating window with the diagnostics from {line_nr}
---
----@note Each of the configuration options accepts:
---- - `false`: Disable this feature
---- - `true`: Enable this feature, use default settings.
---- - `table`: Enable this feature, use overrides.
---- - `function`: Function with signature (bufnr, client_id) that returns any of the above.
---- <pre>
---- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
---- vim.lsp.diagnostic.on_publish_diagnostics, {
---- -- Enable underline, use default values
---- underline = true,
---- -- Enable virtual text, override spacing to 4
---- virtual_text = {
---- spacing = 4,
---- },
---- -- Use a function to dynamically turn signs off
---- -- and on, using buffer local variables
---- signs = function(bufnr, client_id)
---- return vim.bo[bufnr].show_signs == false
---- end,
---- -- Disable a feature
---- update_in_insert = false,
---- }
---- )
---- </pre>
+---@deprecated Prefer |vim.diagnostic.show_line_diagnostics()|
---
----@param config table Configuration table.
---- - underline: (default=true)
---- - Apply underlines to diagnostics.
---- - See |vim.lsp.diagnostic.set_underline()|
---- - virtual_text: (default=true)
---- - Apply virtual text to line endings.
---- - See |vim.lsp.diagnostic.set_virtual_text()|
---- - signs: (default=true)
---- - Apply signs for diagnostics.
---- - See |vim.lsp.diagnostic.set_signs()|
---- - update_in_insert: (default=false)
---- - Update diagnostics in InsertMode or wait until InsertLeave
---- - severity_sort: (default=false)
---- - Sort diagnostics (and thus signs and virtual text)
-function M.on_publish_diagnostics(_, result, ctx, config)
- local client_id = ctx.client_id
- local uri = result.uri
- local bufnr = vim.uri_to_bufnr(uri)
-
- if not bufnr then
- return
- end
-
- local diagnostics = result.diagnostics
-
- if config and if_nil(config.severity_sort, false) then
- table.sort(diagnostics, function(a, b) return a.severity > b.severity end)
- end
-
- -- Always save the diagnostics, even if the buf is not loaded.
- -- Language servers may report compile or build errors via diagnostics
- -- Users should be able to find these, even if they're in files which
- -- are not loaded.
- M.save(diagnostics, bufnr, client_id)
-
- -- Unloaded buffers should not handle diagnostics.
- -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen.
- -- This should trigger another publish of the diagnostics.
- --
- -- In particular, this stops a ton of spam when first starting a server for current
- -- unloaded buffers.
- if not api.nvim_buf_is_loaded(bufnr) then
- return
- end
-
- M.display(diagnostics, bufnr, client_id, config)
-end
-
--- restores the extmarks set by M.display
----@param last number last line that was changed
----@private
-local function restore_extmarks(bufnr, last)
- for client_id, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do
- local ns = M._get_diagnostic_namespace(client_id)
- local extmarks_current = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true})
- local found = {}
- for _, extmark in ipairs(extmarks_current) do
- -- nvim_buf_set_lines will move any extmark to the line after the last
- -- nvim_buf_set_text will move any extmark to the last line
- if extmark[2] ~= last + 1 then
- found[extmark[1]] = true
- end
- end
- for _, extmark in ipairs(extmarks) do
- if not found[extmark[1]] then
- local opts = extmark[4]
- opts.id = extmark[1]
- -- HACK: end_row should be end_line
- if opts.end_row then
- opts.end_line = opts.end_row
- opts.end_row = nil
- end
- pcall(api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
- end
- end
- end
-end
-
--- caches the extmarks set by M.display
----@private
-local function save_extmarks(bufnr, client_id)
- bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
- if not diagnostic_attached_buffers[bufnr] then
- api.nvim_buf_attach(bufnr, false, {
- on_lines = function(_, _, _, _, _, last)
- restore_extmarks(bufnr, last - 1)
- end,
- on_detach = function()
- diagnostic_cache_extmarks[bufnr] = nil
- end})
- diagnostic_attached_buffers[bufnr] = true
- end
- local ns = M._get_diagnostic_namespace(client_id)
- diagnostic_cache_extmarks[bufnr][client_id] = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true})
-end
-
----@private
---- Display diagnostics for the buffer, given a configuration.
-function M.display(diagnostics, bufnr, client_id, config)
- if diagnostic_disabled[bufnr][client_id] then
- return
- end
-
- config = vim.lsp._with_extend('vim.lsp.diagnostic.on_publish_diagnostics', {
- signs = true,
- underline = true,
- virtual_text = true,
- update_in_insert = false,
- severity_sort = false,
- }, config)
-
- -- TODO(tjdevries): Consider how we can make this a "standardized" kind of thing for |lsp-handlers|.
- -- It seems like we would probably want to do this more often as we expose more of them.
- -- It provides a very nice functional interface for people to override configuration.
- local resolve_optional_value = function(option)
- local enabled_val = {}
-
- if not option then
- return false
- elseif option == true then
- return enabled_val
- elseif type(option) == 'function' then
- local val = option(bufnr, client_id)
- if val == true then
- return enabled_val
- else
- return val
- end
- elseif type(option) == 'table' then
- return option
- else
- error("Unexpected option type: " .. vim.inspect(option))
- end
- end
-
- if resolve_optional_value(config.update_in_insert) then
- M._clear_scheduled_display(bufnr, client_id)
- else
- local mode = vim.api.nvim_get_mode()
-
- if string.sub(mode.mode, 1, 1) == 'i' then
- M._schedule_display(bufnr, client_id, config)
- return
- end
- end
-
- M.clear(bufnr, client_id)
-
- diagnostics = diagnostics or M.get(bufnr, client_id)
-
- vim.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged")
-
- if not diagnostics or vim.tbl_isempty(diagnostics) then
- return
- end
-
- local underline_opts = resolve_optional_value(config.underline)
- if underline_opts then
- M.set_underline(diagnostics, bufnr, client_id, nil, underline_opts)
- end
-
- local virtual_text_opts = resolve_optional_value(config.virtual_text)
- if virtual_text_opts then
- M.set_virtual_text(diagnostics, bufnr, client_id, nil, virtual_text_opts)
- end
-
- local signs_opts = resolve_optional_value(config.signs)
- if signs_opts then
- M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts)
+---@param opts table Configuration table
+--- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
+--- |show_diagnostics()| can be used here
+---@param buf_nr number|nil The buffer number
+---@param line_nr number|nil The line number
+---@param client_id number|nil the client id
+---@return table {popup_bufnr, win_id}
+function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id)
+ if client_id then
+ opts = opts or {}
+ opts.namespace = M.get_namespace(client_id)
end
-
- -- cache extmarks
- save_extmarks(bufnr, client_id)
+ return vim.diagnostic.show_line_diagnostics(opts, buf_nr, line_nr)
end
--- Redraw diagnostics for the given buffer and client
---
+---@deprecated Prefer |vim.diagnostic.redraw()|
+---
--- This calls the "textDocument/publishDiagnostics" handler manually using
--- the cached diagnostics already received from the server. This can be useful
--- for redrawing diagnostics after making changes in diagnostics
@@ -1200,183 +610,14 @@ function M.redraw(bufnr, client_id)
end)
end
- -- We need to invoke the publishDiagnostics handler directly instead of just
- -- calling M.display so that we can preserve any custom configuration options
- -- the user may have set with vim.lsp.with.
- vim.lsp.handlers["textDocument/publishDiagnostics"](
- nil,
- {
- uri = vim.uri_from_bufnr(bufnr),
- diagnostics = M.get(bufnr, client_id),
- },
- {
- method = "textDocument/publishDiagnostics",
- client_id = client_id,
- bufnr = bufnr,
- }
- )
- end
-
-
----@private
---- Open a floating window with the provided diagnostics
----
---- The floating window can be customized with the following highlight groups:
---- <pre>
---- LspDiagnosticsFloatingError
---- LspDiagnosticsFloatingWarning
---- LspDiagnosticsFloatingInformation
---- LspDiagnosticsFloatingHint
---- </pre>
----@param opts table Configuration table
---- - show_header (boolean, default true): Show "Diagnostics:" header
---- - all opts for |vim.lsp.util.open_floating_preview()| can be used here
----@param diagnostics table: The diagnostics to display
----@return table {popup_bufnr, win_id}
-local function show_diagnostics(opts, diagnostics)
- if vim.tbl_isempty(diagnostics) then return end
- local lines = {}
- local highlights = {}
- local show_header = if_nil(opts.show_header, true)
- if show_header then
- table.insert(lines, "Diagnostics:")
- table.insert(highlights, {0, "Bold"})
- end
-
- for i, diagnostic in ipairs(diagnostics) do
- local prefix = string.format("%d. ", i)
- local hiname = M._get_floating_severity_highlight_name(diagnostic.severity)
- assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity))
-
- local message_lines = vim.split(diagnostic.message, '\n', true)
- table.insert(lines, prefix..message_lines[1])
- table.insert(highlights, {#prefix, hiname})
- for j = 2, #message_lines do
- table.insert(lines, string.rep(' ', #prefix) .. message_lines[j])
- table.insert(highlights, {0, hiname})
- end
- end
-
- local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts)
- for i, hi in ipairs(highlights) do
- local prefixlen, hiname = unpack(hi)
- -- Start highlight after the prefix
- api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1)
- end
-
- return popup_bufnr, winnr
-end
-
-
--- }}}
--- Diagnostic User Functions {{{
-
---- Open a floating window with the diagnostics from {position}
----@param opts table|nil Configuration keys
---- - severity: (DiagnosticSeverity, default nil)
---- - Only return diagnostics with this severity. Overrides severity_limit
---- - severity_limit: (DiagnosticSeverity, default nil)
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---- - all opts for |show_diagnostics()| can be used here
----@param buf_nr number|nil The buffer number
----@param position table|nil The (0,0)-indexed position
----@return table {popup_bufnr, win_id}
-function M.show_position_diagnostics(opts, buf_nr, position)
- opts = opts or {}
- opts.focus_id = "position_diagnostics"
- buf_nr = buf_nr or vim.api.nvim_get_current_buf()
- if not position then
- local curr_position = vim.api.nvim_win_get_cursor(0)
- curr_position[1] = curr_position[1] - 1
- position = curr_position
- end
- local match_position_predicate = function(diag)
- return position[1] == diag.range['start'].line and
- position[2] >= diag.range['start'].character and
- (position[2] <= diag.range['end'].character or position[1] < diag.range['end'].line)
- end
- local position_diagnostics = M.get(buf_nr, nil, match_position_predicate)
- if opts.severity then
- position_diagnostics = filter_to_severity_limit(opts.severity, position_diagnostics)
- elseif opts.severity_limit then
- position_diagnostics = filter_by_severity_limit(opts.severity_limit, position_diagnostics)
- end
- table.sort(position_diagnostics, function(a, b) return a.severity < b.severity end)
- return show_diagnostics(opts, position_diagnostics)
-end
-
---- Open a floating window with the diagnostics from {line_nr}
-
----@param opts table Configuration table
---- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
---- |show_diagnostics()| can be used here
----@param buf_nr number|nil The buffer number
----@param line_nr number|nil The line number
----@param client_id number|nil the client id
----@return table {popup_bufnr, win_id}
-function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id)
- opts = opts or {}
- opts.focus_id = "line_diagnostics"
- line_nr = line_nr or (vim.api.nvim_win_get_cursor(0)[1] - 1)
- local line_diagnostics = M.get_line_diagnostics(buf_nr, line_nr, opts, client_id)
- return show_diagnostics(opts, line_diagnostics)
-end
-
---- Clear diagnotics and diagnostic cache
----
---- Handles saving diagnostics from multiple clients in the same buffer.
----@param client_id number
----@param buffer_client_map table map of buffers to active clients
-function M.reset(client_id, buffer_client_map)
- buffer_client_map = vim.deepcopy(buffer_client_map)
- vim.schedule(function()
- for bufnr, client_ids in pairs(buffer_client_map) do
- if client_ids[client_id] then
- clear_diagnostic_cache(bufnr, client_id)
- M.clear(bufnr, client_id)
- end
- end
- end)
-end
-
----@private
---- Gets diagnostics, converts them to quickfix/location list items, and applies the item_handler callback to the items.
----@param item_handler function Callback to apply to the diagnostic items
----@param command string|nil Command to execute after applying the item_handler
----@param opts table|nil Configuration table. Keys:
---- - {client_id}: (number)
---- - If nil, will consider all clients attached to buffer.
---- - {severity}: (DiagnosticSeverity)
---- - Exclusive severity to consider. Overrides {severity_limit}
---- - {severity_limit}: (DiagnosticSeverity)
---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---- - {workspace}: (boolean, default false)
---- - Set the list with workspace diagnostics
-local function apply_to_diagnostic_items(item_handler, command, opts)
- opts = opts or {}
- local current_bufnr = api.nvim_get_current_buf()
- local diags = opts.workspace and M.get_all(opts.client_id) or {
- [current_bufnr] = M.get(current_bufnr, opts.client_id)
- }
- local predicate = function(d)
- local severity = to_severity(opts.severity)
- if severity then
- return d.severity == severity
- end
- local severity_limit = to_severity(opts.severity_limit)
- if severity_limit then
- return d.severity <= severity_limit
- end
- return true
- end
- local items = util.diagnostics_to_items(diags, predicate)
- item_handler(items)
- if command then
- vim.cmd(command)
- end
+ local namespace = M.get_namespace(client_id)
+ return vim.diagnostic.show(namespace, bufnr)
end
--- Sets the quickfix list
+---
+---@deprecated Prefer |vim.diagnostic.setqflist()|
+---
---@param opts table|nil Configuration table. Keys:
--- - {open}: (boolean, default true)
--- - Open quickfix list after set
@@ -1390,13 +631,24 @@ end
--- - Set the list with workspace diagnostics
function M.set_qflist(opts)
opts = opts or {}
- opts.workspace = if_nil(opts.workspace, true)
- local open_qflist = if_nil(opts.open, true)
- local command = open_qflist and [[copen]] or nil
- apply_to_diagnostic_items(util.set_qflist, command, opts)
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ if opts.client_id then
+ opts.client_id = nil
+ opts.namespace = M.get_namespace(opts.client_id)
+ end
+ local workspace = vim.F.if_nil(opts.workspace, true)
+ opts.bufnr = not workspace and 0
+ return vim.diagnostic.setqflist(opts)
end
--- Sets the location list
+---
+---@deprecated Prefer |vim.diagnostic.setloclist()|
+---
---@param opts table|nil Configuration table. Keys:
--- - {open}: (boolean, default true)
--- - Open loclist after set
@@ -1410,12 +662,24 @@ end
--- - Set the list with workspace diagnostics
function M.set_loclist(opts)
opts = opts or {}
- local open_loclist = if_nil(opts.open, true)
- local command = open_loclist and [[lopen]] or nil
- apply_to_diagnostic_items(util.set_loclist, command, opts)
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ if opts.client_id then
+ opts.client_id = nil
+ opts.namespace = M.get_namespace(opts.client_id)
+ end
+ local workspace = vim.F.if_nil(opts.workspace, false)
+ opts.bufnr = not workspace and 0
+ return vim.diagnostic.setloclist(opts)
end
--- Disable diagnostics for the given buffer and client
+---
+---@deprecated Prefer |vim.diagnostic.disable()|
+---
---@param bufnr (optional, number): Buffer handle, defaults to current
---@param client_id (optional, number): Disable diagnostics for the given
--- client. The default is to disable diagnostics for all attached
@@ -1430,11 +694,15 @@ function M.disable(bufnr, client_id)
end)
end
- diagnostic_disabled[bufnr][client_id] = true
- M.clear(bufnr, client_id)
+ bufnr = get_bufnr(bufnr)
+ local namespace = M.get_namespace(client_id)
+ return vim.diagnostic.disable(bufnr, namespace)
end
--- Enable diagnostics for the given buffer and client
+---
+---@deprecated Prefer |vim.diagnostic.enable()|
+---
---@param bufnr (optional, number): Buffer handle, defaults to current
---@param client_id (optional, number): Enable diagnostics for the given
--- client. The default is to enable diagnostics for all attached
@@ -1446,14 +714,13 @@ function M.enable(bufnr, client_id)
end)
end
- if not diagnostic_disabled[bufnr][client_id] then
- return
- end
-
- diagnostic_disabled[bufnr][client_id] = nil
-
- M.redraw(bufnr, client_id)
+ bufnr = get_bufnr(bufnr)
+ local namespace = M.get_namespace(client_id)
+ return vim.diagnostic.enable(bufnr, namespace)
end
+
-- }}}
return M
+
+-- vim: fdm=marker
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index dd766496f4..918666ab27 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -210,10 +210,16 @@ local function response_to_list(map_result, entity)
else
config = config or {}
if config.loclist then
- util.set_loclist(map_result(result, ctx.bufnr))
+ vim.fn.setloclist(0, {}, ' ', {
+ title = 'Language Server';
+ items = map_result(result, ctx.bufnr);
+ })
api.nvim_command("lopen")
else
- util.set_qflist(map_result(result, ctx.bufnr))
+ vim.fn.setqflist({}, ' ', {
+ title = 'Language Server';
+ items = map_result(result, ctx.bufnr);
+ })
api.nvim_command("copen")
end
end
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 5a22a311e0..9a3ce185a0 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -31,16 +31,6 @@ local default_border = {
{" ", "NormalFloat"},
}
-
-local DiagnosticSeverity = protocol.DiagnosticSeverity
-local loclist_type_map = {
- [DiagnosticSeverity.Error] = 'E',
- [DiagnosticSeverity.Warning] = 'W',
- [DiagnosticSeverity.Information] = 'I',
- [DiagnosticSeverity.Hint] = 'I',
-}
-
-
---@private
--- Check the border given by opts or the default border for the additional
--- size it adds to a float.
@@ -1543,6 +1533,9 @@ end
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
+--- The result can be passed to the {list} argument of |setqflist()| or
+--- |setloclist()|.
+---
---@param locations (table) list of `Location`s or `LocationLink`s
---@returns (table) list of items
function M.locations_to_items(locations)
@@ -1601,6 +1594,8 @@ end
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
--- Defaults to current window.
---
+---@deprecated Use |setloclist()|
+---
---@param items (table) list of items
function M.set_loclist(items, win_id)
vim.fn.setloclist(win_id or 0, {}, ' ', {
@@ -1612,6 +1607,8 @@ end
--- Fills quickfix list with given list of items.
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
---
+---@deprecated Use |setqflist()|
+---
---@param items (table) list of items
function M.set_qflist(items)
vim.fn.setqflist({}, ' ', {
@@ -1869,40 +1866,6 @@ function M.lookup_section(settings, section)
return settings
end
-
---- Convert diagnostics grouped by bufnr to a list of items for use in the
---- quickfix or location list.
----
----@param diagnostics_by_bufnr table bufnr -> Diagnostic[]
----@param predicate an optional function to filter the diagnostics.
---- If present, only diagnostic items matching will be included.
----@return table (A list of items)
-function M.diagnostics_to_items(diagnostics_by_bufnr, predicate)
- local items = {}
- for bufnr, diagnostics in pairs(diagnostics_by_bufnr or {}) do
- for _, d in pairs(diagnostics) do
- if not predicate or predicate(d) then
- table.insert(items, {
- bufnr = bufnr,
- lnum = d.range.start.line + 1,
- col = d.range.start.character + 1,
- text = d.message,
- type = loclist_type_map[d.severity or DiagnosticSeverity.Error] or 'E'
- })
- end
- end
- end
- table.sort(items, function(a, b)
- if a.bufnr == b.bufnr then
- return a.lnum < b.lnum
- else
- return a.bufnr < b.bufnr
- end
- end)
- return items
-end
-
-
M._get_line_byte_from_position = get_line_byte_from_position
M._warn_once = warn_once