LSP: Eagerly decode request results in the client

Previously the `call` helper (and its related functions) returned a
`serde_json::Value` which was then decoded either later in the client
(see signature help and hover) or by the client's caller. This led to
some unnecessary boilerplate in the client:

    let resp = self.call::<MyRequest>(params);
    Some(async move { Ok(serde_json::from_value(resp.await?)?) })

and in the caller. It also allowed for mistakes with the types. The
workspace symbol request's calling code for example mistakenly decoded a
`lsp::WorkspaceSymbolResponse` as `Vec<lsp::SymbolInformation>` - one of
the untagged enum members (so it parsed successfully) but not the
correct type.

With this change, the `call` helper eagerly decodes the response to a
request as the `lsp::request::Request::Result` trait item. This is
similar to the old helper `request` (which has become redundant and has
been eliminated) but all work is done within the same async block which
avoids some awkward lifetimes. The return types of functions like
`Client::text_document_range_inlay_hints` are now more verbose but it is
no longer possible to accidentally decode as an incorrect type.

Additionally `Client::resolve_code_action` now uses the `call_with_ref`
helper to avoid an unnecessary clone.
This commit is contained in:
Michael Davis 2025-03-22 14:18:17 -04:00
parent 6da1a79d80
commit 7e7a98560e
No known key found for this signature in database
6 changed files with 96 additions and 137 deletions

View file

