git_ui: Support side-by-side diff view in clipboard selection diff (#51966)

Om Chillure created

#### Context

Switches `TextDiffView` from using `Editor` directly to
`SplittableEditor`, enabling side-by-side diff view support for "Diff
Clipboard with Selection". The diff view now respects the user's
`diff_view_style` setting.

Split out from #51457. This PR contains only the `SplittableEditor`
wiring. The multibuffer coordinate fix for non-singleton editors will
follow in a separate PR.

Closes #50912 (partially)

#### How to Review

Small PR — all changes are in `crates/git_ui/src/text_diff_view.rs`.
Focus on:
- `new()`: `SplittableEditor::new` replaces `Editor::for_multibuffer`,
editor-specific setup goes through `rhs_editor()`
- Item trait delegation: `act_as_type`, `for_each_project_item`,
`set_nav_history` updated for `SplittableEditor`
- Tests: pinned `DiffViewStyle::Unified` and assertions go through
`rhs_editor()`

#### Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

#### Video : 
[Screencast from 2026-03-19
23-11-36.webm](https://github.com/user-attachments/assets/c5a2381d-238d-43ef-ac6f-9994996c0c69)


#### Release Notes:

- Improved "Diff Clipboard with Selection" to support side-by-side diff
view style.

Change summary

crates/git_ui/src/text_diff_view.rs | 61 +++++++++++++++++++++---------
1 file changed, 43 insertions(+), 18 deletions(-)

Detailed changes

crates/git_ui/src/text_diff_view.rs 🔗

@@ -2,7 +2,10 @@
 
 use anyhow::Result;
 use buffer_diff::BufferDiff;
-use editor::{Editor, EditorEvent, MultiBuffer, ToPoint, actions::DiffClipboardWithSelectionData};
+use editor::{
+    Editor, EditorEvent, EditorSettings, MultiBuffer, SplittableEditor, ToPoint,
+    actions::DiffClipboardWithSelectionData,
+};
 use futures::{FutureExt, select_biased};
 use gpui::{
     AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
@@ -10,6 +13,7 @@ use gpui::{
 };
 use language::{self, Buffer, Point};
 use project::Project;
+use settings::Settings;
 use std::{
     any::{Any, TypeId},
     cmp,
@@ -22,13 +26,13 @@ use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString};
 use util::paths::PathExt;
 
 use workspace::{
-    Item, ItemHandle as _, ItemNavHistory, Workspace,
+    Item, ItemNavHistory, Workspace,
     item::{ItemEvent, SaveOptions, TabContentParams},
     searchable::SearchableItemHandle,
 };
 
 pub struct TextDiffView {
-    diff_editor: Entity<Editor>,
+    diff_editor: Entity<SplittableEditor>,
     title: SharedString,
     path: Option<SharedString>,
     buffer_changes_tx: watch::Sender<()>,
@@ -125,11 +129,11 @@ impl TextDiffView {
         );
 
         let task = window.spawn(cx, async move |cx| {
-            let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
-
             update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await?;
 
             workspace.update_in(cx, |workspace, window, cx| {
+                let project = workspace.project().clone();
+                let workspace_entity = cx.entity();
                 let diff_view = cx.new(|cx| {
                     TextDiffView::new(
                         clipboard_buffer,
@@ -138,6 +142,7 @@ impl TextDiffView {
                         expanded_selection_range,
                         diff_buffer,
                         project,
+                        workspace_entity,
                         window,
                         cx,
                     )
@@ -162,6 +167,7 @@ impl TextDiffView {
         source_range: Range<Point>,
         diff_buffer: Entity<BufferDiff>,
         project: Entity<Project>,
+        workspace: Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -174,15 +180,24 @@ impl TextDiffView {
             multibuffer
         });
         let diff_editor = cx.new(|cx| {
-            let mut editor = Editor::for_multibuffer(multibuffer, Some(project), window, cx);
-            editor.start_temporary_diff_override();
-            editor.disable_diagnostics(cx);
-            editor.set_expand_all_diff_hunks(cx);
-            editor.set_render_diff_hunk_controls(
+            let splittable = SplittableEditor::new(
+                EditorSettings::get_global(cx).diff_view_style,
+                multibuffer,
+                project,
+                workspace,
+                window,
+                cx,
+            );
+            splittable.set_render_diff_hunk_controls(
                 Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
                 cx,
             );
-            editor
+            splittable.rhs_editor().update(cx, |editor, cx| {
+                editor.start_temporary_diff_override();
+                editor.disable_diagnostics(cx);
+                editor.set_expand_all_diff_hunks(cx);
+            });
+            splittable
         });
 
         let (buffer_changes_tx, mut buffer_changes_rx) = watch::channel(());
@@ -352,12 +367,14 @@ impl Item for TextDiffView {
         &'a self,
         type_id: TypeId,
         self_handle: &'a Entity<Self>,
-        _: &'a App,
+        cx: &'a App,
     ) -> Option<gpui::AnyEntity> {
         if type_id == TypeId::of::<Self>() {
             Some(self_handle.clone().into())
-        } else if type_id == TypeId::of::<Editor>() {
+        } else if type_id == TypeId::of::<SplittableEditor>() {
             Some(self.diff_editor.clone().into())
+        } else if type_id == TypeId::of::<Editor>() {
+            Some(self.diff_editor.read(cx).rhs_editor().clone().into())
         } else {
             None
         }
@@ -372,7 +389,7 @@ impl Item for TextDiffView {
         cx: &App,
         f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
     ) {
-        self.diff_editor.for_each_project_item(cx, f)
+        self.diff_editor.read(cx).for_each_project_item(cx, f)
     }
 
     fn set_nav_history(
@@ -381,7 +398,8 @@ impl Item for TextDiffView {
         _: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.diff_editor.update(cx, |editor, _| {
+        let rhs = self.diff_editor.read(cx).rhs_editor().clone();
+        rhs.update(cx, |editor, _| {
             editor.set_nav_history(Some(nav_history));
         });
     }
@@ -463,11 +481,11 @@ impl Render for TextDiffView {
 mod tests {
     use super::*;
     use editor::{MultiBufferOffset, PathKey, test::editor_test_context::assert_state_with_diff};
-    use gpui::{TestAppContext, VisualContext};
+    use gpui::{BorrowAppContext, TestAppContext, VisualContext};
     use language::Point;
     use project::{FakeFs, Project};
     use serde_json::json;
-    use settings::SettingsStore;
+    use settings::{DiffViewStyle, SettingsStore};
     use unindent::unindent;
     use util::{path, test::marked_text_ranges};
     use workspace::MultiWorkspace;
@@ -476,6 +494,11 @@ mod tests {
         cx.update(|cx| {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings(cx, |settings| {
+                    settings.editor.diff_view_style = Some(DiffViewStyle::Unified);
+                });
+            });
             theme::init(theme::LoadThemes::JustBase, cx);
         });
     }
@@ -918,7 +941,9 @@ mod tests {
         cx.executor().run_until_parked();
 
         assert_state_with_diff(
-            &diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()),
+            &diff_view.read_with(cx, |diff_view, cx| {
+                diff_view.diff_editor.read(cx).rhs_editor().clone()
+            }),
             cx,
             expected_diff,
         );