Ensure language servers from extension properly start on workspace restoration (#51308)

Closes #49877

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [ ] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- Fixed extension language servers not starting when Zed launches with
files already open from a restored session.
This commit is contained in:
João Soares 2026-04-16 13:26:37 -03:00 committed by GitHub
parent 7c45d93e5e
commit 11cfb9e330
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 106 additions and 2 deletions

View file

@ -319,6 +319,8 @@ impl LanguageRegistry {
state
.all_lsp_adapters
.insert(cached.name.clone(), cached.clone());
state.version += 1;
*state.subscription.0.borrow_mut() = ();
}
/// Register a fake language server and adapter
@ -354,6 +356,8 @@ impl LanguageRegistry {
state
.all_lsp_adapters
.insert(cached_adapter.name(), cached_adapter);
state.version += 1;
*state.subscription.0.borrow_mut() = ();
}
/// Register a fake language server (without the adapter)

View file

@ -4747,6 +4747,7 @@ impl LspStore {
this.update(cx, |this, cx| {
let mut plain_text_buffers = Vec::new();
let mut buffers_with_language = Vec::new();
let mut buffers_with_unknown_injections = Vec::new();
for handle in this.buffer_store.read(cx).buffers() {
let buffer = handle.read(cx);
@ -4754,8 +4755,11 @@ impl LspStore {
|| buffer.language() == Some(&*language::PLAIN_TEXT)
{
plain_text_buffers.push(handle);
} else if buffer.contains_unknown_injections() {
buffers_with_unknown_injections.push(handle);
} else {
if buffer.contains_unknown_injections() {
buffers_with_unknown_injections.push(handle.clone());
}
buffers_with_language.push(handle);
}
}
@ -4785,6 +4789,24 @@ impl LspStore {
}
}
// Also register buffers that already have a language with
// any newly-available language servers (e.g., from extensions
// that finished loading after buffers were restored).
if let Some(local) = this.as_local_mut() {
for buffer in buffers_with_language {
if local
.registered_buffers
.contains_key(&buffer.read(cx).remote_id())
{
local.register_buffer_with_language_servers(
&buffer,
HashSet::default(),
cx,
);
}
}
}
for buffer in buffers_with_unknown_injections {
buffer.update(cx, |buffer, cx| buffer.reparse(cx, false));
}

View file

@ -1915,6 +1915,84 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
);
}
#[gpui::test]
async fn test_late_lsp_adapter_registration(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({
"test.rs": "const A: i32 = 1;",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
// Add the language first so the buffer gets assigned a language.
language_registry.add(rust_lang());
cx.executor().run_until_parked();
// Open a buffer — it gets assigned the Rust language but there is no LSP adapter yet.
let (rust_buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/dir/test.rs"), cx)
})
.await
.unwrap();
rust_buffer.update(cx, |buffer, _| {
assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
});
// Now register the LSP adapter late (simulating an extension loading after startup).
let mut fake_rust_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "the-rust-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
},
);
cx.executor().run_until_parked();
// The language server should start and receive a DidOpenTextDocument notification
// for the already-open buffer.
let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
assert_eq!(
fake_rust_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
lsp::TextDocumentItem {
uri: lsp::Uri::from_file_path(path!("/dir/test.rs")).unwrap(),
version: 0,
text: "const A: i32 = 1;".to_string(),
language_id: "rust".to_string(),
}
);
// The buffer should be configured with the language server's capabilities.
rust_buffer.update(cx, |buffer, _| {
assert_eq!(
buffer
.completion_triggers()
.iter()
.cloned()
.collect::<Vec<_>>(),
&[".".to_string(), "::".to_string()]
);
});
}
#[gpui::test]
async fn test_language_server_relative_path(cx: &mut gpui::TestAppContext) {
init_test(cx);