mirror of
https://github.com/neovim/neovim.git
synced 2024-12-24 21:25:04 -07:00
feat(lsp)!: add vim.lsp.status, client.progress and promote LspProgressUpdate (#23958)
`client.messages` could grow unbounded because the default handler only added new messages, never removing them. A user either had to consume the messages by calling `vim.lsp.util.get_progress_messages` or by manually removing them from `client.messages.progress`. If they didn't do that, using LSP effectively leaked memory. To fix this, this deprecates the `messages` property and instead adds a `progress` ring buffer that only keeps at most 50 messages. In addition it deprecates `vim.lsp.util.get_progress_messages` in favour of a new `vim.lsp.status()` and also promotes the `LspProgressUpdate` user autocmd to a regular autocmd to allow users to pattern match on the progress kind. Also closes https://github.com/neovim/neovim/pull/20327
This commit is contained in:
parent
f31dba93f9
commit
e5e0bda41b
@ -117,19 +117,21 @@ internally and are no longer exposed as part of the API. Instead, use
|
|||||||
- *vim.lsp.diagnostic.set_virtual_text()*
|
- *vim.lsp.diagnostic.set_virtual_text()*
|
||||||
|
|
||||||
LSP FUNCTIONS
|
LSP FUNCTIONS
|
||||||
- *vim.lsp.buf.range_code_action()* Use |vim.lsp.buf.code_action()| with
|
- *vim.lsp.buf.range_code_action()* Use |vim.lsp.buf.code_action()| with
|
||||||
the `range` parameter.
|
the `range` parameter.
|
||||||
- *vim.lsp.util.diagnostics_to_items()* Use |vim.diagnostic.toqflist()| instead.
|
- *vim.lsp.util.diagnostics_to_items()* Use |vim.diagnostic.toqflist()| instead.
|
||||||
- *vim.lsp.util.set_qflist()* Use |setqflist()| instead.
|
- *vim.lsp.util.set_qflist()* Use |setqflist()| instead.
|
||||||
- *vim.lsp.util.set_loclist()* Use |setloclist()| instead.
|
- *vim.lsp.util.set_loclist()* Use |setloclist()| instead.
|
||||||
- *vim.lsp.buf_get_clients()* Use |vim.lsp.get_active_clients()| with
|
- *vim.lsp.buf_get_clients()* Use |vim.lsp.get_active_clients()| with
|
||||||
{buffer = bufnr} instead.
|
{buffer = bufnr} instead.
|
||||||
- *vim.lsp.buf.formatting()* Use |vim.lsp.buf.format()| with
|
- *vim.lsp.buf.formatting()* Use |vim.lsp.buf.format()| with
|
||||||
{async = true} instead.
|
{async = true} instead.
|
||||||
- *vim.lsp.buf.formatting_sync()* Use |vim.lsp.buf.format()| with
|
- *vim.lsp.buf.formatting_sync()* Use |vim.lsp.buf.format()| with
|
||||||
{async = false} instead.
|
{async = false} instead.
|
||||||
- *vim.lsp.buf.range_formatting()* Use |vim.lsp.formatexpr()|
|
- *vim.lsp.buf.range_formatting()* Use |vim.lsp.formatexpr()|
|
||||||
or |vim.lsp.buf.format()| instead.
|
or |vim.lsp.buf.format()| instead.
|
||||||
|
- *vim.lsp.util.get_progress_messages()* Use |vim.lsp.status()| or access
|
||||||
|
`progress` of |vim.lsp.client|
|
||||||
|
|
||||||
TREESITTER FUNCTIONS
|
TREESITTER FUNCTIONS
|
||||||
- *vim.treesitter.language.require_language()* Use |vim.treesitter.language.add()|
|
- *vim.treesitter.language.require_language()* Use |vim.treesitter.language.add()|
|
||||||
|
@ -659,14 +659,20 @@ callbacks. Example: >lua
|
|||||||
})
|
})
|
||||||
<
|
<
|
||||||
|
|
||||||
Also the following |User| |autocommand| is provided:
|
LspProgress *LspProgress*
|
||||||
|
Upon receipt of a progress notification from the server. Notifications can
|
||||||
|
be polled from a `progress` ring buffer of a |vim.lsp.client| or use
|
||||||
|
|vim.lsp.status()| to get an aggregate message
|
||||||
|
|
||||||
LspProgressUpdate *LspProgressUpdate*
|
If the server sends a "work done progress", the `pattern` is set to `kind`
|
||||||
Upon receipt of a progress notification from the server. See
|
(one of `begin`, `report` or `end`).
|
||||||
|vim.lsp.util.get_progress_messages()|.
|
|
||||||
|
When used from Lua, the event contains a `data` table with `client_id` and
|
||||||
|
`result` properties. `result` will contain the request params sent by the
|
||||||
|
server.
|
||||||
|
|
||||||
Example: >vim
|
Example: >vim
|
||||||
autocmd User LspProgressUpdate redrawstatus
|
autocmd LspProgress * redrawstatus
|
||||||
<
|
<
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
@ -806,6 +812,8 @@ client() *vim.lsp.client*
|
|||||||
|vim.lsp.start_client()|.
|
|vim.lsp.start_client()|.
|
||||||
• {server_capabilities} (table): Response from the server sent on
|
• {server_capabilities} (table): Response from the server sent on
|
||||||
`initialize` describing the server's capabilities.
|
`initialize` describing the server's capabilities.
|
||||||
|
• {progress} A ring buffer (|vim.ringbuf()|) containing progress
|
||||||
|
messages sent by the server.
|
||||||
|
|
||||||
client_is_stopped({client_id}) *vim.lsp.client_is_stopped()*
|
client_is_stopped({client_id}) *vim.lsp.client_is_stopped()*
|
||||||
Checks whether a client is stopped.
|
Checks whether a client is stopped.
|
||||||
@ -1092,6 +1100,13 @@ start_client({config}) *vim.lsp.start_client()*
|
|||||||
not be fully initialized. Use `on_init` to do any actions once the
|
not be fully initialized. Use `on_init` to do any actions once the
|
||||||
client has been initialized.
|
client has been initialized.
|
||||||
|
|
||||||
|
status() *vim.lsp.status()*
|
||||||
|
Consumes the latest progress messages from all clients and formats them as
|
||||||
|
a string. Empty if there are no clients or if no new messages
|
||||||
|
|
||||||
|
Return: ~
|
||||||
|
(string)
|
||||||
|
|
||||||
stop_client({client_id}, {force}) *vim.lsp.stop_client()*
|
stop_client({client_id}, {force}) *vim.lsp.stop_client()*
|
||||||
Stops a client(s).
|
Stops a client(s).
|
||||||
|
|
||||||
|
@ -33,8 +33,8 @@ The following changes may require adaptations in user config or plugins.
|
|||||||
• When switching windows, |CursorMoved| autocommands trigger when Nvim is back
|
• When switching windows, |CursorMoved| autocommands trigger when Nvim is back
|
||||||
in the main loop rather than immediately. This is more compatible with Vim.
|
in the main loop rather than immediately. This is more compatible with Vim.
|
||||||
|
|
||||||
• |LspRequest| autocmd was promoted from a |User| autocmd to a first class
|
• |LspRequest| and LspProgressUpdate (renamed to |LspProgress|) autocmds were
|
||||||
citizen.
|
promoted from a |User| autocmd to first class citizen.
|
||||||
|
|
||||||
• Renamed `vim.treesitter.playground` to `vim.treesitter.dev`.
|
• Renamed `vim.treesitter.playground` to `vim.treesitter.dev`.
|
||||||
|
|
||||||
@ -43,6 +43,8 @@ ADDED FEATURES *news-added*
|
|||||||
|
|
||||||
The following new APIs or features were added.
|
The following new APIs or features were added.
|
||||||
|
|
||||||
|
• Added |vim.lsp.status()| to consume the last progress messages as a string.
|
||||||
|
|
||||||
• Neovim's LSP client now always saves and restores named buffer marks when
|
• Neovim's LSP client now always saves and restores named buffer marks when
|
||||||
applying text edits.
|
applying text edits.
|
||||||
|
|
||||||
@ -142,6 +144,9 @@ release.
|
|||||||
- |nvim_win_get_option()| Use |nvim_get_option_value()| instead.
|
- |nvim_win_get_option()| Use |nvim_get_option_value()| instead.
|
||||||
- |nvim_win_set_option()| Use |nvim_set_option_value()| instead.
|
- |nvim_win_set_option()| Use |nvim_set_option_value()| instead.
|
||||||
|
|
||||||
|
• vim.lsp functions:
|
||||||
|
- |vim.lsp.util.get_progress_messages()| Use |vim.lsp.status()| instead.
|
||||||
|
|
||||||
• `vim.loop` has been renamed to `vim.uv`.
|
• `vim.loop` has been renamed to `vim.uv`.
|
||||||
|
|
||||||
vim:tw=78:ts=8:sw=2:et:ft=help:norl:
|
vim:tw=78:ts=8:sw=2:et:ft=help:norl:
|
||||||
|
@ -807,6 +807,9 @@ end
|
|||||||
---
|
---
|
||||||
--- - {server_capabilities} (table): Response from the server sent on
|
--- - {server_capabilities} (table): Response from the server sent on
|
||||||
--- `initialize` describing the server's capabilities.
|
--- `initialize` describing the server's capabilities.
|
||||||
|
---
|
||||||
|
--- - {progress} A ring buffer (|vim.ringbuf()|) containing progress messages
|
||||||
|
--- sent by the server.
|
||||||
function lsp.client()
|
function lsp.client()
|
||||||
error()
|
error()
|
||||||
end
|
end
|
||||||
@ -891,6 +894,50 @@ function lsp.start(config, opts)
|
|||||||
return client_id
|
return client_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Consumes the latest progress messages from all clients and formats them as a string.
|
||||||
|
--- Empty if there are no clients or if no new messages
|
||||||
|
---
|
||||||
|
---@return string
|
||||||
|
function lsp.status()
|
||||||
|
local percentage = nil
|
||||||
|
local groups = {}
|
||||||
|
for _, client in ipairs(vim.lsp.get_active_clients()) do
|
||||||
|
for progress in client.progress do
|
||||||
|
local value = progress.value
|
||||||
|
if type(value) == 'table' and value.kind then
|
||||||
|
local group = groups[progress.token]
|
||||||
|
if not group then
|
||||||
|
group = {}
|
||||||
|
groups[progress.token] = group
|
||||||
|
end
|
||||||
|
group.title = value.title or group.title
|
||||||
|
group.message = value.message or group.message
|
||||||
|
if value.percentage then
|
||||||
|
percentage = math.max(percentage or 0, value.percentage)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- else: Doesn't look like work done progress and can be in any format
|
||||||
|
-- Just ignore it as there is no sensible way to display it
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local messages = {}
|
||||||
|
for _, group in pairs(groups) do
|
||||||
|
if group.title then
|
||||||
|
table.insert(
|
||||||
|
messages,
|
||||||
|
group.message and (group.title .. ': ' .. group.message) or group.title
|
||||||
|
)
|
||||||
|
elseif group.message then
|
||||||
|
table.insert(messages, group.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local message = table.concat(messages, ', ')
|
||||||
|
if percentage then
|
||||||
|
return string.format('%03d: %s', percentage, message)
|
||||||
|
end
|
||||||
|
return message
|
||||||
|
end
|
||||||
|
|
||||||
---@private
|
---@private
|
||||||
-- Determines whether the given option can be set by `set_defaults`.
|
-- Determines whether the given option can be set by `set_defaults`.
|
||||||
local function is_empty_or_default(bufnr, option)
|
local function is_empty_or_default(bufnr, option)
|
||||||
@ -1266,10 +1313,23 @@ function lsp.start_client(config)
|
|||||||
|
|
||||||
--- @type table<integer,{ type: string, bufnr: integer, method: string}>
|
--- @type table<integer,{ type: string, bufnr: integer, method: string}>
|
||||||
requests = {},
|
requests = {},
|
||||||
-- for $/progress report
|
|
||||||
|
--- Contains $/progress report messages.
|
||||||
|
--- They have the format {token: integer|string, value: any}
|
||||||
|
--- For "work done progress", value will be one of:
|
||||||
|
--- - lsp.WorkDoneProgressBegin,
|
||||||
|
--- - lsp.WorkDoneProgressReport (extended with title from Begin)
|
||||||
|
--- - lsp.WorkDoneProgressEnd (extended with title from Begin)
|
||||||
|
progress = vim.ringbuf(50),
|
||||||
|
|
||||||
|
---@deprecated use client.progress instead
|
||||||
messages = { name = name, messages = {}, progress = {}, status = {} },
|
messages = { name = name, messages = {}, progress = {}, status = {} },
|
||||||
dynamic_capabilities = require('vim.lsp._dynamic').new(client_id),
|
dynamic_capabilities = require('vim.lsp._dynamic').new(client_id),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
---@type table<string|integer, string> title of unfinished progress sequences by token
|
||||||
|
client.progress.pending = {}
|
||||||
|
|
||||||
--- @type lsp.ClientCapabilities
|
--- @type lsp.ClientCapabilities
|
||||||
client.config.capabilities = config.capabilities or protocol.make_client_capabilities()
|
client.config.capabilities = config.capabilities or protocol.make_client_capabilities()
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ local M = {}
|
|||||||
|
|
||||||
---@private
|
---@private
|
||||||
--- Writes to error buffer.
|
--- Writes to error buffer.
|
||||||
---@param ... (table of strings) Will be concatenated before being written
|
---@param ... string Will be concatenated before being written
|
||||||
local function err_message(...)
|
local function err_message(...)
|
||||||
vim.notify(table.concat(vim.tbl_flatten({ ... })), vim.log.levels.ERROR)
|
vim.notify(table.concat(vim.tbl_flatten({ ... })), vim.log.levels.ERROR)
|
||||||
api.nvim_command('redraw')
|
api.nvim_command('redraw')
|
||||||
@ -20,63 +20,52 @@ M['workspace/executeCommand'] = function(_, _, _, _)
|
|||||||
-- Error handling is done implicitly by wrapping all handlers; see end of this file
|
-- Error handling is done implicitly by wrapping all handlers; see end of this file
|
||||||
end
|
end
|
||||||
|
|
||||||
---@private
|
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
|
||||||
local function progress_handler(_, result, ctx, _)
|
---@param result lsp.ProgressParams
|
||||||
local client_id = ctx.client_id
|
---@param ctx lsp.HandlerContext
|
||||||
local client = vim.lsp.get_client_by_id(client_id)
|
M['$/progress'] = function(_, result, ctx)
|
||||||
local client_name = client and client.name or string.format('id=%d', client_id)
|
local client = vim.lsp.get_client_by_id(ctx.client_id)
|
||||||
if not client then
|
if not client then
|
||||||
err_message('LSP[', client_name, '] client has shut down during progress update')
|
err_message('LSP[id=', tostring(ctx.client_id), '] client has shut down during progress update')
|
||||||
return vim.NIL
|
return vim.NIL
|
||||||
end
|
end
|
||||||
local val = result.value -- unspecified yet
|
local kind = nil
|
||||||
local token = result.token -- string or number
|
local value = result.value
|
||||||
|
|
||||||
if type(val) ~= 'table' then
|
if type(value) == 'table' then
|
||||||
val = { content = val }
|
kind = value.kind
|
||||||
end
|
-- Carry over title of `begin` messages to `report` and `end` messages
|
||||||
if val.kind then
|
-- So that consumers always have it available, even if they consume a
|
||||||
if val.kind == 'begin' then
|
-- subset of the full sequence
|
||||||
client.messages.progress[token] = {
|
if kind == 'begin' then
|
||||||
title = val.title,
|
client.progress.pending[result.token] = value.title
|
||||||
cancellable = val.cancellable,
|
else
|
||||||
message = val.message,
|
value.title = client.progress.pending[result.token]
|
||||||
percentage = val.percentage,
|
if kind == 'end' then
|
||||||
}
|
client.progress.pending[result.token] = nil
|
||||||
elseif val.kind == 'report' then
|
|
||||||
client.messages.progress[token].cancellable = val.cancellable
|
|
||||||
client.messages.progress[token].message = val.message
|
|
||||||
client.messages.progress[token].percentage = val.percentage
|
|
||||||
elseif val.kind == 'end' then
|
|
||||||
if client.messages.progress[token] == nil then
|
|
||||||
err_message('LSP[', client_name, '] received `end` message with no corresponding `begin`')
|
|
||||||
else
|
|
||||||
client.messages.progress[token].message = val.message
|
|
||||||
client.messages.progress[token].done = true
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
|
||||||
client.messages.progress[token] = val
|
|
||||||
client.messages.progress[token].done = true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
api.nvim_exec_autocmds('User', { pattern = 'LspProgressUpdate', modeline = false })
|
client.progress:push(result)
|
||||||
|
|
||||||
|
api.nvim_exec_autocmds('LspProgress', {
|
||||||
|
pattern = kind,
|
||||||
|
modeline = false,
|
||||||
|
data = { client_id = ctx.client_id, result = result },
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
|
|
||||||
M['$/progress'] = progress_handler
|
|
||||||
|
|
||||||
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create
|
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create
|
||||||
|
---@param result lsp.WorkDoneProgressCreateParams
|
||||||
|
---@param ctx lsp.HandlerContext
|
||||||
M['window/workDoneProgress/create'] = function(_, result, ctx)
|
M['window/workDoneProgress/create'] = function(_, result, ctx)
|
||||||
local client_id = ctx.client_id
|
local client = vim.lsp.get_client_by_id(ctx.client_id)
|
||||||
local client = vim.lsp.get_client_by_id(client_id)
|
|
||||||
local token = result.token -- string or number
|
|
||||||
local client_name = client and client.name or string.format('id=%d', client_id)
|
|
||||||
if not client then
|
if not client then
|
||||||
err_message('LSP[', client_name, '] client has shut down while creating progress report')
|
err_message('LSP[id=', tostring(ctx.client_id), '] client has shut down during progress update')
|
||||||
return vim.NIL
|
return vim.NIL
|
||||||
end
|
end
|
||||||
client.messages.progress[token] = {}
|
client.progress:push(result)
|
||||||
return vim.NIL
|
return vim.NIL
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
---@meta
|
---@meta
|
||||||
|
|
||||||
---@alias lsp-handler fun(err: lsp.ResponseError|nil, result: any, context: table, config: table|nil)
|
---@alias lsp-handler fun(err: lsp.ResponseError|nil, result: any, context: lsp.HandlerContext, config: table|nil)
|
||||||
|
|
||||||
|
---@class lsp.HandlerContext
|
||||||
|
---@field method string
|
||||||
|
---@field client_id integer
|
||||||
|
---@field bufnr integer
|
||||||
|
---@field params any
|
||||||
|
|
||||||
---@class lsp.ResponseError
|
---@class lsp.ResponseError
|
||||||
---@field code integer
|
---@field code integer
|
||||||
|
@ -353,11 +353,40 @@ end
|
|||||||
|
|
||||||
--- Process and return progress reports from lsp server
|
--- Process and return progress reports from lsp server
|
||||||
---@private
|
---@private
|
||||||
|
---@deprecated Use vim.lsp.status() or access client.progress directly
|
||||||
function M.get_progress_messages()
|
function M.get_progress_messages()
|
||||||
|
vim.deprecate('vim.lsp.util.get_progress_messages', 'vim.lsp.status', '0.11.0')
|
||||||
local new_messages = {}
|
local new_messages = {}
|
||||||
local progress_remove = {}
|
local progress_remove = {}
|
||||||
|
|
||||||
for _, client in ipairs(vim.lsp.get_active_clients()) do
|
for _, client in ipairs(vim.lsp.get_active_clients()) do
|
||||||
|
local groups = {}
|
||||||
|
for progress in client.progress do
|
||||||
|
local value = progress.value
|
||||||
|
if type(value) == 'table' and value.kind then
|
||||||
|
local group = groups[progress.token]
|
||||||
|
if not group then
|
||||||
|
group = {
|
||||||
|
done = false,
|
||||||
|
progress = true,
|
||||||
|
title = 'empty title',
|
||||||
|
}
|
||||||
|
groups[progress.token] = group
|
||||||
|
end
|
||||||
|
group.title = value.title or group.title
|
||||||
|
group.cancellable = value.cancellable or group.cancellable
|
||||||
|
if value.kind == 'end' then
|
||||||
|
group.done = true
|
||||||
|
end
|
||||||
|
group.message = value.message or group.message
|
||||||
|
group.percentage = value.percentage or group.percentage
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, group in pairs(groups) do
|
||||||
|
table.insert(new_messages, group)
|
||||||
|
end
|
||||||
|
|
||||||
local messages = client.messages
|
local messages = client.messages
|
||||||
local data = messages
|
local data = messages
|
||||||
for token, ctx in pairs(data.progress) do
|
for token, ctx in pairs(data.progress) do
|
||||||
|
@ -74,6 +74,7 @@ return {
|
|||||||
'LspDetach', -- after an LSP client detaches from a buffer
|
'LspDetach', -- after an LSP client detaches from a buffer
|
||||||
'LspRequest', -- after an LSP request is started, canceled, or completed
|
'LspRequest', -- after an LSP request is started, canceled, or completed
|
||||||
'LspTokenUpdate', -- after a visible LSP token is updated
|
'LspTokenUpdate', -- after a visible LSP token is updated
|
||||||
|
'LspProgress', -- after a LSP progress update
|
||||||
'MenuPopup', -- just before popup menu is displayed
|
'MenuPopup', -- just before popup menu is displayed
|
||||||
'ModeChanged', -- after changing the mode
|
'ModeChanged', -- after changing the mode
|
||||||
'OptionSet', -- after setting any option
|
'OptionSet', -- after setting any option
|
||||||
@ -154,6 +155,7 @@ return {
|
|||||||
LspAttach=true,
|
LspAttach=true,
|
||||||
LspDetach=true,
|
LspDetach=true,
|
||||||
LspRequest=true,
|
LspRequest=true,
|
||||||
|
LspProgress=true,
|
||||||
LspTokenUpdate=true,
|
LspTokenUpdate=true,
|
||||||
RecordingEnter=true,
|
RecordingEnter=true,
|
||||||
RecordingLeave=true,
|
RecordingLeave=true,
|
||||||
|
Loading…
Reference in New Issue
Block a user