feat(lsp): track pending+cancel requests on client object #15949

This commit is contained in:
jdrouhard 2021-10-29 07:45:01 -05:00 committed by GitHub
parent 1dbbaf89bf
commit d1c470957b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 254 additions and 10 deletions

View File

@ -451,6 +451,22 @@ LspSignatureActiveParameter
Used to highlight the active parameter in the signature help. See Used to highlight the active parameter in the signature help. See
|vim.lsp.handlers.signature_help()|. |vim.lsp.handlers.signature_help()|.
==============================================================================
EVENTS *lsp-events*
LspProgressUpdate *LspProgressUpdate*
Upon receipt of a progress notification from the server. See
|vim.lsp.util.get_progress_messages()|.
LspRequest *LspRequest*
After a change to the active set of pending LSP requests. See {requests}
in |vim.lsp.client|.
Example: >
autocmd User LspProgressUpdate redrawstatus
autocmd User LspRequest redrawstatus
<
============================================================================== ==============================================================================
Lua module: vim.lsp *lsp-core* Lua module: vim.lsp *lsp-core*
@ -608,6 +624,11 @@ client() *vim.lsp.client*
server. server.
• {handlers} (table): The handlers used by the client as • {handlers} (table): The handlers used by the client as
described in |lsp-handler|. described in |lsp-handler|.
• {requests} (table): The current pending requests in flight
to the server. Entries are key-value pairs with the key
being the request ID while the value is a table with `type`,
`bufnr`, and `method` key-value pairs. `type` is either "pending"
for an active request, or "cancel" for a cancel request.
• {config} (table): copy of the table that was passed by the • {config} (table): copy of the table that was passed by the
user to |vim.lsp.start_client()|. user to |vim.lsp.start_client()|.
• {server_capabilities} (table): Response from the server • {server_capabilities} (table): Response from the server

View File

@ -772,8 +772,10 @@ function lsp.start_client(config)
attached_buffers = {}; attached_buffers = {};
handlers = handlers; handlers = handlers;
requests = {};
-- for $/progress report -- 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. -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes.
@ -906,11 +908,21 @@ function lsp.start_client(config)
end end
-- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state
changetracking.flush(client) changetracking.flush(client)
bufnr = resolve_bufnr(bufnr)
local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, 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}) 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) 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 end
---@private ---@private
@ -970,6 +982,11 @@ function lsp.start_client(config)
---@see |vim.lsp.client.notify()| ---@see |vim.lsp.client.notify()|
function client.cancel_request(id) function client.cancel_request(id)
validate{id = {id, 'n'}} 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 }) return rpc.notify("$/cancelRequest", { id = id })
end end

View File

@ -297,6 +297,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
local message_index = 0 local message_index = 0
local message_callbacks = {} local message_callbacks = {}
local notify_reply_callbacks = {}
local handle, pid local handle, pid
do do
@ -309,8 +310,9 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
stdout:close() stdout:close()
stderr:close() stderr:close()
handle: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 message_callbacks = nil
notify_reply_callbacks = nil
dispatchers.on_exit(code, signal) dispatchers.on_exit(code, signal)
end end
local spawn_params = { 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 method (string) The invoked LSP method
---@param params (table) Parameters for the invoked LSP method ---@param params (table) Parameters for the invoked LSP method
---@param callback (function) Callback to invoke ---@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 ---@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 { validate {
callback = { callback, 'f' }; callback = { callback, 'f' };
notify_reply_callback = { notify_reply_callback, 'f', true };
} }
message_index = message_index + 1 message_index = message_index + 1
local message_id = message_index local message_id = message_index
@ -388,8 +392,15 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
method = method; method = method;
params = params; params = params;
} }
if result and message_callbacks then if result then
message_callbacks[message_id] = schedule_wrap(callback) 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 return result, message_id
else else
return false 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. -- We sent a number, so we expect a number.
local result_id = tonumber(decoded.id) 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. -- Do not surface RequestCancelled to users, it is RPC-internal.
if decoded.error then if decoded.error then
local mute_error = false local mute_error = false

View File