@ -385,23 +385,11 @@ impl Client {
self.workspace_folders.lock()
}
/// Execute a RPC request on the language server.
async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
where
R::Params: serde::Serialize,
R::Result: core::fmt::Debug, // TODO: temporary
{
// a future that resolves into the response
let json = self.call::<R>(params).await?;
let response = serde_json::from_value(json)?;
Ok(response)
}
/// Execute a RPC request on the language server.
fn call<R: lsp::request::Request>(
&self,
params: R::Params,
) -> impl Future<Output = Result<Value>>
) -> impl Future<Output = Result<R::Result>>
where
R::Params: serde::Serialize,
{
@ -411,7 +399,7 @@ impl Client {
fn call_with_ref<R: lsp::request::Request>(
&self,
params: &R::Params,
) -> impl Future<Output = Result<Value>>
) -> impl Future<Output = Result<R::Result>>
where
R::Params: serde::Serialize,
{
@ -422,7 +410,7 @@ impl Client {
&self,
params: &R::Params,
timeout_secs: u64,
) -> impl Future<Output = Result<Value>>
) -> impl Future<Output = Result<R::Result>>
where
R::Params: serde::Serialize,
{
@ -458,6 +446,7 @@ impl Client {
.await
.map_err(|_| Error::Timeout(id))? // return Timeout
.ok_or(Error::StreamClosed)?
.and_then(|value| serde_json::from_value(value).map_err(Into::into))
}
}
@ -697,11 +686,11 @@ impl Client {
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
};
self.request::<lsp::request::Initialize>(params).await
self.call::<lsp::request::Initialize>(params).await
}
pub async fn shutdown(&self) -> Result<()> {
self.request::<lsp::request::Shutdown>(()).await
self.call::<lsp::request::Shutdown>(()).await
}
pub fn exit(&self) {
@ -746,7 +735,7 @@ impl Client {
old_path: &Path,
new_path: &Path,
is_dir: bool,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceEdit>>>> {
let capabilities = self.file_operations_intests();
if !capabilities.will_rename.has_interest(old_path, is_dir) {
return None;
@ -763,16 +752,10 @@ impl Client {
old_uri: url_from_path(old_path)?,
new_uri: url_from_path(new_path)?,
}];
let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
Some(self.call_with_timeout::<lsp::request::WillRenameFiles>(
&lsp::RenameFilesParams { files },
5,
);
Some(async move {
let json = request.await?;
let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
))
}
pub fn did_rename(&self, old_path: &Path, new_path: &Path, is_dir: bool) -> Option<()> {
@ -1016,7 +999,7 @@ impl Client {
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
context: lsp::CompletionContext,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::CompletionResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support completion.
@ -1042,14 +1025,13 @@ impl Client {
&self,
completion_item: &lsp::CompletionItem,
) -> impl Future<Output = Result<lsp::CompletionItem>> {
let res = self.call_with_ref::<lsp::request::ResolveCompletionItem>(completion_item);
async move { Ok(serde_json::from_value(res.await?)?) }
self.call_with_ref::<lsp::request::ResolveCompletionItem>(completion_item)
}
pub fn resolve_code_action(
&self,
code_action: lsp::CodeAction,
) -> Option<impl Future<Output = Result<Value>>> {
code_action: &lsp::CodeAction,
) -> Option<impl Future<Output = Result<lsp::CodeAction>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support resolving code actions.
@ -1061,7 +1043,7 @@ impl Client {
_ => return None,
}
Some(self.call::<lsp::request::CodeActionResolveRequest>(code_action))
Some(self.call_with_ref::<lsp::request::CodeActionResolveRequest>(code_action))
}
pub fn text_document_signature_help(
@ -1085,8 +1067,7 @@ impl Client {
// lsp::SignatureHelpContext
};
let res = self.call::<lsp::request::SignatureHelpRequest>(params);
Some(async move { Ok(serde_json::from_value(res.await?)?) })
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
}
pub fn text_document_range_inlay_hints(
@ -1094,7 +1075,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::InlayHint>>>>> {
let capabilities = self.capabilities.get().unwrap();
match capabilities.inlay_hint_provider {
@ -1140,8 +1121,7 @@ impl Client {
// lsp::SignatureHelpContext
};
let res = self.call::<lsp::request::HoverRequest>(params);
Some(async move { Ok(serde_json::from_value(res.await?)?) })
Some(self.call::<lsp::request::HoverRequest>(params))
}
// formatting
@ -1151,7 +1131,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::TextEdit>>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support formatting.
@ -1184,13 +1164,7 @@ impl Client {
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
};
let request = self.call::<lsp::request::Formatting>(params);
Some(async move {
let json = request.await?;
let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
Some(self.call::<lsp::request::Formatting>(params))
}
pub fn text_document_range_formatting(
@ -1199,7 +1173,7 @@ impl Client {
range: lsp::Range,
options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::TextEdit>>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support range formatting.
@ -1215,13 +1189,7 @@ impl Client {
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
};
let request = self.call::<lsp::request::RangeFormatting>(params);
Some(async move {
let json = request.await?;
let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
Some(self.call::<lsp::request::RangeFormatting>(params))
}
pub fn text_document_document_highlight(
@ -1229,7 +1197,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::DocumentHighlight>>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support document highlight.
@ -1262,7 +1230,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
) -> impl Future<Output = Result<T::Result>> {
let params = lsp::GotoDefinitionParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
@ -1282,7 +1250,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-definition.
@ -1303,7 +1271,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-declaration.
@ -1328,7 +1296,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-type-definition.
@ -1352,7 +1320,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-definition.
@ -1377,7 +1345,7 @@ impl Client {
position: lsp::Position,
include_declaration: bool,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::Location>>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-reference.
@ -1406,7 +1374,7 @@ impl Client {
pub fn document_symbols(
&self,
text_document: lsp::TextDocumentIdentifier,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::DocumentSymbolResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support document symbols.
@ -1428,7 +1396,7 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::PrepareRenameResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
match capabilities.rename_provider {
@ -1448,7 +1416,10 @@ impl Client {
}
// empty string to get all symbols
pub fn workspace_symbols(&self, query: String) -> Option<impl Future<Output = Result<Value>>> {
pub fn workspace_symbols(
&self,
query: String,
) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceSymbolResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support workspace symbols.
@ -1471,7 +1442,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
context: lsp::CodeActionContext,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::CodeActionOrCommand>>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support code actions.
@ -1499,7 +1470,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
new_name: String,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceEdit>>>> {
if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
return None;
}
@ -1515,16 +1486,13 @@ impl Client {
},
};
let request = self.call::<lsp::request::Rename>(params);
Some(async move {
let json = request.await?;
let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
Some(self.call::<lsp::request::Rename>(params))
}
pub fn command(&self, command: lsp::Command) -> Option<impl Future<Output = Result<Value>>> {
pub fn command(
&self,
command: lsp::Command,
) -> Option<impl Future<Output = Result<Option<Value>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the language server does not support executing commands.