mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00: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:
		 Mathias Fußenegger
					Mathias Fußenegger
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							f31dba93f9
						
					
				
				
					commit
					e5e0bda41b
				
			| @@ -130,6 +130,8 @@ LSP FUNCTIONS | ||||
| 						{async = false} instead. | ||||
| - *vim.lsp.buf.range_formatting()*		Use |vim.lsp.formatexpr()| | ||||
| 						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 | ||||
| - *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* | ||||
|     Upon receipt of a progress notification from the server. See | ||||
|     |vim.lsp.util.get_progress_messages()|. | ||||
|     If the server sends a "work done progress", the `pattern` is set to `kind` | ||||
|     (one of `begin`, `report` or `end`). | ||||
|  | ||||
|     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 | ||||
|     autocmd User LspProgressUpdate redrawstatus | ||||
|     autocmd LspProgress * redrawstatus | ||||
| < | ||||
|  | ||||
| ============================================================================== | ||||
| @@ -806,6 +812,8 @@ client()                                                      *vim.lsp.client* | ||||
|         |vim.lsp.start_client()|. | ||||
|       • {server_capabilities} (table): Response from the server sent on | ||||
|         `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()* | ||||
|     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 | ||||
|         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()* | ||||
|     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 | ||||
|   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 | ||||
|   citizen. | ||||
| • |LspRequest| and LspProgressUpdate (renamed to |LspProgress|) autocmds were | ||||
|   promoted from a |User| autocmd to first class citizen. | ||||
|  | ||||
| • Renamed `vim.treesitter.playground` to `vim.treesitter.dev`. | ||||
|  | ||||
| @@ -43,6 +43,8 @@ ADDED FEATURES                                                     *news-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 | ||||
|   applying text edits. | ||||
|  | ||||
| @@ -142,6 +144,9 @@ release. | ||||
|   - |nvim_win_get_option()|	Use |nvim_get_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:tw=78:ts=8:sw=2:et:ft=help:norl: | ||||
|   | ||||
| @@ -807,6 +807,9 @@ end | ||||
| --- | ||||
| ---  - {server_capabilities} (table): Response from the server sent on | ||||
| ---    `initialize` describing the server's capabilities. | ||||
| --- | ||||
| ---  - {progress} A ring buffer (|vim.ringbuf()|) containing progress messages | ||||
| ---    sent by the server. | ||||
| function lsp.client() | ||||
|   error() | ||||
| end | ||||
| @@ -891,6 +894,50 @@ function lsp.start(config, opts) | ||||
|   return client_id | ||||
| 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 | ||||
| -- Determines whether the given option can be set by `set_defaults`. | ||||
| 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}> | ||||
|     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 = {} }, | ||||
|     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 | ||||
|   client.config.capabilities = config.capabilities or protocol.make_client_capabilities() | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ local M = {} | ||||
|  | ||||
| ---@private | ||||
| --- 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(...) | ||||
|   vim.notify(table.concat(vim.tbl_flatten({ ... })), vim.log.levels.ERROR) | ||||
|   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 | ||||
| end | ||||
|  | ||||
| ---@private | ||||
| local function progress_handler(_, result, ctx, _) | ||||
|   local client_id = ctx.client_id | ||||
|   local client = vim.lsp.get_client_by_id(client_id) | ||||
|   local client_name = client and client.name or string.format('id=%d', client_id) | ||||
| --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress | ||||
| ---@param result lsp.ProgressParams | ||||
| ---@param ctx lsp.HandlerContext | ||||
| M['$/progress'] = function(_, result, ctx) | ||||
|   local client = vim.lsp.get_client_by_id(ctx.client_id) | ||||
|   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 | ||||
|   end | ||||
|   local val = result.value -- unspecified yet | ||||
|   local token = result.token -- string or number | ||||
|   local kind = nil | ||||
|   local value = result.value | ||||
|  | ||||
|   if type(val) ~= 'table' then | ||||
|     val = { content = val } | ||||
|   end | ||||
|   if val.kind then | ||||
|     if val.kind == 'begin' then | ||||
|       client.messages.progress[token] = { | ||||
|         title = val.title, | ||||
|         cancellable = val.cancellable, | ||||
|         message = val.message, | ||||
|         percentage = val.percentage, | ||||
|       } | ||||
|     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`') | ||||
|   if type(value) == 'table' then | ||||
|     kind = value.kind | ||||
|     -- Carry over title of `begin` messages to `report` and `end` messages | ||||
|     -- So that consumers always have it available, even if they consume a | ||||
|     -- subset of the full sequence | ||||
|     if kind == 'begin' then | ||||
|       client.progress.pending[result.token] = value.title | ||||
|     else | ||||
|         client.messages.progress[token].message = val.message | ||||
|         client.messages.progress[token].done = true | ||||
|       value.title = client.progress.pending[result.token] | ||||
|       if kind == 'end' then | ||||
|         client.progress.pending[result.token] = nil | ||||
|       end | ||||
|     end | ||||
|   else | ||||
|     client.messages.progress[token] = val | ||||
|     client.messages.progress[token].done = true | ||||
|   end | ||||
|  | ||||
|   api.nvim_exec_autocmds('User', { pattern = 'LspProgressUpdate', modeline = false }) | ||||
| end | ||||
|   client.progress:push(result) | ||||
|  | ||||
| --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress | ||||
| M['$/progress'] = progress_handler | ||||
|   api.nvim_exec_autocmds('LspProgress', { | ||||
|     pattern = kind, | ||||
|     modeline = false, | ||||
|     data = { client_id = ctx.client_id, result = result }, | ||||
|   }) | ||||
| end | ||||
|  | ||||
| --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) | ||||
|   local client_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) | ||||
|   local client = vim.lsp.get_client_by_id(ctx.client_id) | ||||
|   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 | ||||
|   end | ||||
|   client.messages.progress[token] = {} | ||||
|   client.progress:push(result) | ||||
|   return vim.NIL | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,12 @@ | ||||
| ---@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 | ||||
| ---@field code integer | ||||
|   | ||||
| @@ -353,11 +353,40 @@ end | ||||
|  | ||||
| --- Process and return progress reports from lsp server | ||||
| ---@private | ||||
| ---@deprecated Use vim.lsp.status() or access client.progress directly | ||||
| function M.get_progress_messages() | ||||
|   vim.deprecate('vim.lsp.util.get_progress_messages', 'vim.lsp.status', '0.11.0') | ||||
|   local new_messages = {} | ||||
|   local progress_remove = {} | ||||
|  | ||||
|   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 data = messages | ||||
|     for token, ctx in pairs(data.progress) do | ||||
|   | ||||
| @@ -74,6 +74,7 @@ return { | ||||
|     'LspDetach',              -- after an LSP client detaches from a buffer | ||||
|     'LspRequest',             -- after an LSP request is started, canceled, or completed | ||||
|     'LspTokenUpdate',         -- after a visible LSP token is updated | ||||
|     'LspProgress',            -- after a LSP progress update | ||||
|     'MenuPopup',              -- just before popup menu is displayed | ||||
|     'ModeChanged',            -- after changing the mode | ||||
|     'OptionSet',              -- after setting any option | ||||
| @@ -154,6 +155,7 @@ return { | ||||
|     LspAttach=true, | ||||
|     LspDetach=true, | ||||
|     LspRequest=true, | ||||
|     LspProgress=true, | ||||
|     LspTokenUpdate=true, | ||||
|     RecordingEnter=true, | ||||
|     RecordingLeave=true, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user