git: Add toolbar buttons for `SplittableEditor` (#48323)

Cole Miller created

Release Notes:

- N/A

Change summary

Cargo.lock                         |   2 
crates/git_ui/Cargo.toml           |   1 
crates/git_ui/src/project_diff.rs  |  35 -------
crates/icons/src/icons.rs          |   2 
crates/search/Cargo.toml           |   1 
crates/search/src/buffer_search.rs | 137 ++++++++++++++++++++++++++-----
6 files changed, 120 insertions(+), 58 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7273,7 +7273,6 @@ dependencies = [
  "ctor",
  "db",
  "editor",
- "feature_flags",
  "futures 0.3.31",
  "fuzzy",
  "git",
@@ -14842,6 +14841,7 @@ dependencies = [
  "client",
  "collections",
  "editor",
+ "feature_flags",
  "futures 0.3.31",
  "gpui",
  "itertools 0.14.0",

crates/git_ui/Cargo.toml 🔗

@@ -27,7 +27,6 @@ command_palette_hooks.workspace = true
 component.workspace = true
 db.workspace = true
 editor.workspace = true
-feature_flags.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 git.workspace = true

crates/git_ui/src/project_diff.rs 🔗

@@ -8,13 +8,12 @@ use anyhow::{Context as _, Result, anyhow};
 use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
 use collections::{HashMap, HashSet};
 use editor::{
-    Addon, Editor, EditorEvent, SelectionEffects, SplitDiffFeatureFlag, SplittableEditor,
-    ToggleSplitDiff,
+    Addon, Editor, EditorEvent, SelectionEffects, SplittableEditor,
     actions::{GoToHunk, GoToPreviousHunk, SendReviewToAgent},
     multibuffer_context_lines,
     scroll::Autoscroll,
 };
-use feature_flags::FeatureFlagAppExt as _;
+
 use git::{
     Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
     repository::{Branch, RepoPath, Upstream, UpstreamTracking, UpstreamTrackingStatus},
@@ -480,7 +479,6 @@ impl ProjectDiff {
     }
 
     fn button_states(&self, cx: &App) -> ButtonStates {
-        let is_split = self.editor.read(cx).is_split();
         let editor = self.editor.read(cx).rhs_editor().read(cx);
         let snapshot = self.multibuffer.read(cx).snapshot(cx);
         let prev_next = snapshot.diff_hunks().nth(1).is_some();
@@ -541,7 +539,6 @@ impl ProjectDiff {
             selection,
             stage_all,
             unstage_all,
-            is_split,
         }
     }
 
@@ -987,6 +984,8 @@ impl Item for ProjectDiff {
             Some(self_handle.clone().into())
         } else if type_id == TypeId::of::<Editor>() {
             Some(self.editor.read(cx).rhs_editor().clone().into())
+        } else if type_id == TypeId::of::<SplittableEditor>() {
+            Some(self.editor.clone().into())
         } else {
             None
         }
@@ -1293,7 +1292,6 @@ struct ButtonStates {
     selection: bool,
     stage_all: bool,
     unstage_all: bool,
-    is_split: bool,
 }
 
 impl Render for ProjectDiffToolbar {
@@ -1433,31 +1431,6 @@ impl Render for ProjectDiffToolbar {
                             )
                         },
                     )
-                    .map(|this| {
-                        if !cx.has_flag::<SplitDiffFeatureFlag>() {
-                            return this;
-                        }
-                        this.child(
-                            Button::new(
-                                "toggle-split",
-                                if button_states.is_split {
-                                    "Stacked View"
-                                } else {
-                                    "Split View"
-                                },
-                            )
-                            .tooltip(Tooltip::for_action_title_in(
-                                "Toggle Split View",
-                                &ToggleSplitDiff,
-                                &focus_handle,
-                            ))
-                            .on_click(cx.listener(
-                                |this, _, window, cx| {
-                                    this.dispatch_action(&ToggleSplitDiff, window, cx);
-                                },
-                            )),
-                        )
-                    })
                     .child(
                         Button::new("commit", "Commit")
                             .tooltip(Tooltip::for_action_title_in(

crates/icons/src/icons.rs 🔗

@@ -92,6 +92,8 @@ pub enum IconName {
     DebugStepOut,
     DebugStepOver,
     Diff,
+    DiffSplit,
+    DiffStacked,
     Disconnected,
     Download,
     EditorAtom,

crates/search/Cargo.toml 🔗

@@ -26,6 +26,7 @@ any_vec.workspace = true
 bitflags.workspace = true
 collections.workspace = true
 editor.workspace = true
+feature_flags.workspace = true
 futures.workspace = true
 gpui.workspace = true
 language.workspace = true

crates/search/src/buffer_search.rs 🔗

@@ -13,14 +13,16 @@ use crate::{
 use any_vec::AnyVec;
 use collections::HashMap;
 use editor::{
-    DisplayPoint, Editor, EditorSettings, MultiBufferOffset,
+    DisplayPoint, Editor, EditorSettings, MultiBufferOffset, SplitDiffFeatureFlag,
+    SplittableEditor, ToggleSplitDiff,
     actions::{Backtab, FoldAll, Tab, ToggleFoldAll, UnfoldAll},
 };
+use feature_flags::FeatureFlagAppExt as _;
 use futures::channel::oneshot;
 use gpui::{
     Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,
     IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,
-    Window, actions, div,
+    WeakEntity, Window, actions, div,
 };
 use language::{Language, LanguageRegistry};
 use project::{
@@ -132,6 +134,8 @@ pub struct BufferSearchBar {
     editor_needed_width: Pixels,
     regex_language: Option<Arc<Language>>,
     is_collapsed: bool,
+    splittable_editor: Option<WeakEntity<SplittableEditor>>,
+    _splittable_editor_subscription: Option<Subscription>,
 }
 
 impl EventEmitter<Event> for BufferSearchBar {}
@@ -140,32 +144,99 @@ impl Render for BufferSearchBar {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let focus_handle = self.focus_handle(cx);
 
+        let has_splittable_editor =
+            self.splittable_editor.is_some() && cx.has_flag::<SplitDiffFeatureFlag>();
+        let split_buttons = if has_splittable_editor {
+            self.splittable_editor
+                .as_ref()
+                .and_then(|weak| weak.upgrade())
+                .map(|splittable_editor| {
+                    let is_split = splittable_editor.read(cx).is_split();
+                    let focus_handle = splittable_editor.focus_handle(cx);
+                    h_flex()
+                        .gap_0p5()
+                        .child(
+                            IconButton::new("diff-stacked", IconName::DiffStacked)
+                                .shape(IconButtonShape::Square)
+                                .toggle_state(!is_split)
+                                .tooltip(|_, cx| {
+                                    Tooltip::for_action("Stacked", &ToggleSplitDiff, cx)
+                                })
+                                .when(is_split, |button| {
+                                    let focus_handle = focus_handle.clone();
+                                    button.on_click(move |_, window, cx| {
+                                        focus_handle.focus(window, cx);
+                                        window.dispatch_action(ToggleSplitDiff.boxed_clone(), cx);
+                                    })
+                                }),
+                        )
+                        .child(
+                            IconButton::new("diff-split", IconName::DiffSplit)
+                                .shape(IconButtonShape::Square)
+                                .toggle_state(is_split)
+                                .tooltip(|_, cx| {
+                                    Tooltip::for_action("Side by Side", &ToggleSplitDiff, cx)
+                                })
+                                .when(!is_split, |button| {
+                                    button.on_click({
+                                        let focus_handle = focus_handle.clone();
+                                        move |_, window, cx| {
+                                            focus_handle.focus(window, cx);
+                                            window
+                                                .dispatch_action(ToggleSplitDiff.boxed_clone(), cx);
+                                        }
+                                    })
+                                }),
+                        )
+                })
+        } else {
+            None
+        };
+
         let collapse_expand_button = if self.needs_expand_collapse_option(cx) {
             let query_editor_focus = self.query_editor.focus_handle(cx);
 
             let (icon, label, tooltip_label) = if self.is_collapsed {
-                (
-                    IconName::ChevronUpDown,
-                    "Expand All",
-                    "Expand All Search Results",
-                )
+                (IconName::ChevronUpDown, "Expand All", "Expand All Files")
             } else {
                 (
                     IconName::ChevronDownUp,
                     "Collapse All",
-                    "Collapse All Search Results",
+                    "Collapse All Files",
                 )
             };
 
             if self.dismissed {
-                let button = Button::new("multibuffer-collapse-expand-empty", label)
+                if has_splittable_editor {
+                    return h_flex()
+                        .gap_1()
+                        .child(
+                            IconButton::new("multibuffer-collapse-expand-empty", icon)
+                                .shape(IconButtonShape::Square)
+                                .tooltip(move |_, cx| {
+                                    Tooltip::for_action_in(
+                                        tooltip_label,
+                                        &ToggleFoldAll,
+                                        &query_editor_focus,
+                                        cx,
+                                    )
+                                })
+                                .on_click(|_event, window, cx| {
+                                    window.dispatch_action(ToggleFoldAll.boxed_clone(), cx)
+                                }),
+                        )
+                        .children(split_buttons)
+                        .into_any_element();
+                }
+
+                return Button::new("multibuffer-collapse-expand-empty", label)
                     .icon_position(IconPosition::Start)
                     .icon(icon)
                     .tooltip(move |_, cx| {
                         Tooltip::for_action_in(
                             tooltip_label,
                             &ToggleFoldAll,
-                            &query_editor_focus.clone(),
+                            &query_editor_focus,
                             cx,
                         )
                     })
@@ -173,24 +244,27 @@ impl Render for BufferSearchBar {
                         window.dispatch_action(ToggleFoldAll.boxed_clone(), cx)
                     })
                     .into_any_element();
-
-                return button;
             }
 
             Some(
-                IconButton::new("multibuffer-collapse-expand", icon)
-                    .shape(IconButtonShape::Square)
-                    .tooltip(move |_, cx| {
-                        Tooltip::for_action_in(
-                            tooltip_label,
-                            &ToggleFoldAll,
-                            &query_editor_focus,
-                            cx,
-                        )
-                    })
-                    .on_click(|_event, window, cx| {
-                        window.dispatch_action(ToggleFoldAll.boxed_clone(), cx)
-                    })
+                h_flex()
+                    .gap_1()
+                    .child(
+                        IconButton::new("multibuffer-collapse-expand", icon)
+                            .shape(IconButtonShape::Square)
+                            .tooltip(move |_, cx| {
+                                Tooltip::for_action_in(
+                                    tooltip_label,
+                                    &ToggleFoldAll,
+                                    &query_editor_focus,
+                                    cx,
+                                )
+                            })
+                            .on_click(|_event, window, cx| {
+                                window.dispatch_action(ToggleFoldAll.boxed_clone(), cx)
+                            }),
+                    )
+                    .children(split_buttons)
                     .into_any_element(),
             )
         } else {
@@ -558,9 +632,20 @@ impl ToolbarItemView for BufferSearchBar {
         cx.notify();
         self.active_searchable_item_subscriptions.take();
         self.active_searchable_item.take();
+        self.splittable_editor = None;
+        self._splittable_editor_subscription = None;
 
         self.pending_search.take();
 
+        if let Some(splittable_editor) = item
+            .and_then(|item| item.act_as_type(TypeId::of::<SplittableEditor>(), cx))
+            .and_then(|entity| entity.downcast::<SplittableEditor>().ok())
+        {
+            self._splittable_editor_subscription =
+                Some(cx.observe(&splittable_editor, |_, _, cx| cx.notify()));
+            self.splittable_editor = Some(splittable_editor.downgrade());
+        }
+
         if let Some(searchable_item_handle) =
             item.and_then(|item| item.to_searchable_item_handle(cx))
         {
@@ -818,6 +903,8 @@ impl BufferSearchBar {
             editor_needed_width: px(0.),
             regex_language: None,
             is_collapsed: false,
+            splittable_editor: None,
+            _splittable_editor_subscription: None,
         }
     }