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