@ -275,6 +275,55 @@ function tests.check_forward_content_modified()
} }
end end
function tests.check_pending_request_tracked()
skeleton {
on_init = function(_)
return { capabilities = {} }
end;
body = function()
local msg = read_message()
assert_eq('slow_request', msg.method)
expect_notification('release')
respond(msg.id, nil, {})
expect_notification('finish')
notify('finish')
end;
}
end
function tests.check_cancel_request_tracked()
skeleton {
on_init = function(_)
return { capabilities = {} }
end;
body = function()
local msg = read_message()
assert_eq('slow_request', msg.method)
expect_notification('$/cancelRequest', {id=msg.id})
expect_notification('release')
respond(msg.id, {code = -32800}, nil)
notify('finish')
end;
}
end
function tests.check_tracked_requests_cleared()
skeleton {
on_init = function(_)
return { capabilities = {} }
end;
body = function()
local msg = read_message()
assert_eq('slow_request', msg.method)
expect_notification('$/cancelRequest', {id=msg.id})
expect_notification('release')
respond(msg.id, nil, {})
expect_notification('finish')
notify('finish')
end;
}
end
function tests.basic_finish() function tests.basic_finish()
skeleton { skeleton {
on_init = function(params) on_init = function(params)

View File

@ -3,9 +3,11 @@ local helpers = require('test.functional.helpers')(after_each)
local assert_log = helpers.assert_log local assert_log = helpers.assert_log
local clear = helpers.clear local clear = helpers.clear
local buf_lines = helpers.buf_lines local buf_lines = helpers.buf_lines
local command = helpers.command
local dedent = helpers.dedent local dedent = helpers.dedent
local exec_lua = helpers.exec_lua local exec_lua = helpers.exec_lua
local eq = helpers.eq local eq = helpers.eq
local eval = helpers.eval
local matches = helpers.matches local matches = helpers.matches
local pcall_err = helpers.pcall_err local pcall_err = helpers.pcall_err
local pesc = helpers.pesc local pesc = helpers.pesc
@ -272,7 +274,7 @@ describe('LSP', function()
return return
end end
local expected_handlers = { local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}}; {NIL, {}, {method="shutdown", bufnr=1, client_id=1}};
{NIL, {}, {method="test", client_id=1}}; {NIL, {}, {method="test", client_id=1}};
} }
test_rpc_server { test_rpc_server {
@ -486,7 +488,7 @@ describe('LSP', function()
it('should forward ContentModified to callback', function() it('should forward ContentModified to callback', function()
local expected_handlers = { local expected_handlers = {
{NIL, {}, {method="finish", client_id=1}}; {NIL, {}, {method="finish", client_id=1}};
{{code = -32801}, NIL, {method = "error_code_test", client_id=1}}; {{code = -32801}, NIL, {method = "error_code_test", bufnr=1, client_id=1}};
} }
local client local client
test_rpc_server { test_rpc_server {
@ -509,6 +511,140 @@ describe('LSP', function()
} }
end) end)
it('should track pending requests to the language server', function()
local expected_handlers = {
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="slow_request", bufnr=1, client_id=1}};
}
local client
test_rpc_server {
test_name = "check_pending_request_tracked";
on_init = function(_client)
client = _client
client.request("slow_request")
local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
eq("slow_request", request.method)
eq("pending", request.type)
client.notify("release")
end;
on_exit = function(code, signal)
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
eq(0, #expected_handlers, "did not call expected handler")
end;
on_handler = function(err, _, ctx)
eq(table.remove(expected_handlers), {err, {}, ctx}, "expected handler")
if ctx.method == 'slow_request' then
local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
eq(NIL, request)
client.notify("finish")
end
if ctx.method == 'finish' then client.stop() end
end;
}
end)
it('should track cancel requests to the language server', function()
local expected_handlers = {
{NIL, {}, {method="finish", client_id=1}};
}
local client
test_rpc_server {
test_name = "check_cancel_request_tracked";
on_init = function(_client)
client = _client
client.request("slow_request")
client.cancel_request(2)
local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
eq("slow_request", request.method)
eq("cancel", request.type)
client.notify("release")
end;
on_exit = function(code, signal)
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
eq(0, #expected_handlers, "did not call expected handler")
end;
on_handler = function(err, _, ctx)
eq(table.remove(expected_handlers), {err, {}, ctx}, "expected handler")
local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
eq(NIL, request)
if ctx.method == 'finish' then client.stop() end
end;
}
end)
it('should clear pending and cancel requests on reply', function()
local expected_handlers = {
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="slow_request", bufnr=1, client_id=1}};
}
local client
test_rpc_server {
test_name = "check_tracked_requests_cleared";
on_init = function(_client)
client = _client
client.request("slow_request")
local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
eq("slow_request", request.method)
eq("pending", request.type)
client.cancel_request(2)
request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
eq("slow_request", request.method)
eq("cancel", request.type)
client.notify("release")
end;
on_exit = function(code, signal)
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
eq(0, #expected_handlers, "did not call expected handler")
end;
on_handler = function(err, _, ctx)
eq(table.remove(expected_handlers), {err, {}, ctx}, "expected handler")
if ctx.method == 'slow_request' then
local request = exec_lua([=[ return TEST_RPC_CLIENT.requests[2] ]=])
eq(NIL, request)
client.notify("finish")
end
if ctx.method == 'finish' then client.stop() end
end;
}
end)
it('should trigger LspRequest autocmd when requests table changes', function()
local expected_handlers = {
{NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="slow_request", bufnr=1, client_id=1}};
}
local client
test_rpc_server {
test_name = "check_tracked_requests_cleared";
on_init = function(_client)
command('let g:requests = 0')
command('autocmd User LspRequest let g:requests+=1')
client = _client
client.request("slow_request")
eq(1, eval('g:requests'))
client.cancel_request(2)
eq(2, eval('g:requests'))
client.notify("release")
end;
on_exit = function(code, signal)
eq(0, code, "exit code", fake_lsp_logfile)
eq(0, signal, "exit signal", fake_lsp_logfile)
eq(0, #expected_handlers, "did not call expected handler")
eq(3, eval('g:requests'))
end;
on_handler = function(err, _, ctx)
eq(table.remove(expected_handlers), {err, {}, ctx}, "expected handler")
if ctx.method == 'slow_request' then
client.notify("finish")
end
if ctx.method == 'finish' then client.stop() end
end;
}
end)
it('should not send didOpen if the buffer closes before init', function() it('should not send didOpen if the buffer closes before init', function()
local expected_handlers = { local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}}; {NIL, {}, {method="shutdown", client_id=1}};
@ -790,7 +926,7 @@ describe('LSP', function()
-- TODO(askhan) we don't support full for now, so we can disable these tests. -- TODO(askhan) we don't support full for now, so we can disable these tests.
pending('should check the body and didChange incremental normal mode editing', function() pending('should check the body and didChange incremental normal mode editing', function()
local expected_handlers = { local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}}; {NIL, {}, {method="shutdown", bufnr=1, client_id=1}};
{NIL, {}, {method="finish", client_id=1}}; {NIL, {}, {method="finish", client_id=1}};
{NIL, {}, {method="start", client_id=1}}; {NIL, {}, {method="start", client_id=1}};
} }