lsp: Do not drop lsp buffer handle from editor when a language change leads to buffer having a legit language (#44469)

Piotr Osiewicz created

Fixes a bug that led to us unnecessarily restarting a language server
when we were looking at a single file of a given language.

Release Notes:

- Fixed a bug that led to Zed sometimes starting an excessive amount of
language servers

Change summary

crates/copilot/src/copilot.rs                     |  2 +-
crates/editor/src/editor.rs                       |  6 ++++--
crates/git_ui/src/file_diff_view.rs               |  2 +-
crates/git_ui/src/text_diff_view.rs               |  2 +-
crates/language/src/buffer.rs                     | 13 ++++++++-----
crates/multi_buffer/src/multi_buffer.rs           |  6 ++++--
crates/project/src/buffer_store.rs                |  2 +-
crates/project/src/git_store.rs                   |  2 +-
crates/project/src/lsp_store.rs                   |  2 +-
crates/toolchain_selector/src/active_toolchain.rs |  2 +-
10 files changed, 23 insertions(+), 16 deletions(-)

Detailed changes

crates/copilot/src/copilot.rs 🔗

@@ -807,7 +807,7 @@ impl Copilot {
                         .ok();
                 }
                 language::BufferEvent::FileHandleChanged
-                | language::BufferEvent::LanguageChanged => {
+                | language::BufferEvent::LanguageChanged(_) => {
                     let new_language_id = id_for_language(buffer.read(cx).language());
                     let Ok(new_uri) = uri_for_buffer(&buffer, cx) else {
                         return Ok(());

crates/editor/src/editor.rs 🔗

@@ -22007,8 +22007,10 @@ impl Editor {
             multi_buffer::Event::DiffHunksToggled => {
                 self.tasks_update_task = Some(self.refresh_runnables(window, cx));
             }
-            multi_buffer::Event::LanguageChanged(buffer_id) => {
-                self.registered_buffers.remove(&buffer_id);
+            multi_buffer::Event::LanguageChanged(buffer_id, is_fresh_language) => {
+                if !is_fresh_language {
+                    self.registered_buffers.remove(&buffer_id);
+                }
                 jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
                 cx.emit(EditorEvent::Reparsed(*buffer_id));
                 cx.notify();

crates/git_ui/src/file_diff_view.rs 🔗

@@ -108,7 +108,7 @@ impl FileDiffView {
         for buffer in [&old_buffer, &new_buffer] {
             cx.subscribe(buffer, move |this, _, event, _| match event {
                 language::BufferEvent::Edited
-                | language::BufferEvent::LanguageChanged
+                | language::BufferEvent::LanguageChanged(_)
                 | language::BufferEvent::Reparsed => {
                     this.buffer_changes_tx.send(()).ok();
                 }

crates/git_ui/src/text_diff_view.rs 🔗

@@ -170,7 +170,7 @@ impl TextDiffView {
 
         cx.subscribe(&source_buffer, move |this, _, event, _| match event {
             language::BufferEvent::Edited
-            | language::BufferEvent::LanguageChanged
+            | language::BufferEvent::LanguageChanged(_)
             | language::BufferEvent::Reparsed => {
                 this.buffer_changes_tx.send(()).ok();
             }

crates/language/src/buffer.rs 🔗

@@ -1,8 +1,8 @@
 pub mod row_chunk;
 
 use crate::{
-    DebuggerTextObject, LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
-    TextObject, TreeSitterOptions,
+    DebuggerTextObject, LanguageScope, Outline, OutlineConfig, PLAIN_TEXT, RunnableCapture,
+    RunnableTag, TextObject, TreeSitterOptions,
     diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup},
     language_settings::{LanguageSettings, language_settings},
     outline::OutlineItem,
@@ -353,7 +353,8 @@ pub enum BufferEvent {
     /// The buffer is in need of a reload
     ReloadNeeded,
     /// The buffer's language was changed.
-    LanguageChanged,
+    /// The boolean indicates whether this buffer did not have a language before, but does now.
+    LanguageChanged(bool),
     /// The buffer's syntax trees were updated.
     Reparsed,
     /// The buffer's diagnostics were updated.
@@ -1386,10 +1387,12 @@ impl Buffer {
     ) {
         self.non_text_state_update_count += 1;
         self.syntax_map.lock().clear(&self.text);
-        self.language = language;
+        let old_language = std::mem::replace(&mut self.language, language);
         self.was_changed();
         self.reparse(cx, may_block);
-        cx.emit(BufferEvent::LanguageChanged);
+        let has_fresh_language =
+            self.language.is_some() && old_language.is_none_or(|old| old == *PLAIN_TEXT);
+        cx.emit(BufferEvent::LanguageChanged(has_fresh_language));
     }
 
     /// Assign a language registry to the buffer. This allows the buffer to retrieve

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -129,7 +129,7 @@ pub enum Event {
         transaction_id: TransactionId,
     },
     Reloaded,
-    LanguageChanged(BufferId),
+    LanguageChanged(BufferId, bool),
     Reparsed(BufferId),
     Saved,
     FileHandleChanged,
@@ -2294,7 +2294,9 @@ impl MultiBuffer {
             BufferEvent::Saved => Event::Saved,
             BufferEvent::FileHandleChanged => Event::FileHandleChanged,
             BufferEvent::Reloaded => Event::Reloaded,
-            BufferEvent::LanguageChanged => Event::LanguageChanged(buffer_id),
+            BufferEvent::LanguageChanged(has_language) => {
+                Event::LanguageChanged(buffer_id, *has_language)
+            }
             BufferEvent::Reparsed => Event::Reparsed(buffer_id),
             BufferEvent::DiagnosticsUpdated => Event::DiagnosticsUpdated,
             BufferEvent::CapabilityChanged => {

crates/project/src/git_store.rs 🔗

@@ -1451,7 +1451,7 @@ impl GitStore {
         match event {
             BufferStoreEvent::BufferAdded(buffer) => {
                 cx.subscribe(buffer, |this, buffer, event, cx| {
-                    if let BufferEvent::LanguageChanged = event {
+                    if let BufferEvent::LanguageChanged(_) = event {
                         let buffer_id = buffer.read(cx).remote_id();
                         if let Some(diff_state) = this.diffs.get(&buffer_id) {
                             diff_state.update(cx, |diff_state, cx| {

crates/project/src/lsp_store.rs 🔗

@@ -219,7 +219,7 @@ struct UnifiedLanguageServer {
     project_roots: HashSet<Arc<RelPath>>,
 }
 
-#[derive(Clone, Hash, PartialEq, Eq)]
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
 struct LanguageServerSeed {
     worktree_id: WorktreeId,
     name: LanguageServerName,

crates/toolchain_selector/src/active_toolchain.rs 🔗

@@ -124,7 +124,7 @@ impl ActiveToolchain {
                 &buffer,
                 window,
                 |this, _, event: &BufferEvent, window, cx| {
-                    if matches!(event, BufferEvent::LanguageChanged) {
+                    if matches!(event, BufferEvent::LanguageChanged(_)) {
                         this._update_toolchain_task = Self::spawn_tracker_task(window, cx);
                     }
                 },