From 14d3c2b61c5be86330ef3eee69c689714b85079d Mon Sep 17 00:00:00 2001 From: rn23thakur Date: Mon, 21 Sep 2026 11:17:46 +0000 Subject: [PATCH] ui: Add buttons to switch diff view when diffing buffers (#64529) # Objective - This PR enables users to switch between unified/split diff views when comparing buffers. - Fixes #64419 . ## Solution - The unified/split toggle (`DiffStyleControls`) is normally surfaced automatically by `BufferSearchBar`, which detects any active item exposing a `SplittableEditor`. That auto-detection is gated to multi-buffer views, since it's paired with an expand/collapse-all-hunks control that only makes sense across multiple files. `FileDiffView` (used by `zed --diff `) is a singleton buffer, so it's intentionally excluded from that generic path. - Git's own single-file diff view, [`SoloDiffView`](https://github.com/zed-industries/zed/blob/f2e33599724432f329f9e23b353a8fb28cd6ece3/crates/git_ui/src/solo_diff_view.rs#L52), hits the same limitation and works around it with a small, dedicated `ToolbarItemView` (`SoloDiffStyleToolbar`) that renders `DiffStyleControls` directly, independent of `BufferSearchBar`. - This PR follows the same pattern: a new `FileDiffStyleToolbar` (in `git_ui_core::file_diff_view`) activates whenever a `FileDiffView` is the focused pane item, and renders just the unified/split toggle, no expand/collapse or file-status controls, since neither concept applies to a plain two-file diff. It's registered in `initialize_pane` alongside the existing git toolbars. ## Testing - Did you test these changes? If so, how? Ran `cargo test -p git_ui_core`, all passed. I've also attached the results of manual testing in the Showcase section below. - Are there any parts that need more testing? I think more tests can be added. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase Adds a unified/split toggle to the diff view opened via `zed --diff `, matching the toggle already available in git's diff views.
Click to view before/after **Before**: screenshot_20260921_020610 **After**: screenshot_20260921_020355
--- Release Notes: - Added a unified/split toggle to the diff view opened via `zed --diff ` command. --------- Co-authored-by: Kirill Bulatov Co-authored-by: Kirill Bulatov --- crates/git_ui_core/src/file_diff_view.rs | 109 +++++++++++++++++++++-- crates/zed/src/zed.rs | 3 + script/bundle-mac | 8 +- 3 files changed, 111 insertions(+), 9 deletions(-) diff --git a/crates/git_ui_core/src/file_diff_view.rs b/crates/git_ui_core/src/file_diff_view.rs index 555fca0a4d..d5fa761b8f 100644 --- a/crates/git_ui_core/src/file_diff_view.rs +++ b/crates/git_ui_core/src/file_diff_view.rs @@ -3,13 +3,13 @@ use anyhow::Result; use buffer_diff::BufferDiff; use editor::{ - Editor, EditorEvent, EditorSettings, HiddenUnstagedDiffHunkRenderer, MultiBuffer, - SplittableEditor, + DiffStyleControls, Editor, EditorEvent, EditorSettings, HiddenUnstagedDiffHunkRenderer, + MultiBuffer, SplittableEditor, }; use futures::{FutureExt, select_biased}; use gpui::{ - App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Font, - IntoElement, Render, Task, WeakEntity, Window, + App, AppContext as _, AsyncApp, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, + Font, IntoElement, Render, Task, WeakEntity, Window, }; use language::{Buffer, HighlightedText, Point}; use project::{Project, ProjectPath}; @@ -21,10 +21,11 @@ use std::{ sync::Arc, time::Duration, }; -use ui::{Color, Icon, IconName, SharedString}; +use ui::{Color, Icon, IconName, SharedString, prelude::*}; use util::paths::PathExt as _; use workspace::{ - Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, + Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, + Workspace, item::{ItemEvent, SaveOptions}, searchable::SearchableItemHandle, }; @@ -128,6 +129,22 @@ impl FileDiffView { splittable }); + let include_root = project.read(cx).visible_worktrees(cx).count() > 1; + let breadcrumb_path = |buffer: &Entity| { + buffer + .read(cx) + .snapshot() + .resolve_file_path(include_root, cx) + .unwrap_or_else(|| MultiBuffer::DEFAULT_TITLE.to_string()) + }; + let old_path = breadcrumb_path(&old_buffer); + let new_path = breadcrumb_path(&new_buffer); + editor.update(cx, |editor, cx| { + editor.rhs_editor().update(cx, |editor, _| { + editor.set_breadcrumb_header(format!("{old_path} ↔ {new_path}")); + }); + }); + let (buffer_changes_tx, mut buffer_changes_rx) = watch::channel(()); // The buffers' languages may load after the diff was built, e.g. when @@ -394,6 +411,57 @@ impl Render for FileDiffView { } } +pub struct FileDiffStyleToolbar { + file_diff: Option>, +} + +impl FileDiffStyleToolbar { + pub fn new(_: &mut Context) -> Self { + Self { file_diff: None } + } + + fn file_diff(&self) -> Option> { + self.file_diff.as_ref()?.upgrade() + } +} + +impl EventEmitter for FileDiffStyleToolbar {} + +impl ToolbarItemView for FileDiffStyleToolbar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + _: &mut Window, + cx: &mut Context, + ) -> ToolbarItemLocation { + self.file_diff = active_pane_item + .and_then(|item| item.act_as::(cx)) + .map(|entity| entity.downgrade()); + + if self.file_diff.is_some() { + ToolbarItemLocation::PrimaryLeft + } else { + ToolbarItemLocation::Hidden + } + } +} + +impl Render for FileDiffStyleToolbar { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let Some(file_diff) = self.file_diff() else { + return Empty.into_any_element(); + }; + + let editor = file_diff.read(cx).editor.clone(); + + h_flex() + .pl_0p5() + .gap_1() + .child(DiffStyleControls::new(editor)) + .into_any_element() + } +} + #[cfg(test)] mod tests { use super::*; @@ -560,7 +628,34 @@ mod tests { path!("test/new_file.txt") ) ); - }) + let (breadcrumbs, _) = diff_view.breadcrumbs(cx).unwrap(); + assert_eq!( + breadcrumbs + .iter() + .map(|crumb| crumb.text.as_ref()) + .collect::>(), + vec!["old_file.txt ↔ new_file.txt"] + ); + }); + + let toolbar = cx.new(FileDiffStyleToolbar::new); + let location = toolbar.update_in(cx, |toolbar, window, cx| { + toolbar.set_active_pane_item(Some(&diff_view), window, cx) + }); + assert_eq!(location, ToolbarItemLocation::PrimaryLeft); + assert_eq!( + toolbar.read_with(cx, |toolbar, _| toolbar.file_diff()), + Some(diff_view.clone()) + ); + + let location = toolbar.update_in(cx, |toolbar, window, cx| { + toolbar.set_active_pane_item(None, window, cx) + }); + assert_eq!(location, ToolbarItemLocation::Hidden); + assert_eq!( + toolbar.read_with(cx, |toolbar, _| toolbar.file_diff()), + None + ); } #[gpui::test] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c4bf14ed8e..4034efd899 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -38,6 +38,7 @@ use git_ui::project_diff::ProjectDiffToolbar; use git_ui::solo_diff_view::{SoloDiffGitToolbar, SoloDiffStyleToolbar}; use git_ui::staged_diff::StagedDiffToolbar; use git_ui::unstaged_diff::UnstagedDiffToolbar; +use git_ui_core::file_diff_view::FileDiffStyleToolbar; use gpui::{ Action, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Element, Entity, FocusHandle, Focusable, Image, ImageFormat, KeyBinding, ParentElement, @@ -1466,6 +1467,8 @@ fn initialize_pane( toolbar.add_item(multibuffer_hint, window, cx); let solo_diff_style_toolbar = cx.new(SoloDiffStyleToolbar::new); toolbar.add_item(solo_diff_style_toolbar, window, cx); + let file_diff_style_toolbar = cx.new(FileDiffStyleToolbar::new); + toolbar.add_item(file_diff_style_toolbar, window, cx); let breadcrumbs = cx.new(|_| Breadcrumbs::new()); toolbar.add_item(breadcrumbs, window, cx); let buffer_search_bar = cx.new(|cx| { diff --git a/script/bundle-mac b/script/bundle-mac index 12deaa7ede..44f36aa1b6 100755 --- a/script/bundle-mac +++ b/script/bundle-mac @@ -4,6 +4,8 @@ set -euo pipefail source script/lib/blob-store.sh build_flag="--release" +zed_features="" +remote_server_features="" target_dir="release" open_result=false local_install=false @@ -34,6 +36,8 @@ do d) export CARGO_INCREMENTAL=true build_flag=""; + zed_features="--features util/debug-embed" + remote_server_features="--features debug-embed" target_dir="debug" ;; i) local_install=true;; @@ -87,10 +91,10 @@ script/generate-licenses rustup target add $target_triple echo "Compiling zed binaries" -cargo --config .cargo/bundle-config.toml build ${build_flag} --package zed --package cli --target $target_triple +cargo --config .cargo/bundle-config.toml build ${build_flag} ${zed_features} --package zed --package cli --target $target_triple # Build remote_server in separate invocation to prevent feature unification from other crates # from influencing dynamic libraries required by it. -cargo --config .cargo/bundle-config.toml build ${build_flag} --package remote_server --target $target_triple +cargo --config .cargo/bundle-config.toml build ${build_flag} ${remote_server_features} --package remote_server --target $target_triple echo "Creating application bundle" pushd crates/zed