aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/diagnostic.lua66
-rw-r--r--runtime/lua/vim/lsp.lua2
-rw-r--r--runtime/lua/vim/lsp/buf.lua125
-rw-r--r--runtime/lua/vim/lsp/codelens.lua16
-rw-r--r--runtime/lua/vim/lsp/handlers.lua46
-rw-r--r--runtime/lua/vim/lsp/protocol.lua4
-rw-r--r--runtime/lua/vim/lsp/rpc.lua46
-rw-r--r--runtime/lua/vim/lsp/util.lua11
-rw-r--r--runtime/lua/vim/ui.lua36
9 files changed, 230 insertions, 122 deletions
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua
index 3f41ee5df8..c7c8c1878e 100644
--- a/runtime/lua/vim/diagnostic.lua
+++ b/runtime/lua/vim/diagnostic.lua
@@ -556,6 +556,9 @@ end
--- - signs: (default true) Use signs for diagnostics. Options:
--- * severity: Only show signs for diagnostics matching the given severity
--- |diagnostic-severity|
+--- * 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.
--- - 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
@@ -617,23 +620,22 @@ function M.set(namespace, bufnr, diagnostics, opts)
}
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
- })
+ clear_diagnostic_cache(namespace, bufnr)
+ else
+ 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, bufnr, diagnostics)
end
- set_diagnostic_cache(namespace, bufnr, diagnostics)
-
if vim.api.nvim_buf_is_loaded(bufnr) then
M.show(namespace, bufnr, diagnostics, opts)
elseif opts then
@@ -643,6 +645,13 @@ function M.set(namespace, bufnr, diagnostics, opts)
vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged")
end
+--- Get current diagnostic namespaces.
+---
+---@return table A list of active diagnostic namespaces |vim.diagnostic|.
+function M.get_namespaces()
+ return vim.deepcopy(all_namespaces)
+end
+
--- Get current diagnostics.
---
---@param bufnr number|nil Buffer number to get diagnostics from. Use 0 for
@@ -806,16 +815,35 @@ function M._set_signs(namespace, bufnr, diagnostics, opts)
}
bufnr = get_bufnr(bufnr)
- opts = get_resolved_options({ signs = opts }, namespace, bufnr).signs
+ opts = get_resolved_options({ signs = opts }, namespace, bufnr)
- if opts and opts.severity then
- diagnostics = filter_by_severity(opts.severity, diagnostics)
+ if opts.signs and opts.signs.severity then
+ diagnostics = filter_by_severity(opts.signs.severity, diagnostics)
end
local ns = get_namespace(namespace)
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)
+ end
+ else
+ get_priority = function(severity)
+ return priority + (vim.diagnostic.severity.HINT - severity)
+ end
+ end
+ else
+ get_priority = function()
+ return priority
+ end
+ end
+
for _, diagnostic in ipairs(diagnostics) do
vim.fn.sign_place(
0,
@@ -823,7 +851,7 @@ function M._set_signs(namespace, bufnr, diagnostics, opts)
sign_highlight_map[diagnostic.severity],
bufnr,
{
- priority = opts and opts.priority,
+ priority = get_priority(diagnostic.severity),
lnum = diagnostic.lnum + 1
}
)
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index ae9a7ab513..c7a88a0993 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -830,7 +830,7 @@ function lsp.start_client(config)
rpc.request('initialize', initialize_params, function(init_err, result)
assert(not init_err, tostring(init_err))
assert(result, "server sent empty result")
- rpc.notify('initialized', {[vim.type_idx]=vim.types.dictionary})
+ rpc.notify('initialized', vim.empty_dict())
client.initialized = true
uninitialized_clients[client_id] = nil
client.workspaceFolders = initialize_params.workspaceFolders
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 054f7aee04..245f29943e 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -289,7 +289,6 @@ function M.references(context)
params.context = context or {
includeDeclaration = true;
}
- params[vim.type_idx] = vim.types.dictionary
request('textDocument/references', params)
end
@@ -451,6 +450,93 @@ function M.clear_references()
util.buf_clear_references()
end
+
+---@private
+--
+--- This is not public because the main extension point is
+--- vim.ui.select which can be overridden independently.
+---
+--- Can't call/use vim.lsp.handlers['textDocument/codeAction'] because it expects
+--- `(err, CodeAction[] | Command[], ctx)`, but we want to aggregate the results
+--- from multiple clients to have 1 single UI prompt for the user, yet we still
+--- need to be able to link a `CodeAction|Command` to the right client for
+--- `codeAction/resolve`
+local function on_code_action_results(results, ctx)
+ local action_tuples = {}
+ for client_id, result in pairs(results) do
+ for _, action in pairs(result.result or {}) do
+ table.insert(action_tuples, { client_id, action })
+ end
+ end
+ if #action_tuples == 0 then
+ vim.notify('No code actions available', vim.log.levels.INFO)
+ return
+ end
+
+ ---@private
+ local function apply_action(action, client)
+ if action.edit then
+ util.apply_workspace_edit(action.edit)
+ end
+ if action.command then
+ local command = type(action.command) == 'table' and action.command or action
+ local fn = vim.lsp.commands[command.command]
+ if fn then
+ local enriched_ctx = vim.deepcopy(ctx)
+ enriched_ctx.client_id = client.id
+ fn(command, ctx)
+ else
+ M.execute_command(command)
+ end
+ end
+ end
+
+ ---@private
+ local function on_user_choice(action_tuple)
+ if not action_tuple then
+ return
+ end
+ -- textDocument/codeAction can return either Command[] or CodeAction[]
+ --
+ -- CodeAction
+ -- ...
+ -- edit?: WorkspaceEdit -- <- must be applied before command
+ -- command?: Command
+ --
+ -- Command:
+ -- title: string
+ -- command: string
+ -- arguments?: any[]
+ --
+ local client = vim.lsp.get_client_by_id(action_tuple[1])
+ local action = action_tuple[2]
+ if not action.edit
+ and client
+ and type(client.resolved_capabilities.code_action) == 'table'
+ and client.resolved_capabilities.code_action.resolveProvider then
+
+ client.request('codeAction/resolve', action, function(err, resolved_action)
+ if err then
+ vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
+ return
+ end
+ apply_action(resolved_action, client)
+ end)
+ else
+ apply_action(action, client)
+ end
+ end
+
+ vim.ui.select(action_tuples, {
+ prompt = 'Code actions:',
+ format_item = function(action_tuple)
+ local title = action_tuple[2].title:gsub('\r\n', '\\r\\n')
+ return title:gsub('\n', '\\n')
+ end,
+ }, on_user_choice)
+end
+
+
--- Requests code actions from all clients and calls the handler exactly once
--- with all aggregated results
---@private
@@ -458,22 +544,28 @@ local function code_action_request(params)
local bufnr = vim.api.nvim_get_current_buf()
local method = 'textDocument/codeAction'
vim.lsp.buf_request_all(bufnr, method, params, function(results)
- local actions = {}
- for _, r in pairs(results) do
- vim.list_extend(actions, r.result or {})
- end
- vim.lsp.handlers[method](nil, actions, {bufnr=bufnr, method=method})
+ on_code_action_results(results, { bufnr = bufnr, method = method, params = params })
end)
end
---- Selects a code action from the input list that is available at the current
+--- Selects a code action available at the current
--- cursor position.
---
----@param context: (table, optional) Valid `CodeActionContext` object
+---@param context table|nil `CodeActionContext` of the LSP specification:
+--- - diagnostics: (table|nil)
+--- LSP `Diagnostic[]`. Inferred from the current
+--- position if not provided.
+--- - only: (string|nil)
+--- LSP `CodeActionKind` used to filter the code actions.
+--- Most language servers support values like `refactor`
+--- or `quickfix`.
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
function M.code_action(context)
validate { context = { context, 't', true } }
- context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
+ context = context or {}
+ if not context.diagnostics then
+ context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
+ end
local params = util.make_range_params()
params.context = context
code_action_request(params)
@@ -481,14 +573,25 @@ end
--- Performs |vim.lsp.buf.code_action()| for a given range.
---
----@param context: (table, optional) Valid `CodeActionContext` object
+---
+---@param context table|nil `CodeActionContext` of the LSP specification:
+--- - diagnostics: (table|nil)
+--- LSP `Diagnostic[]`. Inferred from the current
+--- position if not provided.
+--- - only: (string|nil)
+--- LSP `CodeActionKind` used to filter the code actions.
+--- Most language servers support values like `refactor`
+--- or `quickfix`.
---@param start_pos ({number, number}, optional) mark-indexed position.
---Defaults to the start of the last visual selection.
---@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
function M.range_code_action(context, start_pos, end_pos)
validate { context = { context, 't', true } }
- context = context or { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
+ context = context or {}
+ if not context.diagnostics then
+ context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
+ end
local params = util.make_given_range_params(start_pos, end_pos)
params.context = context
code_action_request(params)
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index 9cedb2f1db..20b203fe99 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -31,10 +31,24 @@ local function execute_lens(lens, bufnr, client_id)
local line = lens.range.start.line
api.nvim_buf_clear_namespace(bufnr, namespaces[client_id], line, line + 1)
+ local command = lens.command
+ local fn = vim.lsp.commands[command.command]
+ if fn then
+ fn(command, { bufnr = bufnr, client_id = client_id })
+ return
+ end
-- Need to use the client that returned the lens → must not use buf_request
local client = vim.lsp.get_client_by_id(client_id)
assert(client, 'Client is required to execute lens, client_id=' .. client_id)
- client.request('workspace/executeCommand', lens.command, function(...)
+ local command_provider = client.server_capabilities.executeCommandProvider
+ local commands = type(command_provider) == 'table' and command_provider.commands or {}
+ if not vim.tbl_contains(commands, command.command) then
+ vim.notify(string.format(
+ "Language server does not support command `%s`. This command may require a client extension.", command.command),
+ vim.log.levels.WARN)
+ return
+ end
+ client.request('workspace/executeCommand', command, function(...)
local result = vim.lsp.handlers['workspace/executeCommand'](...)
M.refresh()
return result
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 624f8b5462..eff27807be 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -3,7 +3,6 @@ local protocol = require 'vim.lsp.protocol'
local util = require 'vim.lsp.util'
local vim = vim
local api = vim.api
-local buf = require 'vim.lsp.buf'
local M = {}
@@ -109,51 +108,6 @@ M['client/registerCapability'] = function(_, _, ctx)
return vim.NIL
end
---see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
-M['textDocument/codeAction'] = function(_, result, ctx)
- if result == nil or vim.tbl_isempty(result) then
- print("No code actions available")
- return
- end
-
- local option_strings = {"Code actions:"}
- for i, action in ipairs(result) do
- local title = action.title:gsub('\r\n', '\\r\\n')
- title = title:gsub('\n', '\\n')
- table.insert(option_strings, string.format("%d. %s", i, title))
- end
-
- local choice = vim.fn.inputlist(option_strings)
- if choice < 1 or choice > #result then
- return
- end
- local action = result[choice]
- -- textDocument/codeAction can return either Command[] or CodeAction[]
- --
- -- CodeAction
- -- ...
- -- edit?: WorkspaceEdit -- <- must be applied before command
- -- command?: Command
- --
- -- Command:
- -- title: string
- -- command: string
- -- arguments?: any[]
- --
- if action.edit then
- util.apply_workspace_edit(action.edit)
- end
- if action.command then
- local command = type(action.command) == 'table' and action.command or action
- local fn = vim.lsp.commands[command.command]
- if fn then
- fn(command, ctx)
- else
- buf.execute_command(command)
- end
- end
-end
-
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
M['workspace/applyEdit'] = function(_, workspace_edit)
if not workspace_edit then return end
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 27703b4503..b3aa8b934f 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -645,6 +645,10 @@ function protocol.make_client_capabilities()
end)();
};
};
+ dataSupport = true;
+ resolveSupport = {
+ properties = { 'edit', }
+ };
};
completion = {
dynamicRegistration = false;
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 7f31bbdf75..255eb65dfe 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -4,34 +4,6 @@ local log = require('vim.lsp.log')
local protocol = require('vim.lsp.protocol')
local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap
--- TODO replace with a better implementation.
----@private
---- Encodes to JSON.
----
----@param data (table) Data to encode
----@returns (string) Encoded object
-local function json_encode(data)
- local status, result = pcall(vim.fn.json_encode, data)
- if status then
- return result
- else
- return nil, result
- end
-end
----@private
---- Decodes from JSON.
----
----@param data (string) Data to decode
----@returns (table) Decoded JSON object
-local function json_decode(data)
- local status, result = pcall(vim.fn.json_decode, data)
- if status then
- return result
- else
- return nil, result
- end
-end
-
---@private
--- Checks whether a given path exists and is a directory.
---@param filename (string) path to check
@@ -389,16 +361,13 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
--- Encodes {payload} into a JSON-RPC message and sends it to the remote
--- process.
---
- ---@param payload (table) Converted into a JSON string, see |json_encode()|
+ ---@param payload table
---@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing.
local function encode_and_send(payload)
local _ = log.debug() and log.debug("rpc.send", payload)
if handle == nil or handle:is_closing() then return false end
- -- TODO(ashkan) remove this once we have a Lua json_encode
- schedule(function()
- local encoded = assert(json_encode(payload))
- stdin:write(format_message_with_content_length(encoded))
- end)
+ local encoded = vim.json.encode(payload)
+ stdin:write(format_message_with_content_length(encoded))
return true
end
@@ -488,14 +457,15 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
---@private
local function handle_body(body)
- local decoded, err = json_decode(body)
- if not decoded then
- -- on_error(client_errors.INVALID_SERVER_JSON, err)
+ local ok, decoded = pcall(vim.json.decode, body)
+ if not ok then
+ on_error(client_errors.INVALID_SERVER_JSON, decoded)
return
end
local _ = log.debug() and log.debug("rpc.receive", decoded)
if type(decoded.method) == 'string' and decoded.id then
+ local err
-- Server Request
decoded.params = convert_NIL(decoded.params)
-- Schedule here so that the users functions don't trigger an error and
@@ -582,8 +552,6 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
end
end
- -- TODO(ashkan) remove this once we have a Lua json_decode
- handle_body = schedule_wrap(handle_body)
local request_parser = coroutine.wrap(request_parser_loop)
request_parser()
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index e95f170427..fca956fb57 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -951,6 +951,11 @@ end
---@param width (number) window width (in character cells)
---@param height (number) window height (in character cells)
---@param opts (table, optional)
+--- - offset_x (number) offset to add to `col`
+--- - offset_y (number) offset to add to `row`
+--- - border (string or table) override `border`
+--- - focusable (string or table) override `focusable`
+--- - zindex (string or table) override `zindex`, defaults to 50
---@returns (table) Options
function M.make_floating_popup_options(width, height, opts)
validate {
@@ -975,7 +980,7 @@ function M.make_floating_popup_options(width, height, opts)
else
anchor = anchor..'S'
height = math.min(lines_above, height)
- row = -get_border_size(opts).height
+ row = 0
end
if vim.fn.wincol() + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then
@@ -1124,8 +1129,6 @@ end
--- - wrap_at character to wrap at for computing height
--- - max_width maximal width of floating window
--- - max_height maximal height of floating window
---- - pad_left number of columns to pad contents at left
---- - pad_right number of columns to pad contents at right
--- - pad_top number of lines to pad contents at top
--- - pad_bottom number of lines to pad contents at bottom
--- - separator insert separator after code block
@@ -1376,8 +1379,6 @@ end
--- - wrap_at character to wrap at for computing height when wrap is enabled
--- - max_width maximal width of floating window
--- - max_height maximal height of floating window
---- - pad_left number of columns to pad contents at left
---- - pad_right number of columns to pad contents at right
--- - pad_top number of lines to pad contents at top
--- - pad_bottom number of lines to pad contents at bottom
--- - focus_id if a popup with this id is opened, then focus it
diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua
new file mode 100644
index 0000000000..5eab20fc54
--- /dev/null
+++ b/runtime/lua/vim/ui.lua
@@ -0,0 +1,36 @@
+local M = {}
+
+--- Prompts the user to pick a single item from a collection of entries
+---
+---@param items table Arbitrary items
+---@param opts table Additional options
+--- - prompt (string|nil)
+--- Text of the prompt. Defaults to `Select one of:`
+--- - format_item (function item -> text)
+--- Function to format an
+--- individual item from `items`. Defaults to `tostring`.
+---@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`.
+--- `nil` if the user aborted the dialog.
+function M.select(items, opts, on_choice)
+ vim.validate {
+ items = { items, 'table', false },
+ on_choice = { on_choice, 'function', false },
+ }
+ opts = opts or {}
+ local choices = {opts.prompt or 'Select one of:'}
+ local format_item = opts.format_item or tostring
+ for i, item in pairs(items) do
+ table.insert(choices, string.format('%d: %s', i, format_item(item)))
+ end
+ local choice = vim.fn.inputlist(choices)
+ if choice < 1 or choice > #items then
+ on_choice(nil, nil)
+ else
+ on_choice(items[choice], choice)
+ end
+end
+
+
+return M