aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/diagnostic.lua717
-rw-r--r--runtime/lua/vim/lsp.lua185
-rw-r--r--runtime/lua/vim/lsp/buf.lua42
-rw-r--r--runtime/lua/vim/lsp/codelens.lua6
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua24
-rw-r--r--runtime/lua/vim/lsp/handlers.lua5
-rw-r--r--runtime/lua/vim/lsp/rpc.lua29
-rw-r--r--runtime/lua/vim/lsp/sync.lua381
-rw-r--r--runtime/lua/vim/lsp/util.lua247
-rw-r--r--runtime/lua/vim/shared.lua2
-rw-r--r--runtime/lua/vim/ui.lua38
11 files changed, 1081 insertions, 595 deletions
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
index 326932d982..b30a678eeb 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -1,3 +1,5 @@
+local if_nil = vim.F.if_nil
+
local M = {}
M.severity = {
@@ -19,10 +21,21 @@ local global_diagnostic_options = {
signs = true,
underline = true,
virtual_text = true,
+ float = true,
update_in_insert = false,
severity_sort = false,
}
+M.handlers = setmetatable({}, {
+ __newindex = function(t, name, handler)
+ vim.validate { handler = {handler, "t" } }
+ rawset(t, name, handler)
+ if not global_diagnostic_options[name] then
+ global_diagnostic_options[name] = true
+ end
+ end,
+})
+
-- Local functions {{{
---@private
@@ -54,7 +67,7 @@ end
local function prefix_source(source, diagnostics)
vim.validate { source = {source, function(v)
return v == "always" or v == "if_many"
- end, "Invalid value for option 'source'" } }
+ end, "'always' or 'if_many'" } }
if source == "if_many" then
local sources = {}
@@ -96,31 +109,9 @@ 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
-
- assert(name, "namespace does not exist or is anonymous")
-
- all_namespaces[ns] = {
- name = name,
- sign_group = string.format("vim.diagnostic.%s", name),
- opts = {}
- }
- end
- return all_namespaces[ns]
-end
-
----@private
local function enabled_value(option, namespace)
- local ns = get_namespace(namespace)
- if type(ns.opts[option]) == "table" then
+ local ns = namespace and M.get_namespace(namespace) or {}
+ if ns.opts and type(ns.opts[option]) == "table" then
return ns.opts[option]
end
@@ -153,8 +144,9 @@ 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)
+ local ns = namespace and M.get_namespace(namespace) or {}
+ -- Do not use tbl_deep_extend so that an empty table can be used to reset to default values
+ local resolved = vim.tbl_extend('keep', opts or {}, ns.opts or {}, global_diagnostic_options)
for k in pairs(global_diagnostic_options) do
if resolved[k] ~= nil then
resolved[k] = resolve_optional_value(k, resolved[k], namespace, bufnr)
@@ -323,7 +315,7 @@ end
---@private
local function save_extmarks(namespace, bufnr)
- bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr
+ bufnr = get_bufnr(bufnr)
if not diagnostic_attached_buffers[bufnr] then
vim.api.nvim_buf_attach(bufnr, false, {
on_lines = function(_, _, _, _, _, last)
@@ -341,7 +333,7 @@ local registered_autocmds = {}
---@private
local function make_augroup_key(namespace, bufnr)
- local ns = get_namespace(namespace)
+ local ns = M.get_namespace(namespace)
return string.format("DiagnosticInsertLeave:%s:%s", bufnr, ns.name)
end
@@ -354,19 +346,15 @@ local function schedule_display(namespace, bufnr, 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")
-
+ vim.cmd(string.format([[augroup %s
+ au!
+ autocmd %s <buffer=%s> lua vim.diagnostic._execute_scheduled_display(%s, %s)
+ augroup END]],
+ key,
+ table.concat(insert_leave_auto_cmds, ","),
+ bufnr,
+ namespace,
+ bufnr))
registered_autocmds[key] = true
end
end
@@ -376,77 +364,14 @@ 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")
-
+ vim.cmd(string.format([[augroup %s
+ au!
+ augroup END]], key))
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 not diagnostics or 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
-
- if opts.format then
- diagnostics = reformat_diagnostics(opts.format, diagnostics)
- end
-
- if opts.source then
- diagnostics = prefix_source(opts.source, diagnostics)
- end
-
- -- Use global setting for severity_sort since 'show_diagnostics' is namespace
- -- independent
- local severity_sort = global_diagnostic_options.severity_sort
- if severity_sort then
- if type(severity_sort) == "table" and severity_sort.reverse then
- table.sort(diagnostics, function(a, b) return a.severity > b.severity end)
- else
- table.sort(diagnostics, function(a, b) return a.severity < b.severity end)
- end
- 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
-
----@private
local function set_list(loclist, opts)
opts = opts or {}
local open = vim.F.if_nil(opts.open, true)
@@ -469,6 +394,7 @@ local function set_list(loclist, opts)
end
---@private
+--- To (slightly) improve performance, modifies diagnostics in place.
local function clamp_line_numbers(bufnr, diagnostics)
local buf_line_count = vim.api.nvim_buf_line_count(bufnr)
if buf_line_count == 0 then
@@ -526,7 +452,7 @@ end
local function diagnostic_move_pos(opts, pos)
opts = opts or {}
- local enable_popup = vim.F.if_nil(opts.enable_popup, true)
+ local float = vim.F.if_nil(opts.float, true)
local win_id = opts.win_id or vim.api.nvim_get_current_win()
if not pos then
@@ -539,10 +465,13 @@ local function diagnostic_move_pos(opts, pos)
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.
+ if float then
+ local float_opts = type(float) == "table" and float or {}
vim.schedule(function()
- M.show_position_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id))
+ M.open_float(
+ vim.api.nvim_win_get_buf(win_id),
+ vim.tbl_extend("keep", float_opts, {scope="cursor"})
+ )
end)
end
end
@@ -561,12 +490,12 @@ end
---
--- For example, if a user enables virtual text globally with
--- <pre>
---- vim.diagnostic.config({virt_text = true})
+--- vim.diagnostic.config({virtual_text = true})
--- </pre>
---
--- and a diagnostic producer sets diagnostics with
--- <pre>
---- vim.diagnostic.set(ns, 0, diagnostics, {virt_text = false})
+--- vim.diagnostic.set(ns, 0, diagnostics, {virtual_text = false})
--- </pre>
---
--- then virtual text will not be enabled for those diagnostics.
@@ -603,6 +532,19 @@ end
--- * priority: (number, default 10) Base priority to use for signs. When
--- {severity_sort} is used, the priority of a sign is adjusted based on
--- its severity. Otherwise, all signs use the same priority.
+--- - float: Options for floating windows:
+--- * severity: See |diagnostic-severity|.
+--- * show_header: (boolean, default true) Show "Diagnostics:" header
+--- * source: (string) Include the diagnostic source in
+--- the message. One of "always" or "if_many".
+--- * format: (function) A function that takes a diagnostic as input and returns a
+--- string. The return value is the text used to display the diagnostic.
+--- * prefix: (function or string) Prefix each diagnostic in the floating window. If
+--- a function, it must have the signature (diagnostic, i, total) -> string,
+--- where {i} is the index of the diagnostic being evaluated and {total} is
+--- the total number of diagnostics displayed in the window. The returned
+--- string is prepended to each diagnostic in the window. Otherwise,
+--- if {prefix} is a string, it is prepended to each diagnostic.
--- - 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
@@ -620,7 +562,7 @@ function M.config(opts, namespace)
local t
if namespace then
- local ns = get_namespace(namespace)
+ local ns = M.get_namespace(namespace)
t = ns.opts
else
t = global_diagnostic_options
@@ -672,7 +614,7 @@ function M.set(namespace, bufnr, diagnostics, opts)
-- Clean up our data when the buffer unloads.
vim.api.nvim_buf_attach(bufnr, false, {
on_detach = function(_, b)
- clear_diagnostic_cache(b, namespace)
+ clear_diagnostic_cache(namespace, b)
diagnostic_cleanup[b][namespace] = nil
end
})
@@ -687,6 +629,32 @@ function M.set(namespace, bufnr, diagnostics, opts)
vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged")
end
+--- Get namespace metadata.
+---
+---@param ns number Diagnostic namespace
+---@return table Namespace metadata
+function M.get_namespace(namespace)
+ vim.validate { namespace = { namespace, 'n' } }
+ if not all_namespaces[namespace] then
+ local name
+ for k, v in pairs(vim.api.nvim_get_namespaces()) do
+ if namespace == v then
+ name = k
+ break
+ end
+ end
+
+ assert(name, "namespace does not exist or is anonymous")
+
+ all_namespaces[namespace] = {
+ name = name,
+ opts = {},
+ user_data = {},
+ }
+ end
+ return all_namespaces[namespace]
+end
+
--- Get current diagnostic namespaces.
---
---@return table A list of active diagnostic namespaces |vim.diagnostic|.
@@ -825,10 +793,9 @@ end
--- |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()|
+--- - float: (boolean or table, default true) If "true", call |vim.diagnostic.open_float()|
+--- after moving. If a table, pass the table as the {opts} parameter to
+--- |vim.diagnostic.open_float()|.
--- - win_id: (number, default 0) Window ID
function M.goto_next(opts)
return diagnostic_move_pos(
@@ -837,156 +804,171 @@ function M.goto_next(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},
- }
+M.handlers.signs = {
+ show = function(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)
+ bufnr = get_bufnr(bufnr)
- if opts.signs and opts.signs.severity then
- diagnostics = filter_by_severity(opts.signs.severity, diagnostics)
- end
-
- local ns = get_namespace(namespace)
+ if opts.signs and opts.signs.severity then
+ diagnostics = filter_by_severity(opts.signs.severity, diagnostics)
+ end
- define_default_signs()
+ define_default_signs()
- -- 10 is the default sign priority when none is explicitly specified
- local priority = opts.signs and opts.signs.priority or 10
- local get_priority
- if opts.severity_sort then
- if type(opts.severity_sort) == "table" and opts.severity_sort.reverse then
- get_priority = function(severity)
- return priority + (severity - vim.diagnostic.severity.ERROR)
+ -- 10 is the default sign priority when none is explicitly specified
+ local priority = opts.signs and opts.signs.priority or 10
+ local get_priority
+ if opts.severity_sort then
+ if type(opts.severity_sort) == "table" and opts.severity_sort.reverse then
+ get_priority = function(severity)
+ return priority + (severity - vim.diagnostic.severity.ERROR)
+ end
+ else
+ get_priority = function(severity)
+ return priority + (vim.diagnostic.severity.HINT - severity)
+ end
end
else
- get_priority = function(severity)
- return priority + (vim.diagnostic.severity.HINT - severity)
+ get_priority = function()
+ return priority
end
end
- else
- get_priority = function()
- return priority
- end
- end
-
- for _, diagnostic in ipairs(diagnostics) do
- vim.fn.sign_place(
- 0,
- ns.sign_group,
- sign_highlight_map[diagnostic.severity],
- bufnr,
- {
- priority = get_priority(diagnostic.severity),
- 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},
- }
+ local ns = M.get_namespace(namespace)
+ if not ns.user_data.sign_group then
+ ns.user_data.sign_group = string.format("vim.diagnostic.%s", ns.name)
+ end
- bufnr = get_bufnr(bufnr)
- opts = get_resolved_options({ underline = opts }, namespace, bufnr).underline
+ local sign_group = ns.user_data.sign_group
+ for _, diagnostic in ipairs(diagnostics) do
+ vim.fn.sign_place(
+ 0,
+ sign_group,
+ sign_highlight_map[diagnostic.severity],
+ bufnr,
+ {
+ priority = get_priority(diagnostic.severity),
+ lnum = diagnostic.lnum + 1
+ }
+ )
+ end
+ end,
+ hide = function(namespace, bufnr)
+ local ns = M.get_namespace(namespace)
+ if ns.user_data.sign_group then
+ vim.fn.sign_unplace(ns.user_data.sign_group, {buffer=bufnr})
+ end
+ end,
+}
- if opts and opts.severity then
- diagnostics = filter_by_severity(opts.severity, diagnostics)
- end
+M.handlers.underline = {
+ show = function(namespace, bufnr, diagnostics, opts)
+ vim.validate {
+ namespace = {namespace, 'n'},
+ bufnr = {bufnr, 'n'},
+ diagnostics = {diagnostics, 't'},
+ opts = {opts, 't', true},
+ }
- for _, diagnostic in ipairs(diagnostics) do
- local higroup = underline_highlight_map[diagnostic.severity]
+ bufnr = get_bufnr(bufnr)
- if higroup == nil then
- -- Default to error if we don't have a highlight associated
- higroup = underline_highlight_map.Error
+ if opts.underline and opts.underline.severity then
+ diagnostics = filter_by_severity(opts.underline.severity, diagnostics)
end
- vim.highlight.range(
- bufnr,
- namespace,
- higroup,
- { diagnostic.lnum, diagnostic.col },
- { diagnostic.end_lnum, diagnostic.end_col }
- )
- end
-end
+ local ns = M.get_namespace(namespace)
+ if not ns.user_data.underline_ns then
+ ns.user_data.underline_ns = vim.api.nvim_create_namespace("")
+ 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.
---- - source: (string) Include the diagnostic source in virtual text. One of "always" or
---- "if_many".
----@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},
- }
+ local underline_ns = ns.user_data.underline_ns
+ for _, diagnostic in ipairs(diagnostics) do
+ local higroup = underline_highlight_map[diagnostic.severity]
- bufnr = get_bufnr(bufnr)
- opts = get_resolved_options({ virtual_text = opts }, namespace, bufnr).virtual_text
+ if higroup == nil then
+ -- Default to error if we don't have a highlight associated
+ higroup = underline_highlight_map.Error
+ end
- if opts and opts.format then
- diagnostics = reformat_diagnostics(opts.format, diagnostics)
+ vim.highlight.range(
+ bufnr,
+ underline_ns,
+ higroup,
+ { diagnostic.lnum, diagnostic.col },
+ { diagnostic.end_lnum, diagnostic.end_col }
+ )
+ end
+ save_extmarks(underline_ns, bufnr)
+ end,
+ hide = function(namespace, bufnr)
+ local ns = M.get_namespace(namespace)
+ if ns.user_data.underline_ns then
+ diagnostic_cache_extmarks[bufnr][ns.user_data.underline_ns] = {}
+ vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.underline_ns, 0, -1)
+ end
end
+}
- if opts and opts.source then
- diagnostics = prefix_source(opts.source, diagnostics)
- end
+M.handlers.virtual_text = {
+ show = function(namespace, bufnr, diagnostics, opts)
+ vim.validate {
+ namespace = {namespace, 'n'},
+ bufnr = {bufnr, 'n'},
+ diagnostics = {diagnostics, 't'},
+ opts = {opts, 't', true},
+ }
- 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)
+ bufnr = get_bufnr(bufnr)
+
+ local severity
+ if opts.virtual_text then
+ if opts.virtual_text.format then
+ diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics)
+ end
+ if opts.virtual_text.source then
+ diagnostics = prefix_source(opts.virtual_text.source, diagnostics)
+ end
+ if opts.virtual_text.severity then
+ severity = opts.virtual_text.severity
+ end
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,
- })
+ local ns = M.get_namespace(namespace)
+ if not ns.user_data.virt_text_ns then
+ ns.user_data.virt_text_ns = vim.api.nvim_create_namespace("")
end
- end
-end
+
+ local virt_text_ns = ns.user_data.virt_text_ns
+ local buffer_line_diagnostics = diagnostic_lines(diagnostics)
+ for line, line_diagnostics in pairs(buffer_line_diagnostics) do
+ if severity then
+ line_diagnostics = filter_by_severity(severity, line_diagnostics)
+ end
+ local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts.virtual_text)
+
+ if virt_texts then
+ vim.api.nvim_buf_set_extmark(bufnr, virt_text_ns, line, 0, {
+ hl_mode = "combine",
+ virt_text = virt_texts,
+ })
+ end
+ end
+ save_extmarks(virt_text_ns, bufnr)
+ end,
+ hide = function(namespace, bufnr)
+ local ns = M.get_namespace(namespace)
+ if ns.user_data.virt_text_ns then
+ diagnostic_cache_extmarks[bufnr][ns.user_data.virt_text_ns] = {}
+ vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_text_ns, 0, -1)
+ end
+ end,
+}
--- Get virtual text chunks to display using |nvim_buf_set_extmark()|.
---
@@ -1055,46 +1037,54 @@ end
--- To hide diagnostics and prevent them from re-displaying, use
--- |vim.diagnostic.disable()|.
---
----@param namespace number The diagnostic namespace
+---@param namespace number|nil Diagnostic namespace. When omitted, hide
+--- diagnostics from all namespaces.
---@param bufnr number|nil Buffer number. Defaults to the current buffer.
function M.hide(namespace, bufnr)
vim.validate {
- namespace = { namespace, 'n' },
+ namespace = { namespace, 'n', true },
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)
+ local namespaces = namespace and {namespace} or vim.tbl_keys(diagnostic_cache[bufnr])
+ for _, iter_namespace in ipairs(namespaces) do
+ for _, handler in pairs(M.handlers) do
+ if handler.hide then
+ handler.hide(iter_namespace, bufnr)
+ end
+ end
+ end
end
-
--- Display diagnostics for the given namespace and buffer.
---
----@param namespace number Diagnostic namespace
+---@param namespace number|nil Diagnostic namespace. When omitted, show
+--- diagnostics from all namespaces.
---@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.
+--- diagnostics. May not be used when {namespace} is nil.
---@param opts table|nil Display options. See |vim.diagnostic.config()|.
function M.show(namespace, bufnr, diagnostics, opts)
vim.validate {
- namespace = { namespace, 'n' },
+ namespace = { namespace, 'n', true },
bufnr = { bufnr, 'n', true },
diagnostics = { diagnostics, 't', true },
opts = { opts, 't', true },
}
bufnr = get_bufnr(bufnr)
+ if not namespace then
+ assert(not diagnostics, "Cannot show diagnostics without a namespace")
+ for iter_namespace in pairs(diagnostic_cache[bufnr]) do
+ M.show(iter_namespace, bufnr, nil, opts)
+ end
+ return
+ end
+
if is_disabled(namespace, bufnr) then
return
end
@@ -1129,83 +1119,154 @@ function M.show(namespace, bufnr, diagnostics, opts)
clamp_line_numbers(bufnr, diagnostics)
- 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)
+ for handler_name, handler in pairs(M.handlers) do
+ if handler.show and opts[handler_name] then
+ handler.show(namespace, bufnr, diagnostics, opts)
+ end
end
-
- save_extmarks(namespace, bufnr)
end
---- Open a floating window with the diagnostics at the given position.
+--- Show diagnostics in a floating window.
---
+---@param bufnr number|nil Buffer number. Defaults to the current buffer.
---@param opts table|nil Configuration table with the same keys as
--- |vim.lsp.util.open_floating_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
---- - source: (string) Include the diagnostic source in
---- the message. One of "always" or "if_many".
+--- - scope: (string, default "buffer") Show diagnostics from the whole buffer ("buffer"),
+--- the current cursor line ("line"), or the current cursor position ("cursor").
+--- - pos: (number or table) If {scope} is "line" or "cursor", use this position rather
+--- than the cursor position. If a number, interpreted as a line number;
+--- otherwise, a (row, col) tuple.
+--- - severity_sort: (default false) Sort diagnostics by severity. Overrides the setting
+--- from |vim.diagnostic.config()|.
+--- - severity: See |diagnostic-severity|. Overrides the setting from
+--- |vim.diagnostic.config()|.
+--- - show_header: (boolean, default true) Show "Diagnostics:" header. Overrides the
+--- setting from |vim.diagnostic.config()|.
+--- - source: (string) Include the diagnostic source in the message. One of "always" or
+--- "if_many". Overrides the setting from |vim.diagnostic.config()|.
--- - format: (function) A function that takes a diagnostic as input and returns a
--- string. The return value is the text used to display the diagnostic.
----@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 tuple ({popup_bufnr}, {win_id})
-function M.show_position_diagnostics(opts, bufnr, position)
+--- Overrides the setting from |vim.diagnostic.config()|.
+--- - prefix: (function or string) Prefix each diagnostic in the floating window.
+--- Overrides the setting from |vim.diagnostic.config()|.
+---@return tuple ({float_bufnr}, {win_id})
+function M.open_float(bufnr, opts)
vim.validate {
- opts = { opts, 't', true },
bufnr = { bufnr, 'n', true },
- position = { position, 't', true },
+ opts = { opts, '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
+ local scope = opts.scope or "buffer"
+ local lnum, col
+ if scope == "line" or scope == "cursor" then
+ if not opts.pos then
+ local pos = vim.api.nvim_win_get_cursor(0)
+ lnum = pos[1] - 1
+ col = pos[2]
+ elseif type(opts.pos) == "number" then
+ lnum = opts.pos
+ elseif type(opts.pos) == "table" then
+ lnum, col = unpack(opts.pos)
+ else
+ error("Invalid value for option 'pos'")
+ end
+ elseif scope ~= "buffer" then
+ error("Invalid value for option 'scope'")
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)
+
+ do
+ -- Resolve options with user settings from vim.diagnostic.config
+ -- Unlike the other decoration functions (e.g. set_virtual_text, set_signs, etc.) `open_float`
+ -- does not have a dedicated table for configuration options; instead, the options are mixed in
+ -- with its `opts` table which also includes "keyword" parameters. So we create a dedicated
+ -- options table that inherits missing keys from the global configuration before resolving.
+ local t = global_diagnostic_options.float
+ local float_opts = vim.tbl_extend("keep", opts, type(t) == "table" and t or {})
+ opts = get_resolved_options({ float = float_opts }, nil, bufnr).float
end
+
local diagnostics = M.get(bufnr, opts)
clamp_line_numbers(bufnr, diagnostics)
- local position_diagnostics = vim.tbl_filter(match_position_predicate, diagnostics)
- 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 tuple ({popup_bufnr}, {win_id})
-function M.show_line_diagnostics(opts, bufnr, lnum)
- vim.validate {
- opts = { opts, 't', true },
- bufnr = { bufnr, 'n', true },
- lnum = { lnum, 'n', true },
- }
+ if scope == "line" then
+ diagnostics = vim.tbl_filter(function(d)
+ return d.lnum == lnum
+ end, diagnostics)
+ elseif scope == "cursor" then
+ -- LSP servers can send diagnostics with `end_col` past the length of the line
+ local line_length = #vim.api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
+ diagnostics = vim.tbl_filter(function(d)
+ return d.lnum == lnum
+ and math.min(d.col, line_length - 1) <= col
+ and (d.end_col >= col or d.end_lnum > lnum)
+ end, diagnostics)
+ end
- opts = opts or {}
- opts.focus_id = "line_diagnostics"
- bufnr = get_bufnr(bufnr)
- local diagnostics = M.get(bufnr, opts)
- clamp_line_numbers(bufnr, diagnostics)
- lnum = lnum or (vim.api.nvim_win_get_cursor(0)[1] - 1)
- local line_diagnostics = diagnostic_lines(diagnostics)[lnum]
- return show_diagnostics(opts, line_diagnostics)
+ if vim.tbl_isempty(diagnostics) then
+ return
+ end
+
+ local severity_sort = vim.F.if_nil(opts.severity_sort, global_diagnostic_options.severity_sort)
+ if severity_sort then
+ if type(severity_sort) == "table" and severity_sort.reverse then
+ table.sort(diagnostics, function(a, b) return a.severity > b.severity end)
+ else
+ table.sort(diagnostics, function(a, b) return a.severity < b.severity end)
+ end
+ 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
+
+ if opts.format then
+ diagnostics = reformat_diagnostics(opts.format, diagnostics)
+ end
+
+ if opts.source then
+ diagnostics = prefix_source(opts.source, diagnostics)
+ end
+
+ local prefix_opt = if_nil(opts.prefix, (scope == "cursor" and #diagnostics <= 1) and "" or function(_, i)
+ return string.format("%d. ", i)
+ end)
+ if prefix_opt then
+ vim.validate { prefix = { prefix_opt, function(v)
+ return type(v) == "string" or type(v) == "function"
+ end, "'string' or 'function'" } }
+ end
+
+ for i, diagnostic in ipairs(diagnostics) do
+ local prefix = type(prefix_opt) == "string" and prefix_opt or prefix_opt(diagnostic, i, #diagnostics)
+ local hiname = floating_highlight_map[diagnostic.severity]
+ local message_lines = vim.split(diagnostic.message, '\n')
+ 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
+
+ -- Used by open_floating_preview to allow the float to be focused
+ if not opts.focus_id then
+ opts.focus_id = scope
+ end
+ local float_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(float_bufnr, -1, hiname, i-1, prefixlen, -1)
+ end
+
+ return float_bufnr, winnr
end
--- Remove all diagnostics from the given namespace.
@@ -1215,19 +1276,23 @@ end
--- simply remove diagnostic decorations in a way that they can be
--- re-displayed, use |vim.diagnostic.hide()|.
---
----@param namespace number
+---@param namespace number|nil Diagnostic namespace. When omitted, remove
+--- diagnostics from all namespaces.
---@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
+ vim.validate {
+ namespace = {namespace, 'n', true},
+ bufnr = {bufnr, 'n', true},
+ }
+
+ local buffers = bufnr and {bufnr} or vim.tbl_keys(diagnostic_cache)
+ for _, iter_bufnr in ipairs(buffers) do
+ local namespaces = namespace and {namespace} or vim.tbl_keys(diagnostic_cache[iter_bufnr])
+ for _, iter_namespace in ipairs(namespaces) do
+ clear_diagnostic_cache(iter_namespace, iter_bufnr)
+ M.hide(iter_namespace, iter_bufnr)
end
- else
- clear_diagnostic_cache(namespace, bufnr)
- M.hide(namespace, bufnr)
end
vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged")
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 9c35351608..0fc0a7a7aa 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -5,6 +5,7 @@ local log = require 'vim.lsp.log'
local lsp_rpc = require 'vim.lsp.rpc'
local protocol = require 'vim.lsp.protocol'
local util = require 'vim.lsp.util'
+local sync = require 'vim.lsp.sync'
local vim = vim
local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option
@@ -108,6 +109,12 @@ local valid_encodings = {
UTF8 = 'utf-8'; UTF16 = 'utf-16'; UTF32 = 'utf-32';
}
+local format_line_ending = {
+ ["unix"] = '\n',
+ ["dos"] = '\r\n',
+ ["mac"] = '\r',
+}
+
local client_index = 0
---@private
--- Returns a new, unused client id.
@@ -122,9 +129,6 @@ local active_clients = {}
local all_buffer_active_clients = {}
local uninitialized_clients = {}
--- Tracks all buffers attached to a client.
-local all_client_active_buffers = {}
-
---@private
--- Invokes a function for each LSP client attached to the buffer {bufnr}.
---
@@ -242,6 +246,7 @@ local function validate_client_config(config)
on_exit = { config.on_exit, "f", true };
on_init = { config.on_init, "f", true };
settings = { config.settings, "t", true };
+ commands = { config.commands, 't', true };
before_init = { config.before_init, "f", true };
offset_encoding = { config.offset_encoding, "s", true };
flags = { config.flags, "t", true };
@@ -353,15 +358,14 @@ do
end
end
- function changetracking.prepare(bufnr, firstline, new_lastline, changedtick)
+ function changetracking.prepare(bufnr, firstline, lastline, new_lastline, changedtick)
local incremental_changes = function(client)
local cached_buffers = state_by_client[client.id].buffers
- local lines = nvim_buf_get_lines(bufnr, 0, -1, true)
- local startline = math.min(firstline + 1, math.min(#cached_buffers[bufnr], #lines))
- local endline = math.min(-(#lines - new_lastline), -1)
- local incremental_change = vim.lsp.util.compute_diff(
- cached_buffers[bufnr], lines, startline, endline, client.offset_encoding or 'utf-16')
- cached_buffers[bufnr] = lines
+ local curr_lines = nvim_buf_get_lines(bufnr, 0, -1, true)
+ local line_ending = format_line_ending[vim.api.nvim_buf_get_option(0, 'fileformat')]
+ local incremental_change = sync.compute_diff(
+ cached_buffers[bufnr], curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending or '\n')
+ cached_buffers[bufnr] = curr_lines
return incremental_change
end
local full_changes = once(function()
@@ -468,7 +472,11 @@ local function text_document_did_open_handler(bufnr, client)
-- Next chance we get, we should re-do the diagnostics
vim.schedule(function()
- vim.lsp.diagnostic.redraw(bufnr, client.id)
+ -- Protect against a race where the buffer disappears
+ -- between `did_open_handler` and the scheduled function firing.
+ if vim.api.nvim_buf_is_valid(bufnr) then
+ vim.lsp.diagnostic.redraw(bufnr, client.id)
+ end
end)
end
@@ -593,6 +601,11 @@ end
--- returned to the language server if requested via `workspace/configuration`.
--- Keys are case-sensitive.
---
+---@param commands table Table that maps string of clientside commands to user-defined functions.
+--- Commands passed to start_client take precedence over the global command registry. Each key
+--- must be a unique comand name, and the value is a function which is called if any LSP action
+--- (code action, code lenses, ...) triggers the command.
+---
---@param init_options Values to pass in the initialization request
--- as `initializationOptions`. See `initialize` in the LSP spec.
---
@@ -647,7 +660,9 @@ end
--- - debounce_text_changes (number, default nil): Debounce didChange
--- notifications to the server by the given number in milliseconds. No debounce
--- occurs if nil
----
+--- - exit_timeout (number, default 500): Milliseconds to wait for server to
+-- exit cleanly after sending the 'shutdown' request before sending kill -15.
+-- If set to false, nvim exits immediately after sending the 'shutdown' request to the server.
---@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be
--- fully initialized. Use `on_init` to do any actions once
--- the client has been initialized.
@@ -742,7 +757,6 @@ function lsp.start_client(config)
lsp.diagnostic.reset(client_id, all_buffer_active_clients)
changetracking.reset(client_id)
- all_client_active_buffers[client_id] = nil
for _, client_ids in pairs(all_buffer_active_clients) do
client_ids[client_id] = nil
end
@@ -771,10 +785,14 @@ function lsp.start_client(config)
rpc = rpc;
offset_encoding = offset_encoding;
config = config;
+ attached_buffers = {};
handlers = handlers;
+ commands = config.commands or {};
+
+ requests = {};
-- for $/progress report
- messages = { name = name, messages = {}, progress = {}, status = {} }
+ messages = { name = name, messages = {}, progress = {}, status = {} };
}
-- Store the uninitialized_clients for cleanup in case we exit before initialize finishes.
@@ -907,11 +925,21 @@ function lsp.start_client(config)
end
-- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state
changetracking.flush(client)
-
+ bufnr = resolve_bufnr(bufnr)
local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr)
- return rpc.request(method, params, function(err, result)
+ local success, request_id = rpc.request(method, params, function(err, result)
handler(err, result, {method=method, client_id=client_id, bufnr=bufnr, params=params})
+ end, function(request_id)
+ client.requests[request_id] = nil
+ nvim_command("doautocmd <nomodeline> User LspRequest")
end)
+
+ if success then
+ client.requests[request_id] = { type='pending', bufnr=bufnr, method=method }
+ nvim_command("doautocmd <nomodeline> User LspRequest")
+ end
+
+ return success, request_id
end
---@private
@@ -971,6 +999,11 @@ function lsp.start_client(config)
---@see |vim.lsp.client.notify()|
function client.cancel_request(id)
validate{id = {id, 'n'}}
+ local request = client.requests[id]
+ if request and request.type == 'pending' then
+ request.type = 'cancel'
+ nvim_command("doautocmd <nomodeline> User LspRequest")
+ end
return rpc.notify("$/cancelRequest", { id = id })
end
@@ -989,7 +1022,6 @@ function lsp.start_client(config)
lsp.diagnostic.reset(client_id, all_buffer_active_clients)
changetracking.reset(client_id)
- all_client_active_buffers[client_id] = nil
for _, client_ids in pairs(all_buffer_active_clients) do
client_ids[client_id] = nil
end
@@ -1032,6 +1064,7 @@ function lsp.start_client(config)
-- TODO(ashkan) handle errors.
pcall(config.on_attach, client, bufnr)
end
+ client.attached_buffers[bufnr] = true
end
initialize()
@@ -1044,22 +1077,14 @@ end
--- Notify all attached clients that a buffer has changed.
local text_document_did_change_handler
do
- text_document_did_change_handler = function(_, bufnr, changedtick,
- firstline, lastline, new_lastline, old_byte_size, old_utf32_size,
- old_utf16_size)
-
- local _ = log.debug() and log.debug(
- string.format("on_lines bufnr: %s, changedtick: %s, firstline: %s, lastline: %s, new_lastline: %s, old_byte_size: %s, old_utf32_size: %s, old_utf16_size: %s",
- bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size),
- nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
- )
+ text_document_did_change_handler = function(_, bufnr, changedtick, firstline, lastline, new_lastline)
-- Don't do anything if there are no clients attached.
if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then
return
end
util.buf_versions[bufnr] = changedtick
- local compute_change_and_notify = changetracking.prepare(bufnr, firstline, new_lastline, changedtick)
+ local compute_change_and_notify = changetracking.prepare(bufnr, firstline, lastline, new_lastline, changedtick)
for_each_buffer_client(bufnr, compute_change_and_notify)
end
end
@@ -1142,12 +1167,6 @@ function lsp.buf_attach_client(bufnr, client_id)
})
end
- if not all_client_active_buffers[client_id] then
- all_client_active_buffers[client_id] = {}
- end
-
- table.insert(all_client_active_buffers[client_id], bufnr)
-
if buffer_client_ids[client_id] then return end
-- This is our first time attaching this client to this buffer.
buffer_client_ids[client_id] = true
@@ -1172,7 +1191,7 @@ end
--- Gets a client by id, or nil if the id is invalid.
--- The returned client may not yet be fully initialized.
--
----@param client_id client id number
+---@param client_id number client id
---
---@returns |vim.lsp.client| object, or nil
function lsp.get_client_by_id(client_id)
@@ -1181,15 +1200,11 @@ end
--- Returns list of buffers attached to client_id.
--
----@param client_id client id
+---@param client_id number client id
---@returns list of buffer ids
function lsp.get_buffers_by_client_id(client_id)
- local active_client_buffers = all_client_active_buffers[client_id]
- if active_client_buffers then
- return active_client_buffers
- else
- return {}
- end
+ local client = lsp.get_client_by_id(client_id)
+ return client and vim.tbl_keys(client.attached_buffers) or {}
end
--- Stops a client(s).
@@ -1239,9 +1254,41 @@ function lsp._vim_exit_handler()
client.stop()
end
- if not vim.wait(500, function() return tbl_isempty(active_clients) end, 50) then
- for _, client in pairs(active_clients) do
- client.stop(true)
+ local timeouts = {}
+ local max_timeout = 0
+ local send_kill = false
+
+ for client_id, client in pairs(active_clients) do
+ local timeout = if_nil(client.config.flags.exit_timeout, 500)
+ if timeout then
+ send_kill = true
+ timeouts[client_id] = timeout
+ max_timeout = math.max(timeout, max_timeout)
+ end
+ end
+
+ local poll_time = 50
+
+ local function check_clients_closed()
+ for client_id, timeout in pairs(timeouts) do
+ timeouts[client_id] = timeout - poll_time
+ end
+
+ for client_id, _ in pairs(active_clients) do
+ if timeouts[client_id] ~= nil and timeouts[client_id] > 0 then
+ return false
+ end
+ end
+ return true
+ end
+
+ if send_kill then
+ if not vim.wait(max_timeout, check_clients_closed, poll_time) then
+ for client_id, client in pairs(active_clients) do
+ if timeouts[client_id] ~= nil then
+ client.stop(true)
+ end
+ end
end
end
end
@@ -1282,7 +1329,7 @@ function lsp.buf_request(bufnr, method, params, handler)
if not tbl_isempty(all_buffer_active_clients[resolve_bufnr(bufnr)] or {}) and not method_supported then
vim.notify(lsp._unsupported_method(method), vim.log.levels.ERROR)
vim.api.nvim_command("redraw")
- return
+ return {}, function() end
end
local client_request_ids = {}
@@ -1430,7 +1477,7 @@ end
---@param findstart 0 or 1, decides behavior
---@param base If findstart=0, text to match against
---
----@returns (number) Decided by `findstart`:
+---@returns (number) Decided by {findstart}:
--- - findstart=0: column where the completion starts, or -2 or -3
--- - findstart=1: list of matches (actually just calls |complete()|)
function lsp.omnifunc(findstart, base)
@@ -1494,6 +1541,52 @@ function lsp.omnifunc(findstart, base)
return -2
end
+--- Provides an interface between the built-in client and a `formatexpr` function.
+---
+--- Currently only supports a single client. This can be set via
+--- `setlocal formatexpr=v:lua.vim.lsp.formatexpr()` but will typically or in `on_attach`
+--- via `vim.api.nvim_buf_set_option(bufnr, 'formatexpr', 'v:lua.vim.lsp.formatexpr(#{timeout_ms:250})')`.
+---
+---@param opts table options for customizing the formatting expression which takes the
+--- following optional keys:
+--- * timeout_ms (default 500ms). The timeout period for the formatting request.
+function lsp.formatexpr(opts)
+ opts = opts or {}
+ local timeout_ms = opts.timeout_ms or 500
+
+ if vim.tbl_contains({'i', 'R', 'ic', 'ix'}, vim.fn.mode()) then
+ -- `formatexpr` is also called when exceeding `textwidth` in insert mode
+ -- fall back to internal formatting
+ return 1
+ end
+
+ local start_line = vim.v.lnum
+ local end_line = start_line + vim.v.count - 1
+
+ if start_line > 0 and end_line > 0 then
+ local params = {
+ textDocument = util.make_text_document_params();
+ range = {
+ start = { line = start_line - 1; character = 0; };
+ ["end"] = { line = end_line - 1; character = 0; };
+ };
+ };
+ params.options = util.make_formatting_params().options
+ local client_results = vim.lsp.buf_request_sync(0, "textDocument/rangeFormatting", params, timeout_ms)
+
+ -- Apply the text edits from one and only one of the clients.
+ for _, response in pairs(client_results) do
+ if response.result then
+ vim.lsp.util.apply_text_edits(response.result, 0)
+ return 0
+ end
+ end
+ end
+
+ -- do not run builtin formatter.
+ return 0
+end
+
---Checks whether a client is stopped.
---
---@param client_id (Number)
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 245f29943e..747d761730 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -247,22 +247,35 @@ end
--- Renames all references to the symbol under the cursor.
---
---@param new_name (string) If not provided, the user will be prompted for a new
----name using |input()|.
+---name using |vim.ui.input()|.
function M.rename(new_name)
- local params = util.make_position_params()
+ local opts = {
+ prompt = "New Name: "
+ }
+
+ ---@private
+ local function on_confirm(input)
+ if not (input and #input > 0) then return end
+ local params = util.make_position_params()
+ params.newName = input
+ request('textDocument/rename', params)
+ end
+
local function prepare_rename(err, result)
if err == nil and result == nil then
vim.notify('nothing to rename', vim.log.levels.INFO)
return
end
if result and result.placeholder then
- new_name = new_name or npcall(vfn.input, "New Name: ", result.placeholder)
+ opts.default = result.placeholder
+ if not new_name then npcall(vim.ui.input, opts, on_confirm) end
elseif result and result.start and result['end'] and
result.start.line == result['end'].line then
local line = vfn.getline(result.start.line+1)
local start_char = result.start.character+1
local end_char = result['end'].character
- new_name = new_name or npcall(vfn.input, "New Name: ", string.sub(line, start_char, end_char))
+ opts.default = string.sub(line, start_char, end_char)
+ if not new_name then npcall(vim.ui.input, opts, on_confirm) end
else
-- fallback to guessing symbol using <cword>
--
@@ -270,13 +283,12 @@ function M.rename(new_name)
-- returns an unexpected response, or requests for "default behavior"
--
-- see https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename
- new_name = new_name or npcall(vfn.input, "New Name: ", vfn.expand('<cword>'))
+ opts.default = vfn.expand('<cword>')
+ if not new_name then npcall(vim.ui.input, opts, on_confirm) end
end
- if not (new_name and #new_name > 0) then return end
- params.newName = new_name
- request('textDocument/rename', params)
+ if new_name then on_confirm(new_name) end
end
- request('textDocument/prepareRename', params, prepare_rename)
+ request('textDocument/prepareRename', util.make_position_params(), prepare_rename)
end
--- Lists all the references to the symbol under the cursor in the quickfix window.
@@ -357,7 +369,7 @@ end
function M.list_workspace_folders()
local workspace_folders = {}
for _, client in pairs(vim.lsp.buf_get_clients()) do
- for _, folder in pairs(client.workspaceFolders) do
+ for _, folder in pairs(client.workspaceFolders or {}) do
table.insert(workspace_folders, folder.name)
end
end
@@ -377,7 +389,7 @@ function M.add_workspace_folder(workspace_folder)
local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}})
for _, client in pairs(vim.lsp.buf_get_clients()) do
local found = false
- for _, folder in pairs(client.workspaceFolders) do
+ for _, folder in pairs(client.workspaceFolders or {}) do
if folder.name == workspace_folder then
found = true
print(workspace_folder, "is already part of this workspace")
@@ -386,6 +398,9 @@ function M.add_workspace_folder(workspace_folder)
end
if not found then
vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
+ if not client.workspaceFolders then
+ client.workspaceFolders = {}
+ end
table.insert(client.workspaceFolders, params.event.added[1])
end
end
@@ -480,11 +495,11 @@ local function on_code_action_results(results, ctx)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
- local fn = vim.lsp.commands[command.command]
+ local fn = client.commands[command.command] or vim.lsp.commands[command.command]
if fn then
local enriched_ctx = vim.deepcopy(ctx)
enriched_ctx.client_id = client.id
- fn(command, ctx)
+ fn(command, enriched_ctx)
else
M.execute_command(command)
end
@@ -529,6 +544,7 @@ local function on_code_action_results(results, ctx)
vim.ui.select(action_tuples, {
prompt = 'Code actions:',
+ kind = 'codeaction',
format_item = function(action_tuple)
local title = action_tuple[2].title:gsub('\r\n', '\\r\\n')
return title:gsub('\n', '\\n')
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index 63fcbe430b..9eb64c9a2e 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -31,15 +31,15 @@ local function execute_lens(lens, bufnr, client_id)
local line = lens.range.start.line
api.nvim_buf_clear_namespace(bufnr, namespaces[client_id], line, line + 1)
+ local client = vim.lsp.get_client_by_id(client_id)
+ assert(client, 'Client is required to execute lens, client_id=' .. client_id)
local command = lens.command
- local fn = vim.lsp.commands[command.command]
+ local fn = client.commands[command.command] or vim.lsp.commands[command.command]
if fn then
fn(command, { bufnr = bufnr, client_id = client_id })
return
end
-- Need to use the client that returned the lens → must not use buf_request
- local client = vim.lsp.get_client_by_id(client_id)
- assert(client, 'Client is required to execute lens, client_id=' .. client_id)
local command_provider = client.server_capabilities.executeCommandProvider
local commands = type(command_provider) == 'table' and command_provider.commands or {}
if not vim.tbl_contains(commands, command.command) then
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 0e63c0dd29..1e6f83c1ba 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -146,7 +146,8 @@ local _client_namespaces = {}
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)
+ local client = vim.lsp.get_client_by_id(client_id)
+ local name = string.format("vim.lsp.%s.%d", client and client.name or "unknown", client_id)
_client_namespaces[client_id] = vim.api.nvim_create_namespace(name)
end
return _client_namespaces[client_id]
@@ -551,14 +552,15 @@ end
---@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
+ opts = opts or {}
+ opts.scope = "cursor"
+ opts.pos = position
+ 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
- return vim.diagnostic.show_position_diagnostics(opts, buf_nr, position)
+ return vim.diagnostic.open_float(buf_nr, opts)
end
--- Open a floating window with the diagnostics from {line_nr}
@@ -573,11 +575,13 @@ end
---@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.scope = "line"
+ opts.pos = line_nr
if client_id then
- opts = opts or {}
opts.namespace = M.get_namespace(client_id)
end
- return vim.diagnostic.show_line_diagnostics(opts, buf_nr, line_nr)
+ return vim.diagnostic.open_float(buf_nr, opts)
end
--- Redraw diagnostics for the given buffer and client
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index eff27807be..a561630c2b 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -28,6 +28,7 @@ local function progress_handler(_, result, ctx, _)
local client_name = client and client.name or string.format("id=%d", client_id)
if not client then
err_message("LSP[", client_name, "] client has shut down after sending the message")
+ return
end
local val = result.value -- unspecified yet
local token = result.token -- string or number
@@ -185,7 +186,7 @@ local function response_to_list(map_result, entity)
title = 'Language Server';
items = map_result(result, ctx.bufnr);
})
- api.nvim_command("copen")
+ api.nvim_command("botright copen")
end
end
end
@@ -349,7 +350,7 @@ M['textDocument/signatureHelp'] = M.signature_help
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
M['textDocument/documentHighlight'] = function(_, result, ctx, _)
if not result then return end
- util.buf_highlight_references(ctx.bufnr, result)
+ util.buf_highlight_references(ctx.bufnr, result, ctx.client_id)
end
---@private
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index d9a684a738..bce1e9f35d 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -297,6 +297,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
local message_index = 0
local message_callbacks = {}
+ local notify_reply_callbacks = {}
local handle, pid
do
@@ -309,8 +310,9 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
stdout:close()
stderr:close()
handle:close()
- -- Make sure that message_callbacks can be gc'd.
+ -- Make sure that message_callbacks/notify_reply_callbacks can be gc'd.
message_callbacks = nil
+ notify_reply_callbacks = nil
dispatchers.on_exit(code, signal)
end
local spawn_params = {
@@ -375,10 +377,12 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
---@param method (string) The invoked LSP method
---@param params (table) Parameters for the invoked LSP method
---@param callback (function) Callback to invoke
+ ---@param notify_reply_callback (function) Callback to invoke as soon as a request is no longer pending
---@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not
- local function request(method, params, callback)
+ local function request(method, params, callback, notify_reply_callback)
validate {
callback = { callback, 'f' };
+ notify_reply_callback = { notify_reply_callback, 'f', true };
}
message_index = message_index + 1
local message_id = message_index
@@ -388,8 +392,15 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
method = method;
params = params;
}
- if result and message_callbacks then
- message_callbacks[message_id] = schedule_wrap(callback)
+ if result then
+ if message_callbacks then
+ message_callbacks[message_id] = schedule_wrap(callback)
+ else
+ return false
+ end
+ if notify_reply_callback and notify_reply_callbacks then
+ notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
+ end
return result, message_id
else
return false
@@ -466,6 +477,16 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
-- We sent a number, so we expect a number.
local result_id = tonumber(decoded.id)
+ -- Notify the user that a response was received for the request
+ local notify_reply_callback = notify_reply_callbacks and notify_reply_callbacks[result_id]
+ if notify_reply_callback then
+ validate {
+ notify_reply_callback = { notify_reply_callback, 'f' };
+ }
+ notify_reply_callback(result_id)
+ notify_reply_callbacks[result_id] = nil
+ end
+
-- Do not surface RequestCancelled to users, it is RPC-internal.
if decoded.error then
local mute_error = false
diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua
new file mode 100644
index 0000000000..37247c61b9
--- /dev/null
+++ b/runtime/lua/vim/lsp/sync.lua
@@ -0,0 +1,381 @@
+-- Notes on incremental sync:
+-- Per the protocol, the text range should be:
+--
+-- A position inside a document (see Position definition below) is expressed as
+-- a zero-based line and character offset. The offsets are based on a UTF-16
+-- string representation. So a string of the form a𐐀b the character offset
+-- of the character a is 0, the character offset of 𐐀 is 1 and the character
+-- offset of b is 3 since 𐐀 is represented using two code units in UTF-16.
+--
+-- To ensure that both client and server split the string into the same line
+-- representation the protocol specifies the following end-of-line sequences: ‘\n’, ‘\r\n’ and ‘\r’.
+--
+-- Positions are line end character agnostic. So you can not specify a position that
+-- denotes \r|\n or \n| where | represents the character offset. This means *no* defining
+-- a range than ends on the same line after a terminating character
+--
+-- Generic warnings about byte level changes in neovim. Many apparently "single"
+-- operations in on_lines callbacks are actually multiple operations.
+--
+-- Join operation (2 operations):
+-- * extends line 1 with the contents of line 2
+-- * deletes line 2
+--
+-- test 1 test 1 test 2 test 1 test 2
+-- test 2 -> test 2 -> test 3
+-- test 3 test 3
+--
+-- Deleting (and undoing) two middle lines (1 operation):
+--
+-- test 1 test 1
+-- test 2 -> test 4
+-- test 3
+-- test 4
+--
+-- Deleting partial lines (5 operations) deleting between asterisks below:
+--
+-- test *1 test * test * test * test *4 test *4*
+-- test 2 -> test 2 -> test *4 -> *4 -> *4 ->
+-- test 3 test 3
+-- test *4 test 4
+
+local M = {}
+
+-- local string.byte, unclear if this is necessary for JIT compilation
+local str_byte = string.byte
+local min = math.min
+local str_utfindex = vim.str_utfindex
+local str_utf_start = vim.str_utf_start
+local str_utf_end = vim.str_utf_end
+
+---@private
+-- Given a line, byte idx, and offset_encoding convert to the
+-- utf-8, utf-16, or utf-32 index.
+---@param line string the line to index into
+---@param byte integer the byte idx
+---@param offset_encoding string utf-8|utf-16|utf-32|nil (default: utf-8)
+--@returns integer the utf idx for the given encoding
+local function byte_to_utf(line, byte, offset_encoding)
+ -- convert to 0 based indexing for str_utfindex
+ byte = byte - 1
+
+ local utf_idx
+ local _
+ -- Convert the byte range to utf-{8,16,32} and convert 1-based (lua) indexing to 0-based
+ if offset_encoding == 'utf-16' then
+ _, utf_idx = str_utfindex(line, byte)
+ elseif offset_encoding == 'utf-32' then
+ utf_idx, _ = str_utfindex(line, byte)
+ else
+ utf_idx = byte
+ end
+
+ -- convert to 1 based indexing
+ return utf_idx + 1
+end
+
+---@private
+-- Given a line, byte idx, alignment, and offset_encoding convert to the aligned
+-- utf-8 index and either the utf-16, or utf-32 index.
+---@param line string the line to index into
+---@param byte integer the byte idx
+---@param align string when dealing with multibyte characters,
+-- to choose the start of the current character or the beginning of the next.
+-- Used for incremental sync for start/end range respectively
+---@param offset_encoding string utf-8|utf-16|utf-32|nil (default: utf-8)
+---@returns table<string, int> byte_idx and char_idx of first change position
+local function align_position(line, byte, align, offset_encoding)
+ local char
+ -- If on the first byte, or an empty string: the trivial case
+ if byte == 1 or #line == 0 then
+ char = byte
+ -- Called in the case of extending an empty line "" -> "a"
+ elseif byte == #line + 1 then
+ byte = byte + str_utf_end(line, #line)
+ char = byte_to_utf(line, byte, offset_encoding)
+ else
+ -- Modifying line, find the nearest utf codepoint
+ if align == 'start' then
+ byte = byte + str_utf_start(line, byte)
+ char = byte_to_utf(line, byte, offset_encoding)
+ elseif align == 'end' then
+ local offset = str_utf_end(line, byte)
+ -- If the byte does not fall on the start of the character, then
+ -- align to the start of the next character.
+ if offset > 0 then
+ char = byte_to_utf(line, byte, offset_encoding) + 1
+ byte = byte + offset
+ else
+ char = byte_to_utf(line, byte, offset_encoding)
+ byte = byte + offset
+ end
+ else
+ error('`align` must be start or end.')
+ end
+ -- Extending line, find the nearest utf codepoint for the last valid character
+ end
+ return byte, char
+end
+
+---@private
+--- Finds the first line, byte, and char index of the difference between the previous and current lines buffer normalized to the previous codepoint.
+---@param prev_lines table list of lines from previous buffer
+---@param curr_lines table list of lines from current buffer
+---@param firstline integer firstline from on_lines, adjusted to 1-index
+---@param lastline integer lastline from on_lines, adjusted to 1-index
+---@param new_lastline integer new_lastline from on_lines, adjusted to 1-index
+---@param offset_encoding string utf-8|utf-16|utf-32|nil (fallback to utf-8)
+---@returns table<int, int> line_idx, byte_idx, and char_idx of first change position
+local function compute_start_range(prev_lines, curr_lines, firstline, lastline, new_lastline, offset_encoding)
+ -- If firstline == lastline, no existing text is changed. All edit operations
+ -- occur on a new line pointed to by lastline. This occurs during insertion of
+ -- new lines(O), the new newline is inserted at the line indicated by
+ -- new_lastline.
+ -- If firstline == new_lastline, the first change occured on a line that was deleted.
+ -- In this case, the first byte change is also at the first byte of firstline
+ if firstline == new_lastline or firstline == lastline then
+ return { line_idx = firstline, byte_idx = 1, char_idx = 1 }
+ end
+
+ local prev_line = prev_lines[firstline]
+ local curr_line = curr_lines[firstline]
+
+ -- Iterate across previous and current line containing first change
+ -- to find the first different byte.
+ -- Note: *about -> a*about will register the second a as the first
+ -- difference, regardless of edit since we do not receive the first
+ -- column of the edit from on_lines.
+ local start_byte_idx = 1
+ for idx = 1, #prev_line + 1 do
+ start_byte_idx = idx
+ if str_byte(prev_line, idx) ~= str_byte(curr_line, idx) then
+ break
+ end
+ end
+
+ -- Convert byte to codepoint if applicable
+ local byte_idx, char_idx = align_position(prev_line, start_byte_idx, 'start', offset_encoding)
+
+ -- Return the start difference (shared for new and prev lines)
+ return { line_idx = firstline, byte_idx = byte_idx, char_idx = char_idx }
+end
+
+---@private
+--- Finds the last line and byte index of the differences between prev and current buffer.
+--- Normalized to the next codepoint.
+--- prev_end_range is the text range sent to the server representing the changed region.
+--- curr_end_range is the text that should be collected and sent to the server.
+--
+---@param prev_lines table list of lines
+---@param curr_lines table list of lines
+---@param start_range table
+---@param lastline integer
+---@param new_lastline integer
+---@param offset_encoding string
+---@returns (int, int) end_line_idx and end_col_idx of range
+local function compute_end_range(prev_lines, curr_lines, start_range, firstline, lastline, new_lastline, offset_encoding)
+ -- If firstline == new_lastline, the first change occured on a line that was deleted.
+ -- In this case, the last_byte...
+ if firstline == new_lastline then
+ return { line_idx = (lastline - new_lastline + firstline), byte_idx = 1, char_idx = 1 }, { line_idx = firstline, byte_idx = 1, char_idx = 1 }
+ end
+ if firstline == lastline then
+ return { line_idx = firstline, byte_idx = 1, char_idx = 1 }, { line_idx = new_lastline - lastline + firstline, byte_idx = 1, char_idx = 1 }
+ end
+ -- Compare on last line, at minimum will be the start range
+ local start_line_idx = start_range.line_idx
+
+ -- lastline and new_lastline were last lines that were *not* replaced, compare previous lines
+ local prev_line_idx = lastline - 1
+ local curr_line_idx = new_lastline - 1
+
+ local prev_line = prev_lines[lastline - 1]
+ local curr_line = curr_lines[new_lastline - 1]
+
+ local prev_line_length = #prev_line
+ local curr_line_length = #curr_line
+
+ local byte_offset = 0
+
+ -- Editing the same line
+ -- If the byte offset is zero, that means there is a difference on the last byte (not newline)
+ if prev_line_idx == curr_line_idx then
+ local max_length
+ if start_line_idx == prev_line_idx then
+ -- Search until beginning of difference
+ max_length = min(prev_line_length - start_range.byte_idx, curr_line_length - start_range.byte_idx) + 1
+ else
+ max_length = min(prev_line_length, curr_line_length) + 1
+ end
+ for idx = 0, max_length do
+ byte_offset = idx
+ if
+ str_byte(prev_line, prev_line_length - byte_offset) ~= str_byte(curr_line, curr_line_length - byte_offset)
+ then
+ break
+ end
+ end
+ end
+
+ -- Iterate from end to beginning of shortest line
+ local prev_end_byte_idx = prev_line_length - byte_offset + 1
+ local prev_byte_idx, prev_char_idx = align_position(prev_line, prev_end_byte_idx, 'start', offset_encoding)
+ local prev_end_range = { line_idx = prev_line_idx, byte_idx = prev_byte_idx, char_idx = prev_char_idx }
+
+ local curr_end_range
+ -- Deletion event, new_range cannot be before start
+ if curr_line_idx < start_line_idx then
+ curr_end_range = { line_idx = start_line_idx, byte_idx = 1, char_idx = 1 }
+ else
+ local curr_end_byte_idx = curr_line_length - byte_offset + 1
+ local curr_byte_idx, curr_char_idx = align_position(curr_line, curr_end_byte_idx, 'start', offset_encoding)
+ curr_end_range = { line_idx = curr_line_idx, byte_idx = curr_byte_idx, char_idx = curr_char_idx }
+ end
+
+ return prev_end_range, curr_end_range
+end
+
+---@private
+--- Get the text of the range defined by start and end line/column
+---@param lines table list of lines
+---@param start_range table table returned by first_difference
+---@param end_range table new_end_range returned by last_difference
+---@returns string text extracted from defined region
+local function extract_text(lines, start_range, end_range, line_ending)
+ if not lines[start_range.line_idx] then
+ return ""
+ end
+ -- Trivial case: start and end range are the same line, directly grab changed text
+ if start_range.line_idx == end_range.line_idx then
+ -- string.sub is inclusive, end_range is not
+ return string.sub(lines[start_range.line_idx], start_range.byte_idx, end_range.byte_idx - 1)
+
+ else
+ -- Handle deletion case
+ -- Collect the changed portion of the first changed line
+ local result = { string.sub(lines[start_range.line_idx], start_range.byte_idx) }
+
+ -- Collect the full line for intermediate lines
+ for idx = start_range.line_idx + 1, end_range.line_idx - 1 do
+ table.insert(result, lines[idx])
+ end
+
+ if lines[end_range.line_idx] then
+ -- Collect the changed portion of the last changed line.
+ table.insert(result, string.sub(lines[end_range.line_idx], 1, end_range.byte_idx - 1))
+ else
+ table.insert(result, "")
+ end
+
+ -- Add line ending between all lines
+ return table.concat(result, line_ending)
+ end
+end
+
+local function compute_line_length(line, offset_encoding)
+ local length
+ local _
+ if offset_encoding == 'utf-16' then
+ _, length = str_utfindex(line)
+ elseif offset_encoding == 'utf-32' then
+ length, _ = str_utfindex(line)
+ else
+ length = #line
+ end
+ return length
+end
+---@private
+-- rangelength depends on the offset encoding
+-- bytes for utf-8 (clangd with extenion)
+-- codepoints for utf-16
+-- codeunits for utf-32
+-- Line endings count here as 2 chars for \r\n (dos), 1 char for \n (unix), and 1 char for \r (mac)
+-- These correspond to Windows, Linux/macOS (OSX and newer), and macOS (version 9 and prior)
+local function compute_range_length(lines, start_range, end_range, offset_encoding, line_ending)
+ local line_ending_length = #line_ending
+ -- Single line case
+ if start_range.line_idx == end_range.line_idx then
+ return end_range.char_idx - start_range.char_idx
+ end
+
+ local start_line = lines[start_range.line_idx]
+ local range_length
+ if start_line and #start_line > 0 then
+ range_length = compute_line_length(start_line, offset_encoding) - start_range.char_idx + 1 + line_ending_length
+ else
+ -- Length of newline character
+ range_length = line_ending_length
+ end
+
+ -- The first and last range of the line idx may be partial lines
+ for idx = start_range.line_idx + 1, end_range.line_idx - 1 do
+ -- Length full line plus newline character
+ if #lines[idx] > 0 then
+ range_length = range_length + compute_line_length(lines[idx], offset_encoding) + #line_ending
+ else
+ range_length = range_length + line_ending_length
+ end
+ end
+
+ local end_line = lines[end_range.line_idx]
+ if end_line and #end_line > 0 then
+ range_length = range_length + end_range.char_idx - 1
+ end
+
+ return range_length
+end
+
+--- Returns the range table for the difference between prev and curr lines
+---@param prev_lines table list of lines
+---@param curr_lines table list of lines
+---@param firstline number line to begin search for first difference
+---@param lastline number line to begin search in old_lines for last difference
+---@param new_lastline number line to begin search in new_lines for last difference
+---@param offset_encoding string encoding requested by language server
+---@returns table TextDocumentContentChangeEvent see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentContentChangeEvent
+function M.compute_diff(prev_lines, curr_lines, firstline, lastline, new_lastline, offset_encoding, line_ending)
+ -- Find the start of changes between the previous and current buffer. Common between both.
+ -- Sent to the server as the start of the changed range.
+ -- Used to grab the changed text from the latest buffer.
+ local start_range = compute_start_range(
+ prev_lines,
+ curr_lines,
+ firstline + 1,
+ lastline + 1,
+ new_lastline + 1,
+ offset_encoding
+ )
+ -- Find the last position changed in the previous and current buffer.
+ -- prev_end_range is sent to the server as as the end of the changed range.
+ -- curr_end_range is used to grab the changed text from the latest buffer.
+ local prev_end_range, curr_end_range = compute_end_range(
+ prev_lines,
+ curr_lines,
+ start_range,
+ firstline + 1,
+ lastline + 1,
+ new_lastline + 1,
+ offset_encoding
+ )
+
+ -- Grab the changed text of from start_range to curr_end_range in the current buffer.
+ -- The text range is "" if entire range is deleted.
+ local text = extract_text(curr_lines, start_range, curr_end_range, line_ending)
+
+ -- Compute the range of the replaced text. Deprecated but still required for certain language servers
+ local range_length = compute_range_length(prev_lines, start_range, prev_end_range, offset_encoding, line_ending)
+
+ -- convert to 0 based indexing
+ local result = {
+ range = {
+ ['start'] = { line = start_range.line_idx - 1, character = start_range.char_idx - 1 },
+ ['end'] = { line = prev_end_range.line_idx - 1, character = prev_end_range.char_idx - 1 },
+ },
+ text = text,
+ rangeLength = range_length,
+ }
+
+ return result
+end
+
+return M
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 952926b67e..a4b7b9922b 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -151,7 +151,7 @@ end
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
--- Returns a zero-indexed column, since set_lines() does the conversion to
--- 1-indexed
-local function get_line_byte_from_position(bufnr, position)
+local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- LSP's line and characters are 0-indexed
-- Vim's line and columns are 1-indexed
local col = position.character
@@ -165,7 +165,13 @@ local function get_line_byte_from_position(bufnr, position)
local line = position.line
local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false)
if #lines > 0 then
- local ok, result = pcall(vim.str_byteindex, lines[1], col)
+ local ok, result
+
+ if offset_encoding == "utf-16" or not offset_encoding then
+ ok, result = pcall(vim.str_byteindex, lines[1], col, true)
+ elseif offset_encoding == "utf-32" then
+ ok, result = pcall(vim.str_byteindex, lines[1], col, false)
+ end
if ok then
return result
@@ -226,9 +232,10 @@ function M.get_progress_messages()
table.remove(client.messages, item.idx)
end
- for _, item in ipairs(progress_remove) do
- client.messages.progress[item.token] = nil
- end
+ end
+
+ for _, item in ipairs(progress_remove) do
+ item.client.messages.progress[item.token] = nil
end
return new_messages
@@ -275,7 +282,8 @@ function M.apply_text_edits(text_edits, bufnr)
-- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
local has_eol_text_edit = false
local max = vim.api.nvim_buf_line_count(bufnr)
- local len = vim.str_utfindex(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '')
+ -- TODO handle offset_encoding
+ local _, len = vim.str_utfindex(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '')
text_edits = vim.tbl_map(function(text_edit)
if max <= text_edit.range.start.line then
text_edit.range.start.line = max - 1
@@ -359,177 +367,6 @@ end
-- function M.glob_to_regex(glob)
-- end
----@private
---- Finds the first line and column of the difference between old and new lines
----@param old_lines table list of lines
----@param new_lines table list of lines
----@returns (int, int) start_line_idx and start_col_idx of range
-local function first_difference(old_lines, new_lines, start_line_idx)
- local line_count = math.min(#old_lines, #new_lines)
- if line_count == 0 then return 1, 1 end
- if not start_line_idx then
- for i = 1, line_count do
- start_line_idx = i
- if old_lines[start_line_idx] ~= new_lines[start_line_idx] then
- break
- end
- end
- end
- local old_line = old_lines[start_line_idx]
- local new_line = new_lines[start_line_idx]
- local length = math.min(#old_line, #new_line)
- local start_col_idx = 1
- while start_col_idx <= length do
- if string.sub(old_line, start_col_idx, start_col_idx) ~= string.sub(new_line, start_col_idx, start_col_idx) then
- break
- end
- start_col_idx = start_col_idx + 1
- end
- return start_line_idx, start_col_idx
-end
-
-
----@private
---- Finds the last line and column of the differences between old and new lines
----@param old_lines table list of lines
----@param new_lines table list of lines
----@param start_char integer First different character idx of range
----@returns (int, int) end_line_idx and end_col_idx of range
-local function last_difference(old_lines, new_lines, start_char, end_line_idx)
- local line_count = math.min(#old_lines, #new_lines)
- if line_count == 0 then return 0,0 end
- if not end_line_idx then
- end_line_idx = -1
- end
- for i = end_line_idx, -line_count, -1 do
- if old_lines[#old_lines + i + 1] ~= new_lines[#new_lines + i + 1] then
- end_line_idx = i
- break
- end
- end
- local old_line
- local new_line
- if end_line_idx <= -line_count then
- end_line_idx = -line_count
- old_line = string.sub(old_lines[#old_lines + end_line_idx + 1], start_char)
- new_line = string.sub(new_lines[#new_lines + end_line_idx + 1], start_char)
- else
- old_line = old_lines[#old_lines + end_line_idx + 1]
- new_line = new_lines[#new_lines + end_line_idx + 1]
- end
- local old_line_length = #old_line
- local new_line_length = #new_line
- local length = math.min(old_line_length, new_line_length)
- local end_col_idx = -1
- while end_col_idx >= -length do
- local old_char = string.sub(old_line, old_line_length + end_col_idx + 1, old_line_length + end_col_idx + 1)
- local new_char = string.sub(new_line, new_line_length + end_col_idx + 1, new_line_length + end_col_idx + 1)
- if old_char ~= new_char then
- break
- end
- end_col_idx = end_col_idx - 1
- end
- return end_line_idx, end_col_idx
-
-end
-
----@private
---- Get the text of the range defined by start and end line/column
----@param lines table list of lines
----@param start_char integer First different character idx of range
----@param end_char integer Last different character idx of range
----@param start_line integer First different line idx of range
----@param end_line integer Last different line idx of range
----@returns string text extracted from defined region
-local function extract_text(lines, start_line, start_char, end_line, end_char)
- if start_line == #lines + end_line + 1 then
- if end_line == 0 then return '' end
- local line = lines[start_line]
- local length = #line + end_char - start_char
- return string.sub(line, start_char, start_char + length + 1)
- end
- local result = string.sub(lines[start_line], start_char) .. '\n'
- for line_idx = start_line + 1, #lines + end_line do
- result = result .. lines[line_idx] .. '\n'
- end
- if end_line ~= 0 then
- local line = lines[#lines + end_line + 1]
- local length = #line + end_char + 1
- result = result .. string.sub(line, 1, length)
- end
- return result
-end
-
----@private
---- Compute the length of the substituted range
----@param lines table list of lines
----@param start_char integer First different character idx of range
----@param end_char integer Last different character idx of range
----@param start_line integer First different line idx of range
----@param end_line integer Last different line idx of range
----@returns (int, int) end_line_idx and end_col_idx of range
-local function compute_length(lines, start_line, start_char, end_line, end_char)
- local adj_end_line = #lines + end_line + 1
- local adj_end_char
- if adj_end_line > #lines then
- adj_end_char = end_char - 1
- else
- adj_end_char = #lines[adj_end_line] + end_char
- end
- if start_line == adj_end_line then
- return adj_end_char - start_char + 1
- end
- local result = #lines[start_line] - start_char + 1
- for line = start_line + 1, adj_end_line -1 do
- result = result + #lines[line] + 1
- end
- result = result + adj_end_char + 1
- return result
-end
-
---- Returns the range table for the difference between old and new lines
----@param old_lines table list of lines
----@param new_lines table list of lines
----@param start_line_idx int line to begin search for first difference
----@param end_line_idx int line to begin search for last difference
----@param offset_encoding string encoding requested by language server
----@returns table start_line_idx and start_col_idx of range
-function M.compute_diff(old_lines, new_lines, start_line_idx, end_line_idx, offset_encoding)
- local start_line, start_char = first_difference(old_lines, new_lines, start_line_idx)
- local end_line, end_char = last_difference(vim.list_slice(old_lines, start_line, #old_lines),
- vim.list_slice(new_lines, start_line, #new_lines), start_char, end_line_idx)
- local text = extract_text(new_lines, start_line, start_char, end_line, end_char)
- local length = compute_length(old_lines, start_line, start_char, end_line, end_char)
-
- local adj_end_line = #old_lines + end_line
- local adj_end_char
- if end_line == 0 then
- adj_end_char = 0
- else
- adj_end_char = #old_lines[#old_lines + end_line + 1] + end_char + 1
- end
-
- local _
- if offset_encoding == "utf-16" then
- _, start_char = vim.str_utfindex(old_lines[start_line], start_char - 1)
- _, end_char = vim.str_utfindex(old_lines[#old_lines + end_line + 1], adj_end_char)
- else
- start_char = start_char - 1
- end_char = adj_end_char
- end
-
- local result = {
- range = {
- start = { line = start_line - 1, character = start_char},
- ["end"] = { line = adj_end_line, character = end_char}
- },
- text = text,
- rangeLength = length + 1,
- }
-
- return result
-end
-
--- Can be used to extract the completion items from a
--- `textDocument/completion` request, which may return one of
--- `CompletionItem[]`, `CompletionList` or null.
@@ -712,18 +549,29 @@ end
-- ignoreIfExists? bool
function M.rename(old_fname, new_fname, opts)
opts = opts or {}
- local bufnr = vim.fn.bufadd(old_fname)
- vim.fn.bufload(bufnr)
local target_exists = vim.loop.fs_stat(new_fname) ~= nil
if target_exists and not opts.overwrite or opts.ignoreIfExists then
vim.notify('Rename target already exists. Skipping rename.')
return
end
+ local oldbuf = vim.fn.bufadd(old_fname)
+ vim.fn.bufload(oldbuf)
+
+ -- The there may be pending changes in the buffer
+ api.nvim_buf_call(oldbuf, function()
+ vim.cmd('w!')
+ end)
+
local ok, err = os.rename(old_fname, new_fname)
assert(ok, err)
- api.nvim_buf_call(bufnr, function()
- vim.cmd('saveas! ' .. vim.fn.fnameescape(new_fname))
- end)
+
+ local newbuf = vim.fn.bufadd(new_fname)
+ for _, win in pairs(api.nvim_list_wins()) do
+ if api.nvim_win_get_buf(win) == oldbuf then
+ api.nvim_win_set_buf(win, newbuf)
+ end
+ end
+ api.nvim_buf_delete(oldbuf, { force = true })
end
@@ -1494,18 +1342,30 @@ do --[[ References ]]
---@param bufnr buffer id
---@param references List of `DocumentHighlight` objects to highlight
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight
- function M.buf_highlight_references(bufnr, references)
+ function M.buf_highlight_references(bufnr, references, client_id)
validate { bufnr = {bufnr, 'n', true} }
+ local client = vim.lsp.get_client_by_id(client_id)
+ if not client then
+ return
+ end
for _, reference in ipairs(references) do
- local start_pos = {reference["range"]["start"]["line"], reference["range"]["start"]["character"]}
- local end_pos = {reference["range"]["end"]["line"], reference["range"]["end"]["character"]}
+ local start_line, start_char = reference["range"]["start"]["line"], reference["range"]["start"]["character"]
+ local end_line, end_char = reference["range"]["end"]["line"], reference["range"]["end"]["character"]
+
+ local start_idx = get_line_byte_from_position(bufnr, { line = start_line, character = start_char }, client.offset_encoding)
+ local end_idx = get_line_byte_from_position(bufnr, { line = start_line, character = end_char }, client.offset_encoding)
+
local document_highlight_kind = {
[protocol.DocumentHighlightKind.Text] = "LspReferenceText";
[protocol.DocumentHighlightKind.Read] = "LspReferenceRead";
[protocol.DocumentHighlightKind.Write] = "LspReferenceWrite";
}
local kind = reference["kind"] or protocol.DocumentHighlightKind.Text
- highlight.range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos)
+ highlight.range(bufnr,
+ reference_ns,
+ document_highlight_kind[kind],
+ { start_line, start_idx },
+ { end_line, end_idx })
end
end
end
@@ -1719,7 +1579,9 @@ function M.symbols_to_items(symbols, bufnr)
})
if symbol.children then
for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do
- vim.list_extend(_items, v)
+ for _, s in ipairs(v) do
+ table.insert(_items, s)
+ end
end
end
end
@@ -1787,7 +1649,9 @@ local function make_position_param()
if not line then
return { line = 0; character = 0; }
end
- col = str_utfindex(line, col)
+ -- TODO handle offset_encoding
+ local _
+ _, col = str_utfindex(line, col)
return { line = row; character = col; }
end
@@ -1837,11 +1701,14 @@ function M.make_given_range_params(start_pos, end_pos)
A[1] = A[1] - 1
B[1] = B[1] - 1
-- account for encoding.
+ -- TODO handle offset_encoding
if A[2] > 0 then
- A = {A[1], M.character_offset(0, A[1], A[2])}
+ local _, char = M.character_offset(0, A[1], A[2])
+ A = {A[1], char}
end
if B[2] > 0 then
- B = {B[1], M.character_offset(0, B[1], B[2])}
+ local _, char = M.character_offset(0, B[1], B[2])
+ B = {B[1], char}
end
-- we need to offset the end character position otherwise we loose the last
-- character of the selection, as LSP end position is exclusive
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index b57b7ad4ad..6e40b6ca52 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -605,7 +605,7 @@ do
function vim.validate(opt)
local ok, err_msg = is_valid(opt)
if not ok then
- error(debug.traceback(err_msg, 2), 2)
+ error(err_msg, 2)
end
end
end
diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua
index 5eab20fc54..9568b60fd0 100644
--- a/runtime/lua/vim/ui.lua
+++ b/runtime/lua/vim/ui.lua
@@ -9,6 +9,11 @@ local M = {}
--- - format_item (function item -> text)
--- Function to format an
--- individual item from `items`. Defaults to `tostring`.
+--- - kind (string|nil)
+--- Arbitrary hint string indicating the item shape.
+--- Plugins reimplementing `vim.ui.select` may wish to
+--- use this to infer the structure or semantics of
+--- `items`, or the context in which select() was called.
---@param on_choice function ((item|nil, idx|nil) -> ())
--- Called once the user made a choice.
--- `idx` is the 1-based index of `item` within `item`.
@@ -32,5 +37,38 @@ function M.select(items, opts, on_choice)
end
end
+--- Prompts the user for input
+---
+---@param opts table Additional options. See |input()|
+--- - prompt (string|nil)
+--- Text of the prompt. Defaults to `Input: `.
+--- - default (string|nil)
+--- Default reply to the input
+--- - completion (string|nil)
+--- Specifies type of completion supported
+--- for input. Supported types are the same
+--- that can be supplied to a user-defined
+--- command using the "-complete=" argument.
+--- See |:command-completion|
+--- - highlight (function)
+--- Function that will be used for highlighting
+--- user inputs.
+---@param on_confirm function ((input|nil) -> ())
+--- Called once the user confirms or abort the input.
+--- `input` is what the user typed.
+--- `nil` if the user aborted the dialog.
+function M.input(opts, on_confirm)
+ vim.validate {
+ on_confirm = { on_confirm, 'function', false },
+ }
+
+ opts = opts or {}
+ local input = vim.fn.input(opts)
+ if #input > 0 then
+ on_confirm(input)
+ else
+ on_confirm(nil)
+ end
+end
return M