Fix two issues with diff highlights (#24309)

Max Brunsfeld , ConradIrwin , and cole-miller created

* fix syntax highlighting of deleted text when buffer language changes
* do not highlight entire untracked files as created, except in the
project diff view

Release Notes:

- N/A

Co-authored-by: ConradIrwin <conrad.irwin@gmail.com>
Co-authored-by: cole-miller <m@cole-miller.net>

Change summary

crates/git/src/diff.rs                  | 12 ++++
crates/git_ui/src/project_diff.rs       | 16 +++--
crates/language/src/buffer.rs           | 17 +++++
crates/multi_buffer/src/multi_buffer.rs | 77 ++++++++++++++++++--------
crates/project/src/buffer_store.rs      | 56 ++++++++++++-------
5 files changed, 126 insertions(+), 52 deletions(-)

Detailed changes

crates/git/src/diff.rs 🔗

@@ -74,6 +74,18 @@ impl BufferDiff {
         }
     }
 
+    pub fn new_with_single_insertion(buffer: &BufferSnapshot) -> Self {
+        Self {
+            tree: SumTree::from_item(
+                InternalDiffHunk {
+                    buffer_range: Anchor::MIN..Anchor::MAX,
+                    diff_base_byte_range: 0..0,
+                },
+                buffer,
+            ),
+        }
+    }
+
     pub fn build(diff_base: Option<&str>, buffer: &text::BufferSnapshot) -> Self {
         let mut tree = SumTree::new(buffer);
 

crates/git_ui/src/project_diff.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
     actions, AnyElement, AnyView, App, AppContext, AsyncWindowContext, Entity, EventEmitter,
     FocusHandle, Focusable, Render, Subscription, Task, WeakEntity,
 };
-use language::{Anchor, Buffer, Capability, OffsetRangeExt};
+use language::{Anchor, Buffer, Capability, OffsetRangeExt, Point};
 use multi_buffer::{MultiBuffer, PathKey};
 use project::{buffer_store::BufferChangeSet, git::GitState, Project, ProjectPath};
 use theme::ActiveTheme;
@@ -293,11 +293,15 @@ impl ProjectDiff {
         let change_set = diff_buffer.change_set;
 
         let snapshot = buffer.read(cx).snapshot();
-        let diff_hunk_ranges = change_set
-            .read(cx)
-            .diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot)
-            .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
-            .collect::<Vec<_>>();
+        let change_set = change_set.read(cx);
+        let diff_hunk_ranges = if change_set.base_text.is_none() {
+            vec![Point::zero()..snapshot.max_point()]
+        } else {
+            change_set
+                .diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot)
+                .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
+                .collect::<Vec<_>>()
+        };
 
         self.multibuffer.update(cx, |multibuffer, cx| {
             multibuffer.set_excerpts_for_path(

crates/language/src/buffer.rs 🔗

@@ -1001,6 +1001,23 @@ impl Buffer {
         }
     }
 
+    pub fn build_empty_snapshot(cx: &mut App) -> BufferSnapshot {
+        let entity_id = cx.reserve_entity::<Self>().entity_id();
+        let buffer_id = entity_id.as_non_zero_u64().into();
+        let text =
+            TextBuffer::new_normalized(0, buffer_id, Default::default(), Rope::new()).snapshot();
+        let syntax = SyntaxMap::new(&text).snapshot();
+        BufferSnapshot {
+            text,
+            syntax,
+            file: None,
+            diagnostics: Default::default(),
+            remote_selections: Default::default(),
+            language: None,
+            non_text_state_update_count: 0,
+        }
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn build_snapshot_sync(
         text: Rope,

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -220,6 +220,22 @@ struct ChangeSetState {
     _subscription: gpui::Subscription,
 }
 
+impl ChangeSetState {
+    fn new(change_set: Entity<BufferChangeSet>, cx: &mut Context<MultiBuffer>) -> Self {
+        ChangeSetState {
+            _subscription: cx.subscribe(&change_set, |this, change_set, event, cx| match event {
+                BufferChangeSetEvent::DiffChanged { changed_range } => {
+                    this.buffer_diff_changed(change_set, changed_range.clone(), cx)
+                }
+                BufferChangeSetEvent::LanguageChanged => {
+                    this.buffer_diff_language_changed(change_set, cx)
+                }
+            }),
+            change_set,
+        }
+    }
+}
+
 /// The contents of a [`MultiBuffer`] at a single point in time.
 #[derive(Clone, Default)]
 pub struct MultiBufferSnapshot {
@@ -560,17 +576,7 @@ impl MultiBuffer {
         for (buffer_id, change_set_state) in self.diff_bases.iter() {
             diff_bases.insert(
                 *buffer_id,
-                ChangeSetState {
-                    _subscription: new_cx.subscribe(
-                        &change_set_state.change_set,
-                        |this, change_set, event, cx| match event {
-                            BufferChangeSetEvent::DiffChanged { changed_range } => {
-                                this.buffer_diff_changed(change_set, changed_range.clone(), cx)
-                            }
-                        },
-                    ),
-                    change_set: change_set_state.change_set.clone(),
-                },
+                ChangeSetState::new(change_set_state.change_set.clone(), new_cx),
             );
         }
         Self {
@@ -2146,6 +2152,30 @@ impl MultiBuffer {
         });
     }
 
+    fn buffer_diff_language_changed(
+        &mut self,
+        change_set: Entity<BufferChangeSet>,
+        cx: &mut Context<Self>,
+    ) {
+        self.sync(cx);
+        let mut snapshot = self.snapshot.borrow_mut();
+        let change_set = change_set.read(cx);
+        let buffer_id = change_set.buffer_id;
+        let base_text = change_set.base_text.clone();
+        let diff = change_set.diff_to_buffer.clone();
+        if let Some(base_text) = base_text {
+            snapshot.diffs.insert(
+                buffer_id,
+                DiffSnapshot {
+                    diff: diff.clone(),
+                    base_text,
+                },
+            );
+        } else {
+            snapshot.diffs.remove(&buffer_id);
+        }
+    }
+
     fn buffer_diff_changed(
         &mut self,
         change_set: Entity<BufferChangeSet>,
@@ -2175,6 +2205,15 @@ impl MultiBuffer {
                     base_text,
                 },
             );
+        } else if self.all_diff_hunks_expanded {
+            let base_text = Buffer::build_empty_snapshot(cx);
+            snapshot.diffs.insert(
+                buffer_id,
+                DiffSnapshot {
+                    diff: git::diff::BufferDiff::new_with_single_insertion(&base_text),
+                    base_text,
+                },
+            );
         } else {
             snapshot.diffs.remove(&buffer_id);
         }
@@ -2316,20 +2355,8 @@ impl MultiBuffer {
     pub fn add_change_set(&mut self, change_set: Entity<BufferChangeSet>, cx: &mut Context<Self>) {
         let buffer_id = change_set.read(cx).buffer_id;
         self.buffer_diff_changed(change_set.clone(), text::Anchor::MIN..text::Anchor::MAX, cx);
-        self.diff_bases.insert(
-            buffer_id,
-            ChangeSetState {
-                _subscription: cx.subscribe(
-                    &change_set,
-                    |this, change_set, event, cx| match event {
-                        BufferChangeSetEvent::DiffChanged { changed_range } => {
-                            this.buffer_diff_changed(change_set, changed_range.clone(), cx);
-                        }
-                    },
-                ),
-                change_set,
-            },
-        );
+        self.diff_bases
+            .insert(buffer_id, ChangeSetState::new(change_set, cx));
     }
 
     pub fn change_set_for(&self, buffer_id: BufferId) -> Option<Entity<BufferChangeSet>> {

crates/project/src/buffer_store.rs 🔗

@@ -85,6 +85,7 @@ struct BufferChangeSetState {
     index_text: Option<Arc<String>>,
     head_changed: bool,
     index_changed: bool,
+    language_changed: bool,
 }
 
 #[derive(Clone, Debug)]
@@ -101,8 +102,7 @@ enum DiffBasesChange {
 impl BufferChangeSetState {
     fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
         self.language = buffer.read(cx).language().cloned();
-        self.index_changed = self.index_text.is_some();
-        self.head_changed = self.head_text.is_some();
+        self.language_changed = true;
         let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
     }
 
@@ -149,34 +149,40 @@ impl BufferChangeSetState {
     ) -> oneshot::Receiver<()> {
         match diff_bases_change {
             DiffBasesChange::SetIndex(index) => {
-                let mut index = index.unwrap_or_default();
-                text::LineEnding::normalize(&mut index);
-                self.index_text = Some(Arc::new(index));
+                self.index_text = index.map(|mut index| {
+                    text::LineEnding::normalize(&mut index);
+                    Arc::new(index)
+                });
                 self.index_changed = true;
             }
             DiffBasesChange::SetHead(head) => {
-                let mut head = head.unwrap_or_default();
-                text::LineEnding::normalize(&mut head);
-                self.head_text = Some(Arc::new(head));
+                self.head_text = head.map(|mut head| {
+                    text::LineEnding::normalize(&mut head);
+                    Arc::new(head)
+                });
                 self.head_changed = true;
             }
             DiffBasesChange::SetBoth(text) => {
-                let mut text = text.unwrap_or_default();
-                text::LineEnding::normalize(&mut text);
-                self.head_text = Some(Arc::new(text));
-                self.index_text = self.head_text.clone();
+                let text = text.map(|mut text| {
+                    text::LineEnding::normalize(&mut text);
+                    Arc::new(text)
+                });
+                self.head_text = text.clone();
+                self.index_text = text;
                 self.head_changed = true;
                 self.index_changed = true;
             }
             DiffBasesChange::SetEach { index, head } => {
-                let mut index = index.unwrap_or_default();
-                text::LineEnding::normalize(&mut index);
-                let mut head = head.unwrap_or_default();
-                text::LineEnding::normalize(&mut head);
-                self.index_text = Some(Arc::new(index));
-                self.head_text = Some(Arc::new(head));
-                self.head_changed = true;
+                self.index_text = index.map(|mut index| {
+                    text::LineEnding::normalize(&mut index);
+                    Arc::new(index)
+                });
                 self.index_changed = true;
+                self.head_text = head.map(|mut head| {
+                    text::LineEnding::normalize(&mut head);
+                    Arc::new(head)
+                });
+                self.head_changed = true;
             }
         }
 
@@ -199,6 +205,7 @@ impl BufferChangeSetState {
         let index = self.index_text.clone();
         let index_changed = self.index_changed;
         let head_changed = self.head_changed;
+        let language_changed = self.language_changed;
         let index_matches_head = match (self.index_text.as_ref(), self.head_text.as_ref()) {
             (Some(index), Some(head)) => Arc::ptr_eq(index, head),
             (None, None) => true,
@@ -206,7 +213,7 @@ impl BufferChangeSetState {
         };
         self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
             if let Some(unstaged_changes) = &unstaged_changes {
-                let staged_snapshot = if index_changed {
+                let staged_snapshot = if index_changed || language_changed {
                     let staged_snapshot = cx.update(|cx| {
                         index.as_ref().map(|head| {
                             language::Buffer::build_snapshot(
@@ -238,6 +245,9 @@ impl BufferChangeSetState {
 
                 unstaged_changes.update(&mut cx, |unstaged_changes, cx| {
                     unstaged_changes.set_state(staged_snapshot.clone(), diff, &buffer, cx);
+                    if language_changed {
+                        cx.emit(BufferChangeSetEvent::LanguageChanged);
+                    }
                 })?;
             }
 
@@ -252,7 +262,7 @@ impl BufferChangeSetState {
                         )
                     })?
                 } else {
-                    let committed_snapshot = if head_changed {
+                    let committed_snapshot = if head_changed || language_changed {
                         let committed_snapshot = cx.update(|cx| {
                             head.as_ref().map(|head| {
                                 language::Buffer::build_snapshot(
@@ -284,6 +294,9 @@ impl BufferChangeSetState {
 
                 uncommitted_changes.update(&mut cx, |change_set, cx| {
                     change_set.set_state(snapshot, diff, &buffer, cx);
+                    if language_changed {
+                        cx.emit(BufferChangeSetEvent::LanguageChanged);
+                    }
                 })?;
             }
 
@@ -323,6 +336,7 @@ impl std::fmt::Debug for BufferChangeSet {
 
 pub enum BufferChangeSetEvent {
     DiffChanged { changed_range: Range<text::Anchor> },
+    LanguageChanged,
 }
 
 enum BufferStoreState {