lsp: Fix language servers not starting up on save (#32156)

Piotr Osiewicz and 张小白 created

Closes #24349

Release Notes:

- Fixed language servers not starting up when a buffer is saved.

---------

Co-authored-by: 张小白 <364772080@qq.com>

Change summary

crates/project/src/lsp_store.rs     | 14 +++-
crates/project/src/project_tests.rs | 80 +++++++++++++++++++++++++++++++
2 files changed, 89 insertions(+), 5 deletions(-)

Detailed changes

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);
             }

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::<lsp::notification::DidOpenTextDocument>()
+            .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);