From d082cfdbec1b7cb0e6e782b936e8273b69775445 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:22:34 +0200 Subject: [PATCH] lsp: Fix language servers not starting up on save (#32156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #24349 Release Notes: - Fixed language servers not starting up when a buffer is saved. --------- Co-authored-by: 张小白 <364772080@qq.com> --- crates/project/src/lsp_store.rs | 14 +++-- crates/project/src/project_tests.rs | 80 +++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index a7be336084..dd0ed856d3 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3960,6 +3960,15 @@ impl LspStore { let buffer_id = buffer.read(cx).remote_id(); let handle = cx.new(|_| buffer.clone()); if let Some(local) = self.as_local_mut() { + let refcount = local.registered_buffers.entry(buffer_id).or_insert(0); + if !ignore_refcounts { + *refcount += 1; + } + + // We run early exits on non-existing buffers AFTER we mark the buffer as registered in order to handle buffer saving. + // When a new unnamed buffer is created and saved, we will start loading it's language. Once the language is loaded, we go over all "language-less" buffers and try to fit that new language + // with them. However, we do that only for the buffers that we think are open in at least one editor; thus, we need to keep tab of unnamed buffers as well, even though they're not actually registered with any language + // servers in practice (we don't support non-file URI schemes in our LSP impl). let Some(file) = File::from_dyn(buffer.read(cx).file()) else { return handle; }; @@ -3967,11 +3976,6 @@ impl LspStore { return handle; } - let refcount = local.registered_buffers.entry(buffer_id).or_insert(0); - if !ignore_refcounts { - *refcount += 1; - } - if ignore_refcounts || *refcount == 1 { local.register_buffer_with_language_servers(buffer, cx); } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 5cd90a6a3c..2da5908b94 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3584,6 +3584,86 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text())); } +#[gpui::test(iterations = 10)] +async fn test_save_file_spawns_language_server(cx: &mut gpui::TestAppContext) { + // Issue: #24349 + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/dir"), json!({})).await; + + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + + language_registry.add(rust_lang()); + 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() + }), + text_document_sync: Some(lsp::TextDocumentSyncCapability::Options( + lsp::TextDocumentSyncOptions { + save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)), + ..Default::default() + }, + )), + ..Default::default() + }, + ..Default::default() + }, + ); + + let buffer = project + .update(cx, |this, cx| this.create_buffer(cx)) + .unwrap() + .await; + project.update(cx, |this, cx| { + this.register_buffer_with_language_servers(&buffer, cx); + buffer.update(cx, |buffer, cx| { + assert!(!this.has_language_servers_for(buffer, cx)); + }) + }); + + project + .update(cx, |this, cx| { + let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); + this.save_buffer_as( + buffer.clone(), + ProjectPath { + worktree_id, + path: Arc::from("file.rs".as_ref()), + }, + cx, + ) + }) + .await + .unwrap(); + // A server is started up, and it is notified about Rust files. + let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path(path!("/dir/file.rs")).unwrap(), + version: 0, + text: "".to_string(), + language_id: "rust".to_string(), + } + ); + + project.update(cx, |this, cx| { + buffer.update(cx, |buffer, cx| { + assert!(this.has_language_servers_for(buffer, cx)); + }) + }); +} + #[gpui::test(iterations = 30)] async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) { init_test(cx);