use std::time::Duration;
use lsp_types::{
CallHierarchyIncomingCallsParams, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
CodeActionContext, CodeActionParams, CompletionContext, CompletionParams, DeclarationParams,
DefinitionParams, DocumentDiagnosticParams, DocumentFormattingParams, DocumentHighlightParams,
DocumentRangeFormattingParams, DocumentSymbolParams, FoldingRangeParams, FormattingOptions,
HoverParams, ImplementationParams, InlayHintParams, PartialResultParams, PrepareRenameParams,
ReferenceContext, ReferenceParams, RenameParams, SelectionRangeParams, SignatureHelpParams,
TextDocumentIdentifier, TextDocumentPositionParams, TypeDefinitionParams,
TypeHierarchyPrepareParams, TypeHierarchySubtypesParams, TypeHierarchySupertypesParams,
WorkDoneProgressParams,
};
use super::Translator;
use super::dto::{CheckedHierarchyItem, Position, PositionRange};
use super::encoding_ctx::EncodingCtx;
use super::hierarchy::{LspHierarchyItem, hierarchy_item_to_lsp};
use super::routing::{Capability, DisclosedCapability, IndexingGate, PreparedDocument};
use crate::bridge::{ClientPath, Indexed, IndexingSignal};
use crate::config::ServerId;
use crate::error::Result;
use crate::lsp::{LspClient, UnclassifiedError};
pub(super) trait RequestTimeout {
fn timeout(client: &LspClient) -> Duration {
client.request_timeout()
}
}
impl RequestTimeout for lsp_types::WorkspaceSymbolParams {}
impl RequestTimeout for lsp_types::CodeAction {
fn timeout(client: &LspClient) -> Duration {
client.code_action_resolve_timeout()
}
}
pub(super) async fn budgeted<R>(client: &LspClient, params: R::Params) -> Result<R::Result>
where
R: lsp_types::Request,
R::Params: RequestTimeout,
{
client
.request_typed::<R>(params, <R::Params as RequestTimeout>::timeout(client))
.await
}
pub(super) async fn budgeted_classified<R>(
client: &LspClient,
params: R::Params,
) -> std::result::Result<R::Result, UnclassifiedError>
where
R: lsp_types::Request,
R::Params: RequestTimeout,
{
client
.request_typed_classified::<R>(params, <R::Params as RequestTimeout>::timeout(client))
.await
}
pub(super) trait FromPosition: RequestTimeout + Sized {
type Extra;
fn from_position(position: TextDocumentPositionParams, extra: Self::Extra) -> Self;
}
macro_rules! from_position_work_done {
($($params:ty),+ $(,)?) => {$(
impl RequestTimeout for $params {}
impl FromPosition for $params {
type Extra = ();
fn from_position(text_document_position_params: TextDocumentPositionParams, (): ()) -> Self {
Self {
text_document_position_params,
work_done_progress_params: WorkDoneProgressParams::default(),
}
}
}
)+};
}
macro_rules! from_position_with_partial {
($($params:ty),+ $(,)?) => {$(
impl RequestTimeout for $params {}
impl FromPosition for $params {
type Extra = ();
fn from_position(text_document_position_params: TextDocumentPositionParams, (): ()) -> Self {
Self {
text_document_position_params,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
}
}
}
)+};
}
from_position_work_done!(
HoverParams,
CallHierarchyPrepareParams,
TypeHierarchyPrepareParams,
PrepareRenameParams,
);
from_position_with_partial!(
DefinitionParams,
ImplementationParams,
TypeDefinitionParams,
DeclarationParams,
DocumentHighlightParams,
);
impl RequestTimeout for ReferenceParams {}
impl FromPosition for ReferenceParams {
type Extra = ReferenceContext;
fn from_position(
text_document_position_params: TextDocumentPositionParams,
context: ReferenceContext,
) -> Self {
Self {
text_document_position_params,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context,
}
}
}
impl RequestTimeout for CompletionParams {
fn timeout(client: &LspClient) -> Duration {
client.completion_timeout()
}
}
impl FromPosition for CompletionParams {
type Extra = Option<CompletionContext>;
fn from_position(
text_document_position_params: TextDocumentPositionParams,
context: Option<CompletionContext>,
) -> Self {
Self {
text_document_position_params,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context,
}
}
}
impl RequestTimeout for SignatureHelpParams {}
impl FromPosition for SignatureHelpParams {
type Extra = ();
fn from_position(text_document_position_params: TextDocumentPositionParams, (): ()) -> Self {
Self {
text_document_position_params,
work_done_progress_params: WorkDoneProgressParams::default(),
context: None,
}
}
}
impl RequestTimeout for SelectionRangeParams {}
impl FromPosition for SelectionRangeParams {
type Extra = ();
fn from_position(text_document_position_params: TextDocumentPositionParams, (): ()) -> Self {
Self {
text_document: text_document_position_params.text_document,
positions: vec![text_document_position_params.position],
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
}
}
}
impl RequestTimeout for RenameParams {}
impl RequestTimeout for InlayHintParams {}
impl FromPosition for RenameParams {
type Extra = String;
fn from_position(
text_document_position_params: TextDocumentPositionParams,
new_name: String,
) -> Self {
Self {
text_document_position_params,
new_name,
work_done_progress_params: WorkDoneProgressParams::default(),
}
}
}
pub(super) trait FromDocument: RequestTimeout + Sized {
type Extra;
fn from_document(text_document: TextDocumentIdentifier, extra: Self::Extra) -> Self;
}
impl RequestTimeout for FoldingRangeParams {}
impl FromDocument for FoldingRangeParams {
type Extra = ();
fn from_document(text_document: TextDocumentIdentifier, (): ()) -> Self {
Self {
text_document,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
}
}
}
impl RequestTimeout for DocumentSymbolParams {}
impl FromDocument for DocumentSymbolParams {
type Extra = ();
fn from_document(text_document: TextDocumentIdentifier, (): ()) -> Self {
Self {
text_document,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
}
}
}
impl RequestTimeout for DocumentDiagnosticParams {}
impl FromDocument for DocumentDiagnosticParams {
type Extra = ();
fn from_document(text_document: TextDocumentIdentifier, (): ()) -> Self {
Self {
text_document,
identifier: None,
previous_result_id: None,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
}
}
}
impl RequestTimeout for DocumentFormattingParams {}
impl FromDocument for DocumentFormattingParams {
type Extra = FormattingOptions;
fn from_document(text_document: TextDocumentIdentifier, options: FormattingOptions) -> Self {
Self {
text_document,
options,
work_done_progress_params: WorkDoneProgressParams::default(),
}
}
}
fn document_params<R>(
doc: &PreparedDocument,
extra: <R::Params as FromDocument>::Extra,
) -> R::Params
where
R: lsp_types::Request,
R::Params: FromDocument,
{
R::Params::from_document(
TextDocumentIdentifier {
uri: doc.uri().clone(),
},
extra,
)
}
pub(super) async fn document_request<R>(
doc: &PreparedDocument,
extra: <R::Params as FromDocument>::Extra,
) -> Result<R::Result>
where
R: lsp_types::Request,
R::Params: FromDocument,
{
budgeted::<R>(doc.client(), document_params::<R>(doc, extra)).await
}
pub(super) async fn document_request_classified<R>(
doc: &PreparedDocument,
extra: <R::Params as FromDocument>::Extra,
) -> std::result::Result<R::Result, UnclassifiedError>
where
R: lsp_types::Request,
R::Params: FromDocument,
{
budgeted_classified::<R>(doc.client(), document_params::<R>(doc, extra)).await
}
pub(super) trait FromRange: RequestTimeout + Sized {
type Extra;
fn from_range(
text_document: TextDocumentIdentifier,
range: lsp_types::Range,
extra: Self::Extra,
) -> Self;
}
impl FromRange for InlayHintParams {
type Extra = ();
fn from_range(text_document: TextDocumentIdentifier, range: lsp_types::Range, (): ()) -> Self {
Self {
text_document,
range,
work_done_progress_params: WorkDoneProgressParams::default(),
}
}
}
impl RequestTimeout for DocumentRangeFormattingParams {}
impl FromRange for DocumentRangeFormattingParams {
type Extra = FormattingOptions;
fn from_range(
text_document: TextDocumentIdentifier,
range: lsp_types::Range,
options: FormattingOptions,
) -> Self {
Self {
text_document,
range,
options,
work_done_progress_params: WorkDoneProgressParams::default(),
}
}
}
impl RequestTimeout for CodeActionParams {}
impl FromRange for CodeActionParams {
type Extra = CodeActionContext;
fn from_range(
text_document: TextDocumentIdentifier,
range: lsp_types::Range,
context: CodeActionContext,
) -> Self {
Self {
text_document,
range,
context,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
}
}
}
pub(super) trait FromItem: RequestTimeout + Sized {
type Item: From<LspHierarchyItem>;
fn from_item(item: Self::Item) -> Self;
}
macro_rules! from_item_with_partial {
($($params:ty => $item:ty),+ $(,)?) => {$(
impl RequestTimeout for $params {}
impl FromItem for $params {
type Item = $item;
fn from_item(item: $item) -> Self {
Self {
item,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
}
}
}
)+};
}
from_item_with_partial!(
CallHierarchyIncomingCallsParams => lsp_types::CallHierarchyItem,
CallHierarchyOutgoingCallsParams => lsp_types::CallHierarchyItem,
TypeHierarchySupertypesParams => lsp_types::TypeHierarchyItem,
TypeHierarchySubtypesParams => lsp_types::TypeHierarchyItem,
);
pub(super) trait DisclosedRequest: lsp_types::Request {
const CAPABILITY: DisclosedCapability;
}
macro_rules! disclosed_requests {
($($request:ty => $capability:ident),+ $(,)?) => {$(
impl DisclosedRequest for $request {
const CAPABILITY: DisclosedCapability = DisclosedCapability::$capability;
}
)+};
}
disclosed_requests!(
lsp_types::SignatureHelpRequest => SignatureHelp,
lsp_types::InlayHintRequest => InlayHints,
lsp_types::CallHierarchyPrepareRequest => CallHierarchy,
lsp_types::TypeHierarchyPrepareRequest => TypeHierarchy,
);
pub(super) trait GotoRequest: lsp_types::Request {
const CAPABILITY: Capability;
}
pub(super) trait ItemRequest: lsp_types::Request {
const CAPABILITY: Capability;
}
macro_rules! capability_requests {
($trait:ident: $($request:ty => $capability:ident),+ $(,)?) => {$(
impl $trait for $request {
const CAPABILITY: Capability = Capability::$capability;
}
)+};
}
capability_requests!(GotoRequest:
lsp_types::DefinitionRequest => Definition,
lsp_types::ImplementationRequest => Implementation,
lsp_types::TypeDefinitionRequest => TypeDefinition,
lsp_types::DeclarationRequest => Declaration,
);
capability_requests!(ItemRequest:
lsp_types::CallHierarchyIncomingCallsRequest => CallHierarchy,
lsp_types::CallHierarchyOutgoingCallsRequest => CallHierarchy,
lsp_types::TypeHierarchySupertypesRequest => TypeHierarchy,
lsp_types::TypeHierarchySubtypesRequest => TypeHierarchy,
);
#[derive(Debug)]
#[must_use = "dropping the document makes it evictable while the response is converted"]
pub(super) struct Positioned<T, D = PreparedDocument> {
pub(super) result: T,
pub(super) ctx: EncodingCtx,
pub(super) doc: D,
}
#[must_use = "a positioned call does nothing until sent"]
pub(super) struct PositionedCall<R: lsp_types::Request> {
doc: PreparedDocument,
ctx: EncodingCtx,
params: R::Params,
}
impl<R> PositionedCall<R>
where
R: lsp_types::Request,
R::Params: FromPosition,
{
pub(super) async fn send(self) -> Result<Positioned<R::Result>> {
let result = budgeted::<R>(self.doc.client(), self.params).await?;
Ok(Positioned {
result,
ctx: self.ctx,
doc: self.doc,
})
}
pub(super) async fn send_classified(
self,
) -> Positioned<std::result::Result<R::Result, UnclassifiedError>> {
let result = budgeted_classified::<R>(self.doc.client(), self.params).await;
Positioned {
result,
ctx: self.ctx,
doc: self.doc,
}
}
}
#[must_use = "dropping the document makes it evictable mid-request"]
pub(super) struct DisclosedDocument<'a> {
translator: &'a Translator,
doc: PreparedDocument,
}
impl<'a> DisclosedDocument<'a> {
pub(super) const fn new(translator: &'a Translator, doc: PreparedDocument) -> Self {
Self { translator, doc }
}
pub(super) const fn server_id(&self) -> &ServerId {
self.doc.server_id()
}
pub(super) const fn uri(&self) -> &lsp_types::Uri {
self.doc.uri()
}
pub(super) async fn request<R>(&self, params: R::Params) -> Result<Indexed<R::Result>>
where
R: lsp_types::Request,
R::Params: RequestTimeout,
{
let client = self.doc.client();
let before = self.translator.sample_indexing(self.server_id()).await;
let result = budgeted::<R>(client, params).await?;
let after = self.translator.sample_indexing(self.server_id()).await;
Ok(Indexed::new(result, before.union(after)))
}
}
impl std::fmt::Debug for DisclosedDocument<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DisclosedDocument")
.field("doc", &self.doc)
.finish_non_exhaustive()
}
}
impl Translator {
async fn sample_indexing(&self, server_id: &ServerId) -> IndexingSignal {
match &self.notification_cache {
Some(cache) => IndexingSignal::sample(&*cache.lock().await, Some(server_id)),
None => IndexingSignal::default(),
}
}
async fn positioned_params<P: FromPosition>(
&self,
server_id: &ServerId,
uri: &lsp_types::Uri,
position: Position,
extra: P::Extra,
) -> (EncodingCtx, P) {
let ctx = self.encoding_ctx(server_id);
let params = P::from_position(
TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: ctx.to_lsp(uri, position).await,
},
extra,
);
(ctx, params)
}
pub(super) async fn position_call<R>(
&self,
file_path: &ClientPath,
position: Position,
indexing_gate: IndexingGate,
extra: <R::Params as FromPosition>::Extra,
) -> Result<PositionedCall<R>>
where
R: lsp_types::Request,
R::Params: FromPosition,
{
let doc = self
.prepare_positioned_document(file_path, indexing_gate, &[position])
.await?;
let (ctx, params) = self
.positioned_params::<R::Params>(doc.server_id(), doc.uri(), position, extra)
.await;
Ok(PositionedCall { doc, ctx, params })
}
pub(super) async fn position_request<R>(
&self,
file_path: &ClientPath,
position: Position,
indexing_gate: IndexingGate,
extra: <R::Params as FromPosition>::Extra,
) -> Result<Positioned<R::Result>>
where
R: lsp_types::Request,
R::Params: FromPosition,
{
self.position_call::<R>(file_path, position, indexing_gate, extra)
.await?
.send()
.await
}
pub(super) async fn disclosed_position_request<R>(
&self,
file_path: &ClientPath,
position: Position,
extra: <R::Params as FromPosition>::Extra,
) -> Result<Positioned<Indexed<R::Result>, DisclosedDocument<'_>>>
where
R: DisclosedRequest,
R::Params: FromPosition,
{
let doc = self
.prepare_disclosed_document(file_path, R::CAPABILITY, &[position])
.await?;
let (ctx, params) = self
.positioned_params::<R::Params>(doc.server_id(), doc.uri(), position, extra)
.await;
let result = doc.request::<R>(params).await?;
Ok(Positioned { result, ctx, doc })
}
async fn ranged_params<P: FromRange>(
&self,
server_id: &ServerId,
uri: &lsp_types::Uri,
range: PositionRange,
extra: P::Extra,
) -> (EncodingCtx, P) {
let ctx = self.encoding_ctx(server_id);
let lsp_range = ctx.denormalize_range(uri, range).await;
let params = P::from_range(
TextDocumentIdentifier { uri: uri.clone() },
lsp_range,
extra,
);
(ctx, params)
}
pub(super) async fn range_request<R>(
&self,
file_path: &ClientPath,
indexing_gate: IndexingGate,
range: PositionRange,
extra: <R::Params as FromRange>::Extra,
) -> Result<Positioned<R::Result>>
where
R: lsp_types::Request,
R::Params: FromRange,
{
let doc = self
.prepare_positioned_document(file_path, indexing_gate, &[range.start(), range.end()])
.await?;
let (ctx, params) = self
.ranged_params::<R::Params>(doc.server_id(), doc.uri(), range, extra)
.await;
let result = budgeted::<R>(doc.client(), params).await?;
Ok(Positioned { result, ctx, doc })
}
pub(super) async fn disclosed_range_request<R>(
&self,
file_path: &ClientPath,
range: PositionRange,
extra: <R::Params as FromRange>::Extra,
) -> Result<Positioned<Indexed<R::Result>, DisclosedDocument<'_>>>
where
R: DisclosedRequest,
R::Params: FromRange,
{
let doc = self
.prepare_disclosed_document(file_path, R::CAPABILITY, &[range.start(), range.end()])
.await?;
let (ctx, params) = self
.ranged_params::<R::Params>(doc.server_id(), doc.uri(), range, extra)
.await;
let result = doc.request::<R>(params).await?;
Ok(Positioned { result, ctx, doc })
}
pub(super) async fn item_request<R>(
&self,
item: CheckedHierarchyItem,
) -> Result<Positioned<R::Result>>
where
R: ItemRequest,
R::Params: FromItem,
{
let item_uri = lsp_types::Uri::from(item.uri());
let path = self.parse_file_uri(&item_uri).await?;
let doc = self
.prepare_gated_document_for_path(&path, IndexingGate::Required(R::CAPABILITY))
.await?;
let ctx = self.encoding_ctx(doc.server_id());
let lsp_item = hierarchy_item_to_lsp(item, doc.uri().clone(), &ctx).await;
let result = budgeted::<R>(doc.client(), R::Params::from_item(lsp_item)).await?;
Ok(Positioned { result, ctx, doc })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn position_params() -> TextDocumentPositionParams {
TextDocumentPositionParams {
text_document: TextDocumentIdentifier {
uri: lsp_types::Uri::from("file:///ws/a.rs"),
},
position: lsp_types::Position {
line: 3,
character: 7,
},
}
}
#[test]
fn test_from_range_builds_params_for_each_range_request() {
let range = lsp_types::Range {
start: lsp_types::Position {
line: 1,
character: 2,
},
end: lsp_types::Position {
line: 3,
character: 4,
},
};
let document = position_params().text_document;
let hints = InlayHintParams::from_range(document.clone(), range, ());
assert_eq!(hints.range, range);
assert_eq!(hints.text_document, document);
let format = DocumentRangeFormattingParams::from_range(
document.clone(),
range,
FormattingOptions {
tab_size: 2,
insert_spaces: true,
..Default::default()
},
);
assert_eq!(format.range, range);
assert_eq!(format.options.tab_size, 2);
let actions = CodeActionParams::from_range(
document,
range,
CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
);
assert_eq!(actions.range, range);
}
#[test]
fn test_from_document_builds_params_for_each_document_request() {
let document = position_params().text_document;
let folding = FoldingRangeParams::from_document(document.clone(), ());
assert_eq!(folding.text_document, document);
let symbols = DocumentSymbolParams::from_document(document.clone(), ());
assert_eq!(symbols.text_document, document);
let pull = DocumentDiagnosticParams::from_document(document.clone(), ());
assert_eq!(pull.text_document, document);
assert!(pull.identifier.is_none() && pull.previous_result_id.is_none());
let format = DocumentFormattingParams::from_document(
document.clone(),
FormattingOptions {
tab_size: 8,
insert_spaces: false,
..Default::default()
},
);
assert_eq!(format.text_document, document);
assert_eq!(format.options.tab_size, 8);
}
#[test]
fn test_selection_range_params_carry_the_single_position() {
let params = SelectionRangeParams::from_position(position_params(), ());
assert_eq!(params.text_document, position_params().text_document);
assert_eq!(params.positions, [position_params().position]);
}
#[test]
fn test_request_kinds_are_tied_to_their_capability() {
assert_eq!(
<lsp_types::SignatureHelpRequest as DisclosedRequest>::CAPABILITY,
DisclosedCapability::SignatureHelp
);
assert_eq!(
<lsp_types::TypeHierarchyPrepareRequest as DisclosedRequest>::CAPABILITY,
DisclosedCapability::TypeHierarchy
);
assert_eq!(
<lsp_types::DeclarationRequest as GotoRequest>::CAPABILITY,
Capability::Declaration
);
assert_eq!(
<lsp_types::TypeHierarchySubtypesRequest as ItemRequest>::CAPABILITY,
Capability::TypeHierarchy
);
}
#[test]
fn test_from_position_keeps_position_for_progress_only_params() {
let params = HoverParams::from_position(position_params(), ());
assert_eq!(params.text_document_position_params, position_params());
}
#[test]
fn test_from_position_carries_reference_context() {
let params = ReferenceParams::from_position(
position_params(),
ReferenceContext {
include_declaration: true,
},
);
assert!(params.context.include_declaration);
assert_eq!(params.text_document_position_params, position_params());
}
#[test]
fn test_from_position_carries_rename_new_name() {
let params = RenameParams::from_position(position_params(), "renamed".to_owned());
assert_eq!(params.new_name, "renamed");
}
#[test]
fn test_from_position_signature_help_has_no_context() {
let params = SignatureHelpParams::from_position(position_params(), ());
assert!(params.context.is_none());
}
#[test]
fn test_from_position_carries_completion_context() {
let context = CompletionContext {
trigger_kind: lsp_types::CompletionTriggerKind::TriggerCharacter,
trigger_character: Some(".".to_owned()),
};
let params = CompletionParams::from_position(position_params(), Some(context));
assert_eq!(
params.context.and_then(|c| c.trigger_character).as_deref(),
Some(".")
);
}
#[test]
fn test_request_timeout_is_per_params_type() {
let client = LspClient::new(crate::config::LspServerConfig::rust_analyzer());
assert_eq!(
<CompletionParams as RequestTimeout>::timeout(&client),
client.completion_timeout()
);
assert!(client.completion_timeout() < client.request_timeout());
assert_eq!(
<InlayHintParams as RequestTimeout>::timeout(&client),
client.request_timeout()
);
assert_eq!(
<SignatureHelpParams as RequestTimeout>::timeout(&client),
client.request_timeout()
);
assert_eq!(
<lsp_types::WorkspaceSymbolParams as RequestTimeout>::timeout(&client),
client.request_timeout()
);
assert_eq!(
<lsp_types::CodeAction as RequestTimeout>::timeout(&client),
client.code_action_resolve_timeout()
);
}
}