Files
zed/crates/project/tests/integration/dynamic_registration.rs
T
4ee80bcbbb lsp: Handle dynamic registration of textDocument/documentHighlight (#63029)
Zed did not advertise document highlight dynamic registration support or
handle `textDocument/documentHighlight` registrations. Dynamically
registered providers therefore fell through to the unhandled-capability
path, while servers with a fallback had to advertise a static provider
for every attached document.

Advertise dynamic registration support and track document highlight
registrations and unregistrations alongside the other selector-aware
dynamic text document capabilities. Refresh editors whenever the
effective document highlight registration state changes, including
selector-only changes that leave the merged provider unchanged, so
runtime registrations take effect immediately and unregistrations clear
stale or in-flight highlights.

Add coverage for the client capability, registration lifecycle with
multiple selectors sharing identical provider options, and editor
refresh behavior when an applicable selector is added or removed.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed’s UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

---------

Co-authored-by: GPT-5.6 Sol <noreply@openai.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-09-09 14:55:56 +00:00

3285 lines
111 KiB
Rust

//! Tests for dynamic (un)registration of language server capabilities, handled in
//! `project::lsp_store::dynamic_registration`.
use std::collections::BTreeMap;
use pretty_assertions::assert_eq;
use project::lsp_store::TokenType;
use super::*;
#[gpui::test]
async fn test_dynamic_semantic_tokens_registration(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/the-root"),
json!({
"a.rs": "fn main() {}",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "the-language-server",
// Crucially, no `semantic_tokens_provider` is advertised statically; the
// server only offers it through dynamic registration (as Roslyn does).
..FakeLspAdapter::default()
},
);
let _buffer = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let fake_server = fake_servers.next().await.unwrap();
let server_id = fake_server.server.server_id();
cx.executor().run_until_parked();
let semantic_tokens_provider = |cx: &mut gpui::TestAppContext| {
project.read_with(cx, |project, cx| {
project
.lsp_store()
.read(cx)
.lsp_server_capabilities
.get(&server_id)
.and_then(|capabilities| capabilities.semantic_tokens_provider.clone())
})
};
assert!(
semantic_tokens_provider(cx).is_none(),
"server should not advertise semantic tokens before dynamic registration"
);
fake_server
.request::<lsp::request::RegisterCapability>(
lsp::RegistrationParams {
registrations: vec![lsp::Registration {
id: "semantic-tokens".to_string(),
method: "textDocument/semanticTokens".to_string(),
register_options: serde_json::to_value(
lsp::SemanticTokensRegistrationOptions {
text_document_registration_options:
lsp::TextDocumentRegistrationOptions {
document_selector: None,
},
semantic_tokens_options: lsp::SemanticTokensOptions {
legend: lsp::SemanticTokensLegend {
token_types: vec!["keyword".into(), "variable".into()],
token_modifiers: vec![],
},
full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
..lsp::SemanticTokensOptions::default()
},
static_registration_options: lsp::StaticRegistrationOptions {
id: None,
},
},
)
.ok(),
}],
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await
.into_response()
.unwrap();
cx.executor().run_until_parked();
let provider = semantic_tokens_provider(cx)
.expect("semantic tokens provider should be set after dynamic registration");
// The capability round-trips through capability-sync serialization, which may
// normalize the registration options into plain options; either shape is fine
// as long as the legend survives.
let legend = match provider {
lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(options) => options.legend,
lsp::SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(options) => {
options.semantic_tokens_options.legend
}
};
assert_eq!(
legend.token_types,
vec!["keyword".into(), "variable".into()],
);
fake_server
.request::<lsp::request::UnregisterCapability>(
lsp::UnregistrationParams {
unregisterations: vec![lsp::Unregistration {
id: "semantic-tokens".to_string(),
method: "textDocument/semanticTokens".to_string(),
}],
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await
.into_response()
.unwrap();
cx.executor().run_until_parked();
assert!(
semantic_tokens_provider(cx).is_none(),
"semantic tokens provider should be cleared after unregistration"
);
}
#[gpui::test]
async fn test_local_semantic_tokens_request_uses_matching_dynamic_registration(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/semanticTokens";
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let matching_options = lsp::SemanticTokensRegistrationOptions {
text_document_registration_options: lsp::TextDocumentRegistrationOptions {
document_selector: Some(vec![lsp::DocumentFilter {
language: Some("rust".to_string()),
scheme: Some("file".to_string()),
pattern: None,
}]),
},
semantic_tokens_options: lsp::SemanticTokensOptions {
legend: lsp::SemanticTokensLegend {
token_types: vec!["keyword".into()],
token_modifiers: Vec::new(),
},
full: Some(lsp::SemanticTokensFullOptions::Delta { delta: Some(true) }),
..lsp::SemanticTokensOptions::default()
},
static_registration_options: lsp::StaticRegistrationOptions::default(),
};
let nonmatching_options = lsp::SemanticTokensRegistrationOptions {
text_document_registration_options: lsp::TextDocumentRegistrationOptions {
document_selector: Some(vec![lsp::DocumentFilter {
language: Some("rust".to_string()),
scheme: Some("untitled".to_string()),
pattern: None,
}]),
},
semantic_tokens_options: lsp::SemanticTokensOptions {
legend: lsp::SemanticTokensLegend {
token_types: vec!["comment".into()],
token_modifiers: Vec::new(),
},
full: Some(lsp::SemanticTokensFullOptions::Bool(false)),
..lsp::SemanticTokensOptions::default()
},
static_registration_options: lsp::StaticRegistrationOptions::default(),
};
register_capability(
&fake_server,
method,
"file-semantic-tokens",
serde_json::to_value(matching_options).ok(),
)
.await;
register_capability(
&fake_server,
method,
"untitled-semantic-tokens",
serde_json::to_value(nonmatching_options).ok(),
)
.await;
cx.executor().run_until_parked();
let aggregate_options = match server_capabilities(&project, server_id, cx)
.semantic_tokens_provider
.expect("expected the later registration to become the aggregate capability")
{
lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(options) => options,
lsp::SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(options) => {
options.semantic_tokens_options
}
};
assert_eq!(
aggregate_options.full,
Some(lsp::SemanticTokensFullOptions::Bool(false)),
"expected the nonmatching registration to make the aggregate capability reject full requests",
);
let semantic_token_request_count = Arc::new(atomic::AtomicUsize::new(0));
fake_server.set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>({
let semantic_token_request_count = semantic_token_request_count.clone();
move |params, _| {
semantic_token_request_count.fetch_add(1, atomic::Ordering::SeqCst);
assert_eq!(
params.text_document.uri,
lsp::Uri::from_file_path(path!("/the-root/a.rs")).unwrap()
);
async move {
Ok(Some(lsp::SemanticTokensResult::Tokens(
lsp::SemanticTokens::default(),
)))
}
}
});
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.semantic_tokens(buffer.clone(), cx)
})
.await
.unwrap();
assert_eq!(
semantic_token_request_count.load(atomic::Ordering::SeqCst),
1,
"expected the matching registration to route a full semantic tokens request",
);
let stylizer_token_type = |cx: &mut gpui::TestAppContext| {
lsp_store.update(cx, |lsp_store, cx| {
let language = buffer.read(cx).language().map(|language| language.name());
lsp_store
.get_or_create_token_stylizer(server_id, language.as_ref(), cx)
.and_then(|stylizer| stylizer.token_type_name(TokenType(0)).cloned())
})
};
assert_eq!(
stylizer_token_type(cx).as_deref(),
Some("keyword"),
"expected the stylizer to use the matching registration's legend instead of the aggregate one",
);
let updated_matching_options = lsp::SemanticTokensRegistrationOptions {
text_document_registration_options: lsp::TextDocumentRegistrationOptions {
document_selector: Some(vec![lsp::DocumentFilter {
language: Some("rust".to_string()),
scheme: Some("file".to_string()),
pattern: None,
}]),
},
semantic_tokens_options: lsp::SemanticTokensOptions {
legend: lsp::SemanticTokensLegend {
token_types: vec!["function".into()],
token_modifiers: Vec::new(),
},
full: Some(lsp::SemanticTokensFullOptions::Delta { delta: Some(true) }),
..lsp::SemanticTokensOptions::default()
},
static_registration_options: lsp::StaticRegistrationOptions::default(),
};
register_capability(
&fake_server,
method,
"file-semantic-tokens-2",
serde_json::to_value(updated_matching_options).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
stylizer_token_type(cx).as_deref(),
Some("function"),
"expected the cached stylizer to be rebuilt for the newest matching registration's legend",
);
}
#[gpui::test]
async fn test_semantic_tokens_refresh_when_duplicate_removal_changes_provider_order(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/semanticTokens";
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let registration_options = |scheme: &str, token_type: &str| {
serde_json::to_value(lsp::SemanticTokensRegistrationOptions {
text_document_registration_options: lsp::TextDocumentRegistrationOptions {
document_selector: Some(vec![lsp::DocumentFilter {
language: Some("rust".to_string()),
scheme: Some(scheme.to_string()),
pattern: None,
}]),
},
semantic_tokens_options: lsp::SemanticTokensOptions {
legend: lsp::SemanticTokensLegend {
token_types: vec![token_type.to_owned().into()],
token_modifiers: Vec::new(),
},
full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
..lsp::SemanticTokensOptions::default()
},
static_registration_options: lsp::StaticRegistrationOptions::default(),
})
.ok()
};
let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
register_capability(
&fake_server,
method,
"file-keyword-a",
registration_options("file", "keyword"),
)
.await;
register_capability(
&fake_server,
method,
"file-function",
registration_options("file", "function"),
)
.await;
register_capability(
&fake_server,
method,
"file-keyword-b",
registration_options("file", "keyword"),
)
.await;
register_capability(
&fake_server,
method,
"untitled-comment",
registration_options("untitled", "comment"),
)
.await;
cx.executor().run_until_parked();
refresh_events.lock().clear();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let stylizer_token_type = |cx: &mut gpui::TestAppContext| {
lsp_store.update(cx, |lsp_store, cx| {
let language = buffer.read(cx).language().map(|language| language.name());
lsp_store
.get_or_create_token_stylizer(server_id, language.as_ref(), cx)
.and_then(|stylizer| stylizer.token_type_name(TokenType(0)).cloned())
})
};
assert_eq!(
stylizer_token_type(cx).as_deref(),
Some("keyword"),
"expected the newest matching duplicate to provide the initial legend",
);
unregister_capabilities(&fake_server, method, &["file-keyword-b"]).await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().as_slice(),
&[format!("semantic_tokens({server_id})")],
"expected removing an ordered duplicate to invalidate semantic tokens",
);
assert_eq!(
stylizer_token_type(cx).as_deref(),
Some("function"),
"expected the next matching registration to provide the updated legend",
);
}
#[gpui::test]
async fn test_completion_resolve_uses_matching_dynamic_registration(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let method = "textDocument/completion";
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
register_capability(
&fake_server,
method,
"file-completion",
Some(json!({
"documentSelector": [{ "language": "rust", "scheme": "file" }],
"resolveProvider": true,
})),
)
.await;
register_capability(
&fake_server,
method,
"untitled-completion",
Some(json!({
"documentSelector": [{ "language": "rust", "scheme": "untitled" }],
"resolveProvider": false,
})),
)
.await;
cx.executor().run_until_parked();
fake_server.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "completion".to_string(),
data: Some(json!({ "resolve": true })),
..lsp::CompletionItem::default()
},
])))
});
let resolve_requests = Arc::new(atomic::AtomicUsize::new(0));
fake_server.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
let resolve_requests = resolve_requests.clone();
move |mut item, _| {
resolve_requests.fetch_add(1, atomic::Ordering::SeqCst);
async move {
item.detail = Some("resolved".to_string());
Ok(item)
}
}
});
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let responses = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.completions(
&buffer,
PointUtf16::new(0, 0),
lsp::CompletionContext {
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
trigger_character: None,
},
cx,
)
})
.await
.unwrap();
let completions = responses
.into_iter()
.flat_map(|response| response.completions)
.collect::<Vec<_>>();
assert_eq!(completions.len(), 1);
let completions = Rc::new(RefCell::new(completions.into_boxed_slice()));
lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.resolve_completions(buffer.clone(), vec![0], completions, cx)
})
.await
.unwrap();
assert_eq!(
resolve_requests.load(atomic::Ordering::SeqCst),
1,
"expected the matching file registration to enable completion resolve",
);
}
#[gpui::test]
async fn test_code_lens_resolve_uses_matching_dynamic_registration(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let method = "textDocument/codeLens";
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
register_capability(
&fake_server,
method,
"file-code-lens",
Some(json!({
"documentSelector": [{ "language": "rust", "scheme": "file" }],
"resolveProvider": true,
})),
)
.await;
register_capability(
&fake_server,
method,
"untitled-code-lens",
Some(json!({
"documentSelector": [{ "language": "rust", "scheme": "untitled" }],
"resolveProvider": false,
})),
)
.await;
cx.executor().run_until_parked();
fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
Ok(Some(vec![lsp::CodeLens {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
command: None,
data: Some(json!({ "resolve": true })),
}]))
});
let resolve_requests = Arc::new(atomic::AtomicUsize::new(0));
fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>({
let resolve_requests = resolve_requests.clone();
move |mut lens, _| {
resolve_requests.fetch_add(1, atomic::Ordering::SeqCst);
async move {
lens.command = Some(lsp::Command {
title: "resolved".to_string(),
command: "test.resolve".to_string(),
arguments: None,
});
Ok(lens)
}
}
});
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let actions = lsp_store
.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
.await
.unwrap()
.expect("expected code lens actions");
let (lens_id, action) = actions.into_iter().next().expect("expected one code lens");
let resolved = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.resolve_code_lens(&buffer, action.server_id, lens_id, cx)
})
.await
.expect("expected the code lens to resolve")
.1;
assert_eq!(
resolve_requests.load(atomic::Ordering::SeqCst),
1,
"expected the matching file registration to enable code lens resolve",
);
assert_eq!(resolved.lsp_action.title(), "resolved");
}
#[gpui::test]
async fn test_inlay_hint_resolve_state_uses_matching_dynamic_registration(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/inlayHint";
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, "fn main() {}")], None, cx);
});
cx.executor().run_until_parked();
register_capability(
&fake_server,
method,
"file-inlay-hints",
Some(json!({
"documentSelector": [{ "language": "rust", "scheme": "file" }],
"resolveProvider": true,
})),
)
.await;
register_capability(
&fake_server,
method,
"untitled-inlay-hints",
Some(json!({
"documentSelector": [{ "language": "rust", "scheme": "untitled" }],
"resolveProvider": false,
})),
)
.await;
cx.executor().run_until_parked();
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(|_, _| async move {
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, 0),
label: lsp::InlayHintLabel::String("hint".to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: Some(json!({ "resolve": true })),
}]))
});
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let hint_tasks = lsp_store.update(cx, |lsp_store, cx| {
let range =
buffer.read(cx).anchor_before(0)..buffer.read(cx).anchor_after(buffer.read(cx).len());
lsp_store.inlay_hints(
InvalidationStrategy::None,
buffer.clone(),
vec![range],
None,
cx,
)
});
let mut hints = Vec::new();
for (_, hint_task) in hint_tasks {
for (_, server_hints) in hint_task.await.unwrap() {
hints.extend(server_hints.into_iter().map(|(_, hint)| hint));
}
}
assert_eq!(hints.len(), 1);
assert_matches!(
&hints[0].resolve_state,
ResolveState::CanResolve(id, _) if *id == server_id,
"expected the matching file registration to mark the hint as resolvable",
);
}
#[gpui::test]
async fn test_dynamic_document_highlight_registration(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/documentHighlight";
let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
assert_eq!(
server_capabilities(&project, server_id, cx).document_highlight_provider,
None,
);
let registration_options = |language: &str| {
json!({
"documentSelector": [{ "language": language, "scheme": "file" }],
})
};
let registered_provider = Some(lsp::OneOf::Right(lsp::DocumentHighlightOptions {
work_done_progress_options: lsp::WorkDoneProgressOptions::default(),
}));
register_capability(
&fake_server,
method,
"python-document-highlight",
Some(registration_options("python")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).document_highlight_provider,
registered_provider,
);
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("document_highlights({server_id})")],
"expected the first registration to refresh",
);
register_capability(
&fake_server,
method,
"rust-document-highlight",
Some(registration_options("rust")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).document_highlight_provider,
registered_provider,
);
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("document_highlights({server_id})")],
"expected adding a distinct selector with identical provider options to refresh",
);
register_capability(
&fake_server,
method,
"rust-document-highlight-duplicate",
Some(registration_options("rust")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().as_slice(),
&[] as &[String],
"expected a registration identical to an existing one to not refresh",
);
unregister_capabilities(&fake_server, method, &["rust-document-highlight-duplicate"]).await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().as_slice(),
&[] as &[String],
"expected removing a duplicate registration to not refresh",
);
unregister_capabilities(&fake_server, method, &["rust-document-highlight"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).document_highlight_provider,
registered_provider,
);
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("document_highlights({server_id})")],
"expected removing a selector while another provider remains to refresh",
);
unregister_capabilities(&fake_server, method, &["python-document-highlight"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).document_highlight_provider,
None,
);
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("document_highlights({server_id})")],
"expected the last unregistration to clear the capability and refresh",
);
}
#[gpui::test]
async fn test_dynamic_registration_sends_metadata_downstream_before_refresh(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let downstream = Arc::new(RecordingProtoClient::default());
project.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.shared(1, rpc::AnyProtoClient::new(downstream.clone()), cx);
});
});
cx.executor().run_until_parked();
downstream.sent.lock().clear();
register_capability(
&fake_server,
"textDocument/documentHighlight",
"rust-document-highlight",
Some(json!({
"documentSelector": [{ "language": "rust", "scheme": "file" }],
})),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
downstream.sent.lock().drain(..).collect::<Vec<_>>(),
vec!["UpdateLanguageServer", "RefreshDocumentHighlights"],
"expected the capability update to reach guests before the refresh that depends on it",
);
unregister_capabilities(
&fake_server,
"textDocument/documentHighlight",
&["rust-document-highlight"],
)
.await;
cx.executor().run_until_parked();
assert_eq!(
downstream.sent.lock().drain(..).collect::<Vec<_>>(),
vec!["UpdateLanguageServer", "RefreshDocumentHighlights"],
);
}
#[gpui::test]
async fn test_multi_registration_inlay_hint(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/inlayHint";
let options_a = lsp::InlayHintOptions {
resolve_provider: Some(true),
..lsp::InlayHintOptions::default()
};
let options_b = lsp::InlayHintOptions {
resolve_provider: Some(false),
..lsp::InlayHintOptions::default()
};
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
None,
"expected no inlay hint provider before any registration",
);
register_capability(
&fake_server,
method,
"inlay-hint-a",
serde_json::to_value(&options_a).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
Some(lsp::OneOf::Right(
lsp::InlayHintServerCapabilities::Options(options_a.clone())
)),
"expected the first registration's options after the first registration",
);
register_capability(
&fake_server,
method,
"inlay-hint-b",
serde_json::to_value(&options_b).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
Some(lsp::OneOf::Right(
lsp::InlayHintServerCapabilities::Options(options_b)
)),
"expected the second registration's options after the second registration",
);
unregister_capabilities(&fake_server, method, &["inlay-hint-b"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
Some(lsp::OneOf::Right(
lsp::InlayHintServerCapabilities::Options(options_a)
)),
"expected the remaining registration's options to be restored",
);
unregister_capabilities(&fake_server, method, &["inlay-hint-a"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
None,
"expected inlay hint provider to be cleared after unregistering the last registration",
);
}
#[gpui::test]
async fn test_multi_registration_code_lens(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/codeLens";
let options_a = lsp::CodeLensOptions {
resolve_provider: Some(true),
};
let options_b = lsp::CodeLensOptions {
resolve_provider: Some(false),
};
assert_eq!(
server_capabilities(&project, server_id, cx).code_lens_provider,
None,
"expected no code lens provider before any registration",
);
register_capability(
&fake_server,
method,
"code-lens-a",
serde_json::to_value(options_a).ok(),
)
.await;
register_capability(
&fake_server,
method,
"code-lens-b",
serde_json::to_value(options_b).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).code_lens_provider,
Some(options_b),
"expected the second registration's options after two registrations",
);
unregister_capabilities(&fake_server, method, &["code-lens-b"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).code_lens_provider,
Some(options_a),
"expected the remaining registration's options to be restored",
);
unregister_capabilities(&fake_server, method, &["code-lens-a"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).code_lens_provider,
None,
"expected code lens provider to be cleared after unregistering the last registration",
);
}
#[gpui::test]
async fn test_multi_registration_document_symbol(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/documentSymbol";
let options_b = lsp::DocumentSymbolOptions {
label: Some("custom".to_string()),
work_done_progress_options: lsp::WorkDoneProgressOptions::default(),
};
assert_eq!(
server_capabilities(&project, server_id, cx).document_symbol_provider,
None,
"expected no document symbol provider before any registration",
);
register_capability(&fake_server, method, "document-symbol-a", None).await;
register_capability(
&fake_server,
method,
"document-symbol-b",
serde_json::to_value(&options_b).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).document_symbol_provider,
Some(lsp::OneOf::Right(options_b)),
"expected the second registration's options after two registrations",
);
unregister_capabilities(&fake_server, method, &["document-symbol-b"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).document_symbol_provider,
Some(lsp::OneOf::Left(true)),
"expected the remaining registration's options to be restored",
);
unregister_capabilities(&fake_server, method, &["document-symbol-a"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).document_symbol_provider,
None,
"expected document symbol provider to be cleared after unregistering the last registration",
);
}
#[gpui::test]
async fn test_multi_registration_restores_static_capability(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) = setup_dynamic_registration_test(
cx,
lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
)
.await;
let server_id = fake_server.server.server_id();
let method = "textDocument/inlayHint";
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
Some(lsp::OneOf::Left(true)),
"expected the statically declared inlay hint provider before any dynamic registration",
);
let dynamic_options = lsp::InlayHintOptions {
resolve_provider: Some(true),
..lsp::InlayHintOptions::default()
};
register_capability(
&fake_server,
method,
"inlay-hint-dynamic",
serde_json::to_value(&dynamic_options).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
Some(lsp::OneOf::Right(
lsp::InlayHintServerCapabilities::Options(dynamic_options)
)),
"expected the dynamic registration's options to override the static capability",
);
unregister_capabilities(&fake_server, method, &["unknown-id", "inlay-hint-dynamic"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
Some(lsp::OneOf::Left(true)),
"expected the static capability to be restored after unregistering the last dynamic registration, despite an unknown ID earlier in the batch",
);
}
#[gpui::test]
async fn test_multi_registration_duplicate_id_keeps_order(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/inlayHint";
let options_a = lsp::InlayHintOptions {
resolve_provider: Some(true),
..lsp::InlayHintOptions::default()
};
let options_b = lsp::InlayHintOptions {
resolve_provider: Some(false),
..lsp::InlayHintOptions::default()
};
let options_a_replacement = lsp::InlayHintOptions::default();
let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
register_capability(
&fake_server,
method,
"inlay-hint-a",
serde_json::to_value(&options_a).ok(),
)
.await;
register_capability(
&fake_server,
method,
"inlay-hint-b",
serde_json::to_value(&options_b).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![
format!("inlay_hints({server_id})"),
format!("inlay_hints({server_id})"),
],
"expected both registrations to trigger a refresh",
);
register_capability(
&fake_server,
method,
"inlay-hint-a",
serde_json::to_value(&options_a_replacement).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
Some(lsp::OneOf::Right(
lsp::InlayHintServerCapabilities::Options(options_b)
)),
"expected the latest distinct registration to stay active after a duplicate ID replaced an older one",
);
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("inlay_hints({server_id})")],
"expected replacing an inactive registration to refresh selector-aware caches",
);
unregister_capabilities(&fake_server, method, &["inlay-hint-b"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
Some(lsp::OneOf::Right(
lsp::InlayHintServerCapabilities::Options(options_a_replacement)
)),
"expected the replaced registration's options to be restored",
);
unregister_capabilities(&fake_server, method, &["inlay-hint-a"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
None,
"expected inlay hint provider to be cleared after unregistering the last registration",
);
}
#[gpui::test]
async fn test_registration_with_unchanged_options_does_not_refresh(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/codeLens";
let options = lsp::CodeLensOptions {
resolve_provider: Some(true),
};
let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
register_capability(
&fake_server,
method,
"lens-a",
serde_json::to_value(options).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("code_lens({server_id})")],
"expected the first registration to refresh",
);
register_capability(
&fake_server,
method,
"lens-a",
serde_json::to_value(options).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().as_slice(),
&[] as &[String],
"expected a registration with options identical to the active ones to not refresh",
);
unregister_capabilities(&fake_server, method, &["lens-a"]).await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("code_lens({server_id})")],
"expected the last unregistration to clear the capability and refresh",
);
assert_eq!(
server_capabilities(&project, server_id, cx).code_lens_provider,
None,
"expected the code lens provider to be cleared after unregistering the last registration",
);
}
#[gpui::test]
async fn test_selector_only_registration_change_refreshes_cached_lsp_data(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/codeLens";
let registration_options = |scheme: &str| {
json!({
"documentSelector": [{ "language": "rust", "scheme": scheme }],
"resolveProvider": true,
})
};
let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
register_capability(
&fake_server,
method,
"lens",
Some(registration_options("file")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("code_lens({server_id})")],
);
register_capability(
&fake_server,
method,
"lens",
Some(registration_options("untitled")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("code_lens({server_id})")],
"expected changing only the document selector to invalidate cached LSP data",
);
}
#[gpui::test]
async fn test_duplicate_content_registration_changes_do_not_refresh(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/codeLens";
let registration_options = |scheme: &str| {
json!({
"documentSelector": [{ "language": "rust", "scheme": scheme }],
"resolveProvider": true,
})
};
let expected_options = lsp::CodeLensOptions {
resolve_provider: Some(true),
};
let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
register_capability(
&fake_server,
method,
"lens-a",
Some(registration_options("file")),
)
.await;
register_capability(
&fake_server,
method,
"lens-b",
Some(registration_options("untitled")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![
format!("code_lens({server_id})"),
format!("code_lens({server_id})"),
],
"expected registrations with distinct selectors to refresh",
);
register_capability(
&fake_server,
method,
"lens-a",
Some(registration_options("untitled")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).code_lens_provider,
Some(expected_options),
"expected the active code lens options to survive a duplicate ID replacement",
);
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("code_lens({server_id})")],
"expected a selector change of any registration to refresh",
);
unregister_capabilities(&fake_server, method, &["lens-b"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).code_lens_provider,
Some(expected_options),
"expected the duplicate-content registration to keep the capability active",
);
assert_eq!(
refresh_events.lock().as_slice(),
&[] as &[String],
"expected removing a registration whose content a remaining one duplicates to not refresh",
);
register_capability(
&fake_server,
method,
"lens-c",
Some(registration_options("untitled")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
refresh_events.lock().as_slice(),
&[] as &[String],
"expected a new registration with content identical to an existing one to not refresh",
);
unregister_capabilities(&fake_server, method, &["lens-a"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).code_lens_provider,
Some(expected_options),
"expected the capability to survive removing one of two identical registrations",
);
assert_eq!(
refresh_events.lock().as_slice(),
&[] as &[String],
"expected removing one of two identical registrations to not refresh",
);
unregister_capabilities(&fake_server, method, &["lens-c"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).code_lens_provider,
None,
"expected the code lens provider to be cleared after unregistering the last registration",
);
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("code_lens({server_id})")],
"expected the last unregistration to clear the capability and refresh",
);
}
#[gpui::test]
async fn test_selector_aware_routing_with_duplicate_content_registrations(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/completion";
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let buffer_triggers = |cx: &mut gpui::TestAppContext| {
buffer.read_with(cx, |buffer, _| buffer.completion_triggers().clone())
};
let registration_options = |scheme: &str, trigger: &str| {
json!({
"documentSelector": [{ "language": "rust", "scheme": scheme }],
"triggerCharacters": [trigger],
})
};
let colon_options = lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..lsp::CompletionOptions::default()
};
register_capability(
&fake_server,
method,
"completion-a",
Some(registration_options("file", ".")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::from([".".to_string()]),
"expected the matching registration's triggers to apply to the buffer",
);
register_capability(
&fake_server,
method,
"completion-b",
Some(registration_options("untitled", ":")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::from([".".to_string()]),
"expected the non-matching registration's triggers to be excluded from the buffer",
);
assert_eq!(
server_capabilities(&project, server_id, cx).completion_provider,
Some(colon_options.clone()),
"expected the latest registration's options to be active",
);
register_capability(
&fake_server,
method,
"completion-a",
Some(registration_options("untitled", ":")),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::new(),
"expected re-registering with a non-matching selector to remove the triggers from the buffer",
);
assert_eq!(
server_capabilities(&project, server_id, cx).completion_provider,
Some(colon_options.clone()),
"expected the active options to survive a duplicate ID replacement of an older registration",
);
unregister_capabilities(&fake_server, method, &["completion-b"]).await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::new(),
"expected the remaining duplicate-content registration to still not match the buffer",
);
assert_eq!(
server_capabilities(&project, server_id, cx).completion_provider,
Some(colon_options),
"expected the capability to survive removing one of two identical registrations",
);
unregister_capabilities(&fake_server, method, &["completion-a"]).await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::new(),
"expected no completion triggers after unregistering the last registration",
);
assert_eq!(
server_capabilities(&project, server_id, cx).completion_provider,
None,
"expected the completion provider to be cleared after unregistering the last registration",
);
}
#[gpui::test]
async fn test_multi_registration_completion_triggers(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let method = "textDocument/completion";
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let buffer_triggers = |cx: &mut gpui::TestAppContext| {
buffer.read_with(cx, |buffer, _| buffer.completion_triggers().clone())
};
let options_a = lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..lsp::CompletionOptions::default()
};
let options_b = lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..lsp::CompletionOptions::default()
};
register_capability(
&fake_server,
method,
"completion-a",
serde_json::to_value(&options_a).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::from([".".to_string()]),
"expected the first registration's triggers to be applied",
);
register_capability(
&fake_server,
method,
"completion-b",
serde_json::to_value(&options_b).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::from([".".to_string(), ":".to_string()]),
"expected global dynamic registrations to union their triggers",
);
let options_a_replacement = lsp::CompletionOptions {
trigger_characters: Some(vec!["!".to_string()]),
..lsp::CompletionOptions::default()
};
register_capability(
&fake_server,
method,
"completion-a",
serde_json::to_value(&options_a_replacement).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::from(["!".to_string(), ":".to_string()]),
"expected replacing a registration to update its triggers without dropping the others",
);
unregister_capabilities(&fake_server, method, &["completion-b"]).await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::from(["!".to_string()]),
"expected the replaced registration's triggers to be restored",
);
unregister_capabilities(&fake_server, method, &["completion-a"]).await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::new(),
"expected completion triggers to be cleared after unregistering the last registration",
);
}
#[gpui::test]
async fn test_multi_registration_middle_removal(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/inlayHint";
let options_a = lsp::InlayHintOptions {
resolve_provider: Some(true),
..lsp::InlayHintOptions::default()
};
let options_b = lsp::InlayHintOptions {
resolve_provider: Some(false),
..lsp::InlayHintOptions::default()
};
register_capability(
&fake_server,
method,
"inlay-hint-a",
serde_json::to_value(&options_a).ok(),
)
.await;
register_capability(
&fake_server,
method,
"inlay-hint-b",
serde_json::to_value(&options_b).ok(),
)
.await;
cx.executor().run_until_parked();
let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
unregister_capabilities(&fake_server, method, &["inlay-hint-a"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
Some(lsp::OneOf::Right(
lsp::InlayHintServerCapabilities::Options(options_b)
)),
"expected the latest registration to stay active after removing an older one from the middle",
);
assert_eq!(
refresh_events.lock().drain(..).collect::<Vec<_>>(),
vec![format!("inlay_hints({server_id})")],
"expected removing an inactive registration to refresh selector-aware caches",
);
unregister_capabilities(&fake_server, method, &["inlay-hint-b"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
None,
"expected inlay hint provider to be cleared after unregistering the last registration",
);
}
#[gpui::test]
async fn test_refresh_during_code_lens_fetch_does_not_resurrect_stale_data(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let method = "textDocument/codeLens";
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let lens_requests = Arc::new(atomic::AtomicUsize::new(0));
let (gate_tx, gate_rx) = futures::channel::oneshot::channel::<()>();
let gate_rx = Arc::new(Mutex::new(Some(gate_rx)));
fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>({
let lens_requests = lens_requests.clone();
let gate_rx = gate_rx.clone();
move |_, _| {
lens_requests.fetch_add(1, atomic::Ordering::Release);
let gate = gate_rx.lock().take();
async move {
if let Some(gate) = gate {
gate.await.ok();
}
Ok(Some(vec![lsp::CodeLens {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
command: Some(lsp::Command {
title: "stale lens".to_string(),
command: "lens_cmd".to_string(),
arguments: None,
}),
data: None,
}]))
}
}
});
register_capability(
&fake_server,
method,
"lens",
serde_json::to_value(lsp::CodeLensOptions {
resolve_provider: None,
})
.ok(),
)
.await;
cx.executor().run_until_parked();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let first_fetch =
lsp_store.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx));
cx.executor().advance_clock(Duration::from_millis(50));
cx.executor().run_until_parked();
assert_eq!(
lens_requests.load(atomic::Ordering::Acquire),
1,
"expected the code lens request to be in flight before the unregistration",
);
unregister_capabilities(&fake_server, method, &["lens"]).await;
cx.executor().run_until_parked();
gate_tx.send(()).unwrap();
first_fetch.await.unwrap();
cx.executor().run_until_parked();
let actions = lsp_store
.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
.await
.unwrap();
assert_eq!(
actions.map(|actions| actions.len()),
Some(0),
"expected no code lens data after the unregistration, even though a stale fetch completed after it",
);
assert_eq!(
lens_requests.load(atomic::Ordering::Acquire),
1,
"expected the unregistered server to not be queried again",
);
}
#[gpui::test]
async fn test_multi_registration_unregister_with_static_only(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) = setup_dynamic_registration_test(
cx,
lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
)
.await;
let server_id = fake_server.server.server_id();
unregister_capabilities(&fake_server, "textDocument/inlayHint", &["unknown-id"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).inlay_hint_provider,
Some(lsp::OneOf::Left(true)),
"expected the static capability to survive unregistering an unknown ID",
);
}
#[gpui::test]
async fn test_multi_registration_completion_static_restore(cx: &mut gpui::TestAppContext) {
init_test(cx);
let static_options = lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..lsp::CompletionOptions::default()
};
let (project, fake_server) = setup_dynamic_registration_test(
cx,
lsp::ServerCapabilities {
completion_provider: Some(static_options.clone()),
..lsp::ServerCapabilities::default()
},
)
.await;
let server_id = fake_server.server.server_id();
let method = "textDocument/completion";
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let buffer_triggers = |cx: &mut gpui::TestAppContext| {
buffer.read_with(cx, |buffer, _| buffer.completion_triggers().clone())
};
assert_eq!(
buffer_triggers(cx),
BTreeSet::from([".".to_string()]),
"expected the static trigger characters before any dynamic registration",
);
let dynamic_options = lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..lsp::CompletionOptions::default()
};
register_capability(
&fake_server,
method,
"completion-dynamic",
serde_json::to_value(&dynamic_options).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
buffer_triggers(cx),
BTreeSet::from([".".to_string(), ":".to_string()]),
"expected the dynamic registration's triggers to union with the static ones",
);
unregister_capabilities(&fake_server, method, &["completion-dynamic"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).completion_provider,
Some(static_options),
"expected the static completion provider to be restored",
);
assert_eq!(
buffer_triggers(cx),
BTreeSet::from([".".to_string()]),
"expected the static trigger characters to be restored",
);
}
#[gpui::test]
async fn test_multi_registration_same_id_different_methods(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let code_lens_options = lsp::CodeLensOptions {
resolve_provider: Some(true),
};
register_capability(&fake_server, "textDocument/inlayHint", "shared-id", None).await;
register_capability(
&fake_server,
"textDocument/codeLens",
"shared-id",
serde_json::to_value(code_lens_options).ok(),
)
.await;
cx.executor().run_until_parked();
unregister_capabilities(&fake_server, "textDocument/codeLens", &["shared-id"]).await;
cx.executor().run_until_parked();
let capabilities = server_capabilities(&project, server_id, cx);
assert_eq!(
capabilities.code_lens_provider, None,
"expected the code lens registration to be removed",
);
assert_eq!(
capabilities.inlay_hint_provider,
Some(lsp::OneOf::Left(true)),
"expected the inlay hint registration with the same ID to stay intact",
);
}
#[gpui::test]
async fn test_multi_registration_diagnostics(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/diagnostic";
fake_server.set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(
move |_, _| async move {
Ok(lsp::DocumentDiagnosticReportResult::Report(
lsp::DocumentDiagnosticReport::Full(
lsp::RelatedFullDocumentDiagnosticReport::default(),
),
))
},
);
let options_a = lsp::DiagnosticServerCapabilities::Options(lsp::DiagnosticOptions {
identifier: Some("diagnostics-a".to_string()),
..lsp::DiagnosticOptions::default()
});
let options_b = lsp::DiagnosticServerCapabilities::Options(lsp::DiagnosticOptions {
identifier: Some("diagnostics-b".to_string()),
..lsp::DiagnosticOptions::default()
});
register_capability(
&fake_server,
method,
"diag-a",
serde_json::to_value(&options_a).ok(),
)
.await;
register_capability(
&fake_server,
method,
"diag-b",
serde_json::to_value(&options_b).ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).diagnostic_provider,
Some(options_b),
"expected the latest diagnostic registration to be active",
);
unregister_capabilities(&fake_server, method, &["diag-b"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).diagnostic_provider,
Some(options_a.clone()),
"expected the remaining diagnostic registration to be restored",
);
unregister_capabilities(&fake_server, method, &["unknown-id"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).diagnostic_provider,
Some(options_a),
"expected an unknown unregistration ID to leave the diagnostic provider intact",
);
unregister_capabilities(&fake_server, method, &["diag-a"]).await;
cx.executor().run_until_parked();
assert_eq!(
server_capabilities(&project, server_id, cx).diagnostic_provider,
None,
"expected the diagnostic provider to be cleared after unregistering the last registration",
);
}
/// One large scenario for all per-server-refreshable LSP data kinds (document colors,
/// links, folding ranges, symbols, code lens, semantic tokens, inlay hints), asserting
/// at the data level that per-server refreshes never disturb other servers:
/// neither the second server on the same buffer, nor a server of an unrelated language.
#[gpui::test]
async fn test_per_server_refreshes_keep_other_servers_data(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/the-root"),
json!({ "a.rs": "fn main() {\n}\n", "b.md": "# title\ntext\n" }),
)
.await;
let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
language_registry.add(markdown_lang());
let mut static_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "static-server",
capabilities: all_lsp_data_server_capabilities(),
..FakeLspAdapter::default()
},
);
let mut dynamic_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "dynamic-server",
..FakeLspAdapter::default()
},
);
let mut markdown_servers = language_registry.register_fake_lsp(
"Markdown",
FakeLspAdapter {
name: "markdown-server",
capabilities: all_lsp_data_server_capabilities(),
..FakeLspAdapter::default()
},
);
cx.executor().run_until_parked();
let (rust_buffer, _rust_lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let (markdown_buffer, _markdown_lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/b.md"), cx)
})
.await
.unwrap();
let static_server = static_servers.next().await.unwrap();
let dynamic_server = dynamic_servers.next().await.unwrap();
let markdown_server = markdown_servers.next().await.unwrap();
let static_server_id = static_server.server.server_id();
let dynamic_server_id = dynamic_server.server.server_id();
let markdown_server_id = markdown_server.server.server_id();
let static_counts = serve_lsp_data(&static_server, 1);
let dynamic_counts = serve_lsp_data(&dynamic_server, 2);
let markdown_counts = serve_lsp_data(&markdown_server, 3);
let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
cx.executor().run_until_parked();
let only_static = lsp_data_labels(&[(static_server_id, 1)]);
let both_rust_servers = lsp_data_labels(&[(static_server_id, 1), (dynamic_server_id, 2)]);
let only_markdown = lsp_data_labels(&[(markdown_server_id, 3)]);
let dynamic_refresh = InvalidationStrategy::RefreshRequested {
server_id: dynamic_server_id,
};
let all_dynamic_refresh_events = vec![
format!("code_lens({dynamic_server_id})"),
format!("document_colors({dynamic_server_id})"),
format!("document_links({dynamic_server_id})"),
format!("document_symbols({dynamic_server_id})"),
format!("folding_ranges({dynamic_server_id})"),
format!("inlay_hints({dynamic_server_id})"),
format!("semantic_tokens({dynamic_server_id})"),
];
assert_eq!(
fetch_all_lsp_data(&project, &rust_buffer, InvalidationStrategy::None, cx).await,
only_static,
"expected only the statically capable server's data after the initial fetch",
);
assert_eq!(
fetch_all_lsp_data(&project, &markdown_buffer, InvalidationStrategy::None, cx).await,
only_markdown,
"expected the markdown server's data after the initial fetch",
);
assert_eq!(refresh_events.lock().as_slice(), &[] as &[String]);
register_all_lsp_data_capabilities(&dynamic_server).await;
cx.executor().run_until_parked();
assert_eq!(
sorted(refresh_events.lock().drain(..)),
all_dynamic_refresh_events,
"expected every dynamic registration to trigger the corresponding per-server refresh",
);
assert_eq!(
fetch_all_lsp_data(&project, &rust_buffer, dynamic_refresh, cx).await,
both_rust_servers,
"expected both servers' data after the dynamic registrations",
);
assert_eq!(
fetch_all_lsp_data(&project, &markdown_buffer, InvalidationStrategy::None, cx).await,
only_markdown,
"expected the registrations of another language's server to not disturb markdown data",
);
dynamic_server
.request::<lsp::request::CodeLensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
.await
.into_response()
.unwrap();
dynamic_server
.request::<lsp::request::SemanticTokensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
.await
.into_response()
.unwrap();
dynamic_server
.request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
.await
.into_response()
.unwrap();
cx.executor().run_until_parked();
assert_eq!(
sorted(refresh_events.lock().drain(..)),
vec![
format!("code_lens({dynamic_server_id})"),
format!("inlay_hints({dynamic_server_id})"),
format!("semantic_tokens({dynamic_server_id})"),
],
"expected the server-sent refresh requests to trigger per-server refreshes",
);
assert_eq!(
fetch_all_lsp_data(&project, &rust_buffer, dynamic_refresh, cx).await,
both_rust_servers,
"expected the per-server refresh requests to keep the other server's data",
);
assert_eq!(
fetch_all_lsp_data(&project, &markdown_buffer, InvalidationStrategy::None, cx).await,
only_markdown,
"expected another language server's refresh requests to not disturb markdown data",
);
unregister_all_lsp_data_capabilities(&dynamic_server).await;
cx.executor().run_until_parked();
assert_eq!(
sorted(refresh_events.lock().drain(..)),
all_dynamic_refresh_events,
"expected every unregistration to trigger the corresponding per-server refresh",
);
assert_eq!(
fetch_all_lsp_data(&project, &rust_buffer, dynamic_refresh, cx).await,
only_static,
"expected the unregistrations to drop only the dynamic server's data",
);
assert_eq!(
fetch_all_lsp_data(&project, &markdown_buffer, InvalidationStrategy::None, cx).await,
only_markdown,
"expected the unregistrations of another language's server to not disturb markdown data",
);
assert_eq!(
*static_counts.lock(),
BTreeMap::from_iter([
("code_lens", 1),
("colors", 1),
("document_symbols", 1),
("folding_ranges", 1),
("inlay_hints", 1),
("links", 1),
("semantic_tokens", 1),
]),
"expected the static server to never be re-queried by the other server's changes",
);
assert_eq!(
*dynamic_counts.lock(),
BTreeMap::from_iter([
("code_lens", 2),
("colors", 1),
("document_symbols", 1),
("folding_ranges", 1),
("inlay_hints", 2),
("links", 1),
("semantic_tokens", 2),
]),
"expected the dynamic server to be re-queried only after registering and for the kinds it requested refreshes for",
);
assert_eq!(
*markdown_counts.lock(),
BTreeMap::from_iter([
("code_lens", 1),
("colors", 1),
("document_symbols", 1),
("folding_ranges", 1),
("inlay_hints", 1),
("links", 1),
("semantic_tokens", 1),
]),
"expected the markdown server to never be re-queried by the rust servers' changes",
);
}
/// Unlike [`test_per_server_refreshes_keep_other_servers_data`], focuses on the
/// dynamic capability changes as the refresh trigger: how they combine data from
/// static and dynamic capabilities (e.g. completion trigger characters), and which
/// servers get [re-]queried after the capability changes.
#[gpui::test]
async fn test_dynamic_registration_refreshes_lsp_data(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/the-root"), json!({ "a.rs": "fn main() {\n}\n" }))
.await;
let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut static_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "static-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..lsp::CompletionOptions::default()
}),
..all_lsp_data_server_capabilities()
},
..FakeLspAdapter::default()
},
);
let mut dynamic_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "dynamic-server",
..FakeLspAdapter::default()
},
);
cx.executor().run_until_parked();
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let static_server = static_servers.next().await.unwrap();
let dynamic_server = dynamic_servers.next().await.unwrap();
let dynamic_server_id = dynamic_server.server.server_id();
let static_counts = serve_lsp_data(&static_server, 1);
let dynamic_counts = serve_lsp_data(&dynamic_server, 2);
let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
let buffer_triggers = |cx: &mut gpui::TestAppContext| {
buffer.read_with(cx, |buffer, _| buffer.completion_triggers().clone())
};
cx.executor().run_until_parked();
let all_kinds_queried_once = BTreeMap::from_iter([
("code_lens", 1),
("colors", 1),
("document_symbols", 1),
("folding_ranges", 1),
("inlay_hints", 1),
("links", 1),
("semantic_tokens", 1),
]);
let all_dynamic_refresh_events = vec![
format!("code_lens({dynamic_server_id})"),
format!("document_colors({dynamic_server_id})"),
format!("document_links({dynamic_server_id})"),
format!("document_symbols({dynamic_server_id})"),
format!("folding_ranges({dynamic_server_id})"),
format!("inlay_hints({dynamic_server_id})"),
format!("semantic_tokens({dynamic_server_id})"),
];
fetch_all_lsp_data(&project, &buffer, InvalidationStrategy::None, cx).await;
assert_eq!(
*static_counts.lock(),
all_kinds_queried_once,
"expected the initial fetch to query the statically capable server",
);
assert_eq!(
*dynamic_counts.lock(),
BTreeMap::new(),
"expected the initial fetch to skip the server without capabilities",
);
assert_eq!(buffer_triggers(cx), BTreeSet::from([".".to_string()]));
assert_eq!(refresh_events.lock().as_slice(), &[] as &[String]);
register_all_lsp_data_capabilities(&dynamic_server).await;
register_capability(
&dynamic_server,
"textDocument/completion",
"completions",
serde_json::to_value(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..lsp::CompletionOptions::default()
})
.ok(),
)
.await;
cx.executor().run_until_parked();
assert_eq!(
sorted(refresh_events.lock().drain(..)),
all_dynamic_refresh_events,
"expected every dynamic registration to trigger the corresponding per-server refresh",
);
assert_eq!(
buffer_triggers(cx),
BTreeSet::from([".".to_string(), ":".to_string()]),
"expected trigger characters from both servers to be combined",
);
fetch_all_lsp_data(
&project,
&buffer,
InvalidationStrategy::RefreshRequested {
server_id: dynamic_server_id,
},
cx,
)
.await;
assert_eq!(
*dynamic_counts.lock(),
all_kinds_queried_once,
"expected the newly registered server to be queried after the refreshes",
);
assert_eq!(
*static_counts.lock(),
all_kinds_queried_once,
"expected per-server refreshes to leave the static server's cached data untouched",
);
unregister_all_lsp_data_capabilities(&dynamic_server).await;
unregister_capabilities(&dynamic_server, "textDocument/completion", &["completions"]).await;
cx.executor().run_until_parked();
assert_eq!(
sorted(refresh_events.lock().drain(..)),
all_dynamic_refresh_events,
"expected every unregistration to trigger the corresponding per-server refresh",
);
assert_eq!(
buffer_triggers(cx),
BTreeSet::from([".".to_string()]),
"expected only the static server's trigger characters to remain",
);
fetch_all_lsp_data(
&project,
&buffer,
InvalidationStrategy::RefreshRequested {
server_id: dynamic_server_id,
},
cx,
)
.await;
assert_eq!(
*dynamic_counts.lock(),
all_kinds_queried_once,
"expected the unregistered server to not be queried anymore",
);
assert_eq!(
*static_counts.lock(),
all_kinds_queried_once,
"expected the static server to never be re-queried by another server's capability changes",
);
}
#[gpui::test]
async fn test_semantic_tokens_server_set_shrink_invalidates_completed_task(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let (project, fake_server) =
setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
let server_id = fake_server.server.server_id();
let method = "textDocument/semanticTokens";
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
register_capability(
&fake_server,
method,
"semantic-tokens",
serde_json::to_value(lsp::SemanticTokensRegistrationOptions {
text_document_registration_options: lsp::TextDocumentRegistrationOptions {
document_selector: None,
},
semantic_tokens_options: lsp::SemanticTokensOptions {
legend: lsp::SemanticTokensLegend {
token_types: vec!["keyword".into()],
token_modifiers: Vec::new(),
},
full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
..lsp::SemanticTokensOptions::default()
},
static_registration_options: lsp::StaticRegistrationOptions::default(),
})
.ok(),
)
.await;
fake_server.set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
|_, _| async move {
Ok(Some(lsp::SemanticTokensResult::Tokens(
lsp::SemanticTokens {
result_id: None,
data: vec![0, 0, 2, 0, 0],
},
)))
},
);
cx.executor().run_until_parked();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let initial_tokens = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.semantic_tokens(buffer.clone(), cx)
})
.await
.unwrap();
assert_eq!(
initial_tokens
.tokens
.as_ref()
.map(|tokens| tokens.keys().copied().collect::<HashSet<_>>()),
Some(HashSet::from_iter([server_id])),
);
unregister_capabilities(&fake_server, method, &["semantic-tokens"]).await;
cx.executor().run_until_parked();
let tokens_after_unregistration = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.semantic_tokens(buffer.clone(), cx)
})
.await
.unwrap();
assert!(
tokens_after_unregistration
.tokens
.is_none_or(|tokens| tokens.is_empty()),
"expected the completed semantic-token task to drop the inapplicable server",
);
}
#[gpui::test]
async fn test_semantic_tokens_refresh_invalidates_only_the_refreshed_server(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/the-root"), json!({ "a.rs": "fn main() {}" }))
.await;
let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let semantic_tokens_capabilities = lsp::ServerCapabilities {
semantic_tokens_provider: Some(
lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
lsp::SemanticTokensOptions {
full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
..lsp::SemanticTokensOptions::default()
},
),
),
..lsp::ServerCapabilities::default()
};
let mut servers_a = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "server-a",
capabilities: semantic_tokens_capabilities.clone(),
..FakeLspAdapter::default()
},
);
let mut servers_b = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "server-b",
capabilities: semantic_tokens_capabilities,
..FakeLspAdapter::default()
},
);
cx.executor().run_until_parked();
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
let server_a = servers_a.next().await.unwrap();
let server_b = servers_b.next().await.unwrap();
for server in [&server_a, &server_b] {
server.set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
move |_, _| async move {
Ok(Some(lsp::SemanticTokensResult::Tokens(
lsp::SemanticTokens::default(),
)))
},
);
}
let server_a_id = server_a.server.server_id();
let server_b_id = server_b.server.server_id();
cx.executor().run_until_parked();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let cached_token_servers = |cx: &mut gpui::TestAppContext| {
lsp_store.read_with(cx, |lsp_store, _| {
lsp_store.semantic_token_servers(buffer_id)
})
};
lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.semantic_tokens(buffer.clone(), cx)
})
.await
.unwrap();
assert_eq!(
cached_token_servers(cx),
vec![server_a_id, server_b_id],
"expected tokens from both servers after the initial fetch",
);
for repetition in 0..2 {
server_a
.request::<lsp::request::SemanticTokensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
.await
.into_response()
.unwrap();
let refresh_task = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.semantic_tokens(buffer.clone(), cx)
});
assert_eq!(
cached_token_servers(cx),
vec![server_b_id],
"expected refresh {repetition} to invalidate only the refreshed server's tokens",
);
let concurrent_task = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.semantic_tokens(buffer.clone(), cx)
});
assert_eq!(
cached_token_servers(cx),
vec![server_b_id],
"expected a concurrent query {repetition} to not invalidate the data again",
);
refresh_task.await.unwrap();
concurrent_task.await.unwrap();
assert_eq!(
cached_token_servers(cx),
vec![server_a_id, server_b_id],
"expected the refreshed server's tokens to be re-fetched after refresh {repetition}",
);
}
}
#[gpui::test]
async fn test_semantic_tokens_refresh_during_fetch_does_not_resurrect_stale_data(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let (project, fake_server) = setup_dynamic_registration_test(
cx,
lsp::ServerCapabilities {
semantic_tokens_provider: Some(
lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
lsp::SemanticTokensOptions {
full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
..lsp::SemanticTokensOptions::default()
},
),
),
..lsp::ServerCapabilities::default()
},
)
.await;
let server_id = fake_server.server.server_id();
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
cx.executor().run_until_parked();
let stale_data = vec![0, 0, 2, 0, 0];
let fresh_data = vec![0, 0, 2, 0, 0, 0, 3, 4, 0, 0];
let token_requests = Arc::new(atomic::AtomicUsize::new(0));
let (gate_tx, gate_rx) = futures::channel::oneshot::channel::<()>();
let gate_rx = Arc::new(Mutex::new(Some(gate_rx)));
fake_server.set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>({
let token_requests = token_requests.clone();
let gate_rx = gate_rx.clone();
let stale_data = stale_data.clone();
let fresh_data = fresh_data.clone();
move |_, _| {
let request = token_requests.fetch_add(1, atomic::Ordering::Release);
let gate = gate_rx.lock().take();
let data = if request == 0 {
stale_data.clone()
} else {
fresh_data.clone()
};
async move {
if let Some(gate) = gate {
gate.await.ok();
}
Ok(Some(lsp::SemanticTokensResult::Tokens(
lsp::SemanticTokens {
result_id: None,
data,
},
)))
}
}
});
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let stale_fetch = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.semantic_tokens(buffer.clone(), cx)
});
cx.executor().run_until_parked();
assert_eq!(
token_requests.load(atomic::Ordering::Acquire),
1,
"expected the first fetch to be in flight before the refresh",
);
fake_server
.request::<lsp::request::SemanticTokensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
.await
.into_response()
.unwrap();
cx.executor().run_until_parked();
let fresh_fetch = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.semantic_tokens(buffer.clone(), cx)
});
fresh_fetch.await.unwrap();
gate_tx.send(()).unwrap();
stale_fetch.await.unwrap();
cx.executor().run_until_parked();
assert_eq!(
lsp_store.read_with(cx, |lsp_store, _| lsp_store.semantic_token_data(buffer_id)),
vec![(server_id, fresh_data)],
"expected the stale fetch, completed after the refresh, to not overwrite the refreshed tokens",
);
assert_eq!(
token_requests.load(atomic::Ordering::Acquire),
2,
"expected exactly the stale and the fresh fetches to have queried the server",
);
}
#[gpui::test]
async fn test_code_lens_concurrent_fetches_are_deduplicated(cx: &mut gpui::TestAppContext) {
init_test(cx);
let (project, fake_server) = setup_dynamic_registration_test(
cx,
lsp::ServerCapabilities {
code_lens_provider: Some(lsp::CodeLensOptions {
resolve_provider: None,
}),
..lsp::ServerCapabilities::default()
},
)
.await;
let (buffer, _lsp_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
cx.executor().run_until_parked();
let lens_requests = Arc::new(atomic::AtomicUsize::new(0));
fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>({
let lens_requests = lens_requests.clone();
move |_, _| {
lens_requests.fetch_add(1, atomic::Ordering::Release);
async move { Ok(Some(Vec::new())) }
}
});
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let first_fetch =
lsp_store.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx));
let second_fetch =
lsp_store.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx));
cx.executor().advance_clock(Duration::from_millis(50));
first_fetch.await.unwrap();
second_fetch.await.unwrap();
assert_eq!(
lens_requests.load(atomic::Ordering::Acquire),
1,
"expected concurrent code lens fetches to share one LSP request",
);
lsp_store
.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
.await
.unwrap();
assert_eq!(
lens_requests.load(atomic::Ordering::Acquire),
1,
"expected a repeated fetch for the unchanged buffer to be served from the cache",
);
}
#[gpui::test]
async fn test_multiple_did_change_watched_files_registrations(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"src": {
"a.rs": "",
"b.rs": "",
},
"docs": {
"readme.md": "",
},
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "the-language-server",
..FakeLspAdapter::default()
},
);
cx.executor().run_until_parked();
project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/root/src/a.rs"), cx)
})
.await
.unwrap();
let fake_server = fake_servers.next().await.unwrap();
cx.executor().run_until_parked();
let file_changes = Arc::new(Mutex::new(Vec::new()));
// Register two separate watched file registrations.
register_capability(
&fake_server,
"workspace/didChangeWatchedFiles",
"reg-1",
serde_json::to_value(lsp::DidChangeWatchedFilesRegistrationOptions {
watchers: vec![lsp::FileSystemWatcher {
glob_pattern: lsp::GlobPattern::String(path!("/root/src/*.rs").to_string()),
kind: None,
}],
})
.ok(),
)
.await;
register_capability(
&fake_server,
"workspace/didChangeWatchedFiles",
"reg-2",
serde_json::to_value(lsp::DidChangeWatchedFilesRegistrationOptions {
watchers: vec![lsp::FileSystemWatcher {
glob_pattern: lsp::GlobPattern::String(path!("/root/docs/*.md").to_string()),
kind: None,
}],
})
.ok(),
)
.await;
fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
let file_changes = file_changes.clone();
move |params, _| {
let mut file_changes = file_changes.lock();
file_changes.extend(params.changes);
file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
}
});
cx.executor().run_until_parked();
// Both registrations should match their respective patterns.
fs.create_file(
path!("/root/src/c.rs").as_ref(),
fs::CreateOptions::default(),
)
.await
.unwrap();
fs.create_file(
path!("/root/docs/guide.md").as_ref(),
fs::CreateOptions::default(),
)
.await
.unwrap();
cx.executor().run_until_parked();
assert_eq!(
&*file_changes.lock(),
&[
lsp::FileEvent {
uri: lsp::Uri::from_file_path(path!("/root/docs/guide.md")).unwrap(),
typ: lsp::FileChangeType::CREATED,
},
lsp::FileEvent {
uri: lsp::Uri::from_file_path(path!("/root/src/c.rs")).unwrap(),
typ: lsp::FileChangeType::CREATED,
},
]
);
file_changes.lock().clear();
// Unregister the first registration.
unregister_capabilities(&fake_server, "workspace/didChangeWatchedFiles", &["reg-1"]).await;
cx.executor().run_until_parked();
// Only the second registration should still match.
fs.create_file(
path!("/root/src/d.rs").as_ref(),
fs::CreateOptions::default(),
)
.await
.unwrap();
fs.create_file(
path!("/root/docs/notes.md").as_ref(),
fs::CreateOptions::default(),
)
.await
.unwrap();
cx.executor().run_until_parked();
assert_eq!(
&*file_changes.lock(),
&[lsp::FileEvent {
uri: lsp::Uri::from_file_path(path!("/root/docs/notes.md")).unwrap(),
typ: lsp::FileChangeType::CREATED,
}]
);
}
async fn setup_dynamic_registration_test(
cx: &mut gpui::TestAppContext,
capabilities: lsp::ServerCapabilities,
) -> (Entity<Project>, lsp::FakeLanguageServer) {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/the-root"), json!({ "a.rs": "" }))
.await;
let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "the-language-server",
capabilities,
..FakeLspAdapter::default()
},
);
cx.executor().run_until_parked();
project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
})
.await
.unwrap();
let fake_server = fake_servers.next().await.unwrap();
cx.executor().run_until_parked();
(project, fake_server)
}
async fn register_capability(
fake_server: &lsp::FakeLanguageServer,
method: &str,
id: &str,
register_options: Option<serde_json::Value>,
) {
fake_server
.request::<lsp::request::RegisterCapability>(
lsp::RegistrationParams {
registrations: vec![lsp::Registration {
id: id.to_string(),
method: method.to_string(),
register_options,
}],
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await
.into_response()
.unwrap();
}
async fn unregister_capabilities(
fake_server: &lsp::FakeLanguageServer,
method: &str,
ids: &[&str],
) {
fake_server
.request::<lsp::request::UnregisterCapability>(
lsp::UnregistrationParams {
unregisterations: ids
.iter()
.map(|id| lsp::Unregistration {
id: id.to_string(),
method: method.to_string(),
})
.collect(),
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await
.into_response()
.unwrap();
}
fn server_capabilities(
project: &Entity<Project>,
server_id: LanguageServerId,
cx: &mut gpui::TestAppContext,
) -> lsp::ServerCapabilities {
project.read_with(cx, |project, cx| {
project
.lsp_store()
.read(cx)
.language_server_for_id(server_id)
.unwrap()
.capabilities()
})
}
fn observe_refresh_events(
project: &Entity<Project>,
cx: &mut gpui::TestAppContext,
) -> (Arc<Mutex<Vec<String>>>, gpui::Subscription) {
let events = Arc::new(Mutex::new(Vec::new()));
let subscription = cx.update({
let events = events.clone();
let project = project.clone();
move |cx| {
cx.subscribe(&project, move |_, event, _| {
let label = |name: &str, server_id: &Option<LanguageServerId>| {
let server_id = server_id
.map_or_else(|| "all".to_string(), |server_id| server_id.to_string());
format!("{name}({server_id})")
};
let event = match event {
Event::RefreshInlayHints { server_id, .. } => {
format!("inlay_hints({server_id})")
}
Event::RefreshSemanticTokens { server_id, .. } => {
format!("semantic_tokens({server_id})")
}
Event::RefreshCodeLens { server_id } => label("code_lens", server_id),
Event::RefreshDocumentColors { server_id } => {
label("document_colors", server_id)
}
Event::RefreshDocumentLinks { server_id } => label("document_links", server_id),
Event::RefreshDocumentHighlights { server_id } => {
label("document_highlights", server_id)
}
Event::RefreshFoldingRanges { server_id } => label("folding_ranges", server_id),
Event::RefreshDocumentSymbols { server_id } => {
label("document_symbols", server_id)
}
_ => return,
};
events.lock().push(event);
})
}
});
(events, subscription)
}
fn sorted(events: impl IntoIterator<Item = String>) -> Vec<String> {
let mut events = events.into_iter().collect::<Vec<_>>();
events.sort();
events
}
fn all_lsp_data_server_capabilities() -> lsp::ServerCapabilities {
lsp::ServerCapabilities {
color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
document_link_provider: Some(lsp::DocumentLinkOptions {
resolve_provider: None,
work_done_progress_options: lsp::WorkDoneProgressOptions::default(),
}),
folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
document_symbol_provider: Some(lsp::OneOf::Left(true)),
code_lens_provider: Some(lsp::CodeLensOptions {
resolve_provider: None,
}),
semantic_tokens_provider: Some(
lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
lsp::SemanticTokensOptions {
legend: lsp::SemanticTokensLegend {
token_types: vec!["keyword".into()],
token_modifiers: Vec::new(),
},
full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
..lsp::SemanticTokensOptions::default()
},
),
),
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
}
}
async fn register_all_lsp_data_capabilities(fake_server: &lsp::FakeLanguageServer) {
register_capability(fake_server, "textDocument/documentColor", "colors", None).await;
register_capability(
fake_server,
"textDocument/documentLink",
"links",
serde_json::to_value(lsp::DocumentLinkOptions {
resolve_provider: None,
work_done_progress_options: lsp::WorkDoneProgressOptions::default(),
})
.ok(),
)
.await;
register_capability(fake_server, "textDocument/foldingRange", "folding", None).await;
register_capability(fake_server, "textDocument/documentSymbol", "symbols", None).await;
register_capability(
fake_server,
"textDocument/codeLens",
"code-lens",
serde_json::to_value(lsp::CodeLensOptions {
resolve_provider: None,
})
.ok(),
)
.await;
register_capability(
fake_server,
"textDocument/semanticTokens",
"tokens",
serde_json::to_value(lsp::SemanticTokensRegistrationOptions {
text_document_registration_options: lsp::TextDocumentRegistrationOptions {
document_selector: None,
},
semantic_tokens_options: lsp::SemanticTokensOptions {
legend: lsp::SemanticTokensLegend {
token_types: vec!["keyword".into()],
token_modifiers: Vec::new(),
},
full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
..lsp::SemanticTokensOptions::default()
},
static_registration_options: lsp::StaticRegistrationOptions::default(),
})
.ok(),
)
.await;
register_capability(fake_server, "textDocument/inlayHint", "hints", None).await;
}
async fn unregister_all_lsp_data_capabilities(fake_server: &lsp::FakeLanguageServer) {
unregister_capabilities(fake_server, "textDocument/documentColor", &["colors"]).await;
unregister_capabilities(fake_server, "textDocument/documentLink", &["links"]).await;
unregister_capabilities(fake_server, "textDocument/foldingRange", &["folding"]).await;
unregister_capabilities(fake_server, "textDocument/documentSymbol", &["symbols"]).await;
unregister_capabilities(fake_server, "textDocument/codeLens", &["code-lens"]).await;
unregister_capabilities(fake_server, "textDocument/semanticTokens", &["tokens"]).await;
unregister_capabilities(fake_server, "textDocument/inlayHint", &["hints"]).await;
}
type LspRequestCounts = Arc<Mutex<BTreeMap<&'static str, usize>>>;
/// Serves distinct data, derived from `index`, for every LSP data kind, and counts the
/// requests served per kind. [`lsp_data_labels`] builds the expected fetch results for
/// the same `index`.
fn serve_lsp_data(fake_server: &lsp::FakeLanguageServer, index: u8) -> LspRequestCounts {
let counts = LspRequestCounts::default();
fake_server.set_request_handler::<lsp::request::DocumentColor, _, _>({
let counts = counts.clone();
move |_, _| {
*counts.lock().entry("colors").or_default() += 1;
async move {
Ok(vec![lsp::ColorInformation {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
color: lsp::Color {
red: 0.0,
green: 0.0,
blue: 0.0,
alpha: f32::from(index),
},
}])
}
}
});
fake_server.set_request_handler::<lsp::request::DocumentLinkRequest, _, _>({
let counts = counts.clone();
move |_, _| {
*counts.lock().entry("links").or_default() += 1;
async move {
Ok(Some(vec![lsp::DocumentLink {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
target: Some(format!("https://server-{index}.example/").parse().unwrap()),
tooltip: None,
data: None,
}]))
}
}
});
fake_server.set_request_handler::<lsp::request::FoldingRangeRequest, _, _>({
let counts = counts.clone();
move |_, _| {
*counts.lock().entry("folding_ranges").or_default() += 1;
async move {
Ok(Some(vec![lsp::FoldingRange {
start_line: 0,
start_character: None,
end_line: 1,
end_character: None,
kind: None,
collapsed_text: Some(format!("fold-{index}")),
}]))
}
}
});
fake_server.set_request_handler::<lsp::request::DocumentSymbolRequest, _, _>({
let counts = counts.clone();
move |_, _| {
*counts.lock().entry("document_symbols").or_default() += 1;
async move {
#[allow(deprecated)]
Ok(Some(lsp::DocumentSymbolResponse::Nested(vec![
lsp::DocumentSymbol {
name: format!("symbol-{index}"),
detail: None,
kind: lsp::SymbolKind::FUNCTION,
tags: None,
deprecated: None,
range: lsp::Range::default(),
selection_range: lsp::Range::default(),
children: None,
},
])))
}
}
});
fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>({
let counts = counts.clone();
move |_, _| {
*counts.lock().entry("code_lens").or_default() += 1;
async move {
Ok(Some(vec![lsp::CodeLens {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
command: Some(lsp::Command {
title: format!("lens-{index}"),
command: "test.command".to_string(),
arguments: None,
}),
data: None,
}]))
}
}
});
fake_server.set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>({
let counts = counts.clone();
move |_, _| {
*counts.lock().entry("semantic_tokens").or_default() += 1;
async move {
Ok(Some(lsp::SemanticTokensResult::Tokens(
lsp::SemanticTokens {
result_id: None,
data: semantic_token_test_data(index),
},
)))
}
}
});
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>({
let counts = counts.clone();
move |_, _| {
*counts.lock().entry("inlay_hints").or_default() += 1;
async move {
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, 0),
label: lsp::InlayHintLabel::String(format!("hint-{index}")),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
}
});
counts
}
fn semantic_token_test_data(index: u8) -> Vec<u32> {
vec![0, u32::from(index - 1) * 4, 1, 0, 0]
}
#[derive(Debug, Default, PartialEq)]
struct LspDataLabels {
colors: Vec<String>,
links: Vec<String>,
folding_ranges: Vec<String>,
document_symbols: Vec<String>,
code_lens: Vec<String>,
semantic_tokens: Vec<(LanguageServerId, Vec<u32>)>,
inlay_hints: Vec<(LanguageServerId, Vec<String>)>,
}
impl LspDataLabels {
fn sort(&mut self) {
self.colors.sort();
self.links.sort();
self.folding_ranges.sort();
self.document_symbols.sort();
self.code_lens.sort();
self.semantic_tokens.sort();
self.inlay_hints.sort();
}
}
/// The expected [`fetch_all_lsp_data`] results for servers set up with [`serve_lsp_data`].
fn lsp_data_labels(servers: &[(LanguageServerId, u8)]) -> LspDataLabels {
let mut labels = LspDataLabels::default();
for &(server_id, index) in servers {
labels.colors.push(format!("color-{index}"));
labels
.links
.push(format!("https://server-{index}.example/"));
labels.folding_ranges.push(format!("fold-{index}"));
labels.document_symbols.push(format!("symbol-{index}"));
labels.code_lens.push(format!("lens-{index}"));
labels
.semantic_tokens
.push((server_id, semantic_token_test_data(index)));
labels
.inlay_hints
.push((server_id, vec![format!("hint-{index}")]));
}
labels.sort();
labels
}
async fn fetch_all_lsp_data(
project: &Entity<Project>,
buffer: &Entity<Buffer>,
inlay_hints_invalidate: InvalidationStrategy,
cx: &mut gpui::TestAppContext,
) -> LspDataLabels {
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
let colors = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.document_colors(buffer.clone(), cx)
});
let links = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.fetch_document_links(buffer, cx)
});
let folding_ranges = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.fetch_folding_ranges(buffer, cx)
});
let document_symbols = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.fetch_document_symbols(buffer, cx)
});
let code_lens = lsp_store.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(buffer, cx));
let semantic_tokens = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.semantic_tokens(buffer.clone(), cx)
});
let inlay_hint_tasks = lsp_store.update(cx, |lsp_store, cx| {
let query_range =
buffer.read(cx).anchor_before(0)..buffer.read(cx).anchor_after(buffer.read(cx).len());
lsp_store.inlay_hints(
inlay_hints_invalidate,
buffer.clone(),
vec![query_range],
None,
cx,
)
});
let mut labels = LspDataLabels::default();
if let Some(colors) = colors {
labels.colors.extend(
colors
.await
.unwrap()
.colors
.into_iter()
.map(|color| format!("color-{}", color.color.alpha as u8)),
);
}
labels.links.extend(
links
.await
.unwrap_or_default()
.values()
.flat_map(|server_links| server_links.values())
.filter_map(|link| link.target.clone())
.map(|target| target.to_string()),
);
labels.folding_ranges.extend(
folding_ranges
.await
.into_iter()
.filter_map(|folding_range| folding_range.collapsed_text)
.map(|collapsed_text| collapsed_text.to_string()),
);
labels.document_symbols.extend(
document_symbols
.await
.into_iter()
.map(|symbol| symbol.text.to_string()),
);
labels.code_lens.extend(
code_lens
.await
.unwrap()
.unwrap_or_default()
.values()
.map(|action| action.lsp_action.title().to_string()),
);
semantic_tokens.await.unwrap();
labels.semantic_tokens =
lsp_store.read_with(cx, |lsp_store, _| lsp_store.semantic_token_data(buffer_id));
for (_, hints_task) in inlay_hint_tasks {
for (server_id, server_hints) in hints_task.await.unwrap() {
let hint_labels = server_hints
.into_iter()
.map(|(_, hint)| hint.text().to_string())
.collect::<Vec<_>>();
if !hint_labels.is_empty() {
labels.inlay_hints.push((server_id, hint_labels));
}
}
}
labels.sort();
labels
}
#[derive(Default)]
struct RecordingProtoClient {
sent: Mutex<Vec<&'static str>>,
handler_set: Mutex<rpc::ProtoMessageHandlerSet>,
}
impl rpc::ProtoClient for RecordingProtoClient {
fn request(
&self,
_: rpc::proto::Envelope,
_: &'static str,
) -> futures::future::BoxFuture<'static, Result<rpc::proto::Envelope>> {
unimplemented!()
}
fn send(&self, _: rpc::proto::Envelope, message_type: &'static str) -> Result<()> {
self.sent.lock().push(message_type);
Ok(())
}
fn send_response(&self, _: rpc::proto::Envelope, _: &'static str) -> Result<()> {
Ok(())
}
fn message_handler_set(&self) -> &Mutex<rpc::ProtoMessageHandlerSet> {
&self.handler_set
}
fn is_via_collab(&self) -> bool {
false
}
fn has_wsl_interop(&self) -> bool {
false
}
}