diff --git a/assets/icons/square_dot.svg b/assets/icons/square_dot.svg index 72b32734399..a56ced30861 100644 --- a/assets/icons/square_dot.svg +++ b/assets/icons/square_dot.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/square_minus.svg b/assets/icons/square_minus.svg index 5ba458e8b53..31b2c97d74b 100644 --- a/assets/icons/square_minus.svg +++ b/assets/icons/square_minus.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/square_plus.svg b/assets/icons/square_plus.svg index 063c7dbf826..88ada58011f 100644 --- a/assets/icons/square_plus.svg +++ b/assets/icons/square_plus.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index ba731ec3b9e..9f51e53b292 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -28,7 +28,7 @@ use std::{ ops::Range, sync::Arc, }; -use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider}; +use ui::{CommonAnimationExt, Divider, IconButtonShape, KeyBinding, Tooltip, prelude::*}; use util::ResultExt; use workspace::{ Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, @@ -1099,7 +1099,7 @@ impl Render for AgentDiffToolbar { }), ) .into_any_element(), - vertical_divider().into_any_element(), + Divider::vertical().into_any_element(), h_flex() .gap_0p5() .child( @@ -1141,7 +1141,7 @@ impl Render for AgentDiffToolbar { .mr_1() .gap_1() .children(content) - .child(vertical_divider()) + .child(Divider::vertical()) .when_some(editor.read(cx).workspace(), |this, _workspace| { this.child( IconButton::new("review", IconName::ListTodo) @@ -1158,7 +1158,7 @@ impl Render for AgentDiffToolbar { }), ) }) - .child(vertical_divider()) + .child(Divider::vertical()) .on_action({ let editor = editor.clone(); move |_action: &OpenAgentDiff, window, cx| { diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index c2f06da5261..90ea600ff5f 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -4913,7 +4913,7 @@ impl ThreadView { ); }), ) - .child(Divider::vertical().h_4()) + .child(Divider::vertical()) .into_any_element(), ) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d2eb44ecc87..1d224ad40b4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -103,7 +103,7 @@ pub use editor_settings::{ }; pub use element::{ CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, - render_breadcrumb_text, + file_status_label_color, render_breadcrumb_text, }; pub use git::blame::BlameRenderer; pub(crate) use git::{DiffHunkKey, StoredReviewComment}; @@ -124,7 +124,7 @@ pub use multi_buffer::{ MultiBufferOffset, MultiBufferOffsetUtf16, MultiBufferSnapshot, PathKey, RowInfo, ToOffset, ToPoint, }; -pub use split::{SplittableEditor, ToggleSplitDiff}; +pub use split::{DiffStyleControls, SplittableEditor, ToggleSplitDiff}; pub use split_editor_view::SplitEditorView; pub use text::Bias; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 80f6bf7fab9..e5836446cee 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3,6 +3,7 @@ mod mouse; #[cfg(test)] pub(crate) use header::StickyHeader; +pub use header::file_status_label_color; pub(crate) use header::{header_jump_data, render_buffer_header}; use crate::{ diff --git a/crates/editor/src/element/header.rs b/crates/editor/src/element/header.rs index 9340ee6ac11..a6611b0c90a 100644 --- a/crates/editor/src/element/header.rs +++ b/crates/editor/src/element/header.rs @@ -21,7 +21,7 @@ use text::BufferId; use theme::ActiveTheme; use ui::{ ButtonLike, ContextMenu, DiffStat, Indicator, KeyBinding, Tooltip, prelude::*, - right_click_menu, text_for_keystroke, + right_click_menu, text_for_keystroke, utils::WithRemSize, }; use util::ResultExt; use workspace::{ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel}; @@ -691,6 +691,9 @@ pub(crate) fn render_buffer_header( let opaque_window = cx.theme().window_background_appearance() == WindowBackgroundAppearance::Opaque; + let show_open_file_button = + can_open_excerpts && relative_path.is_some() && (is_selected || header_hovered); + let header = div() .id(("buffer-header", buffer_id.to_proto())) .on_hover(move |hovered, _window, cx| { @@ -713,7 +716,6 @@ pub(crate) fn render_buffer_header( .pr_2() .rounded_sm() .gap_1p5() - .when(is_sticky && opaque_window, |el| el.shadow_md()) .border_1() .map(|border| { let border_color = @@ -724,10 +726,9 @@ pub(crate) fn render_buffer_header( }; border.border_color(border_color) }) - .when(opaque_window, |el| { - el.bg(colors.editor_subheader_background) - }) - .hover(|style| style.bg(colors.element_hover)) + .when(is_sticky && opaque_window, |s| s.shadow_md()) + .when(opaque_window, |s| s.bg(colors.editor_subheader_background)) + .hover(|s| s.bg(colors.element_hover)) .map(|header| { let editor = editor.clone(); let buffer_id = for_excerpt.buffer_id(); @@ -906,46 +907,46 @@ pub(crate) fn render_buffer_header( }) }, )) - .when_some(diff_stat, |this, (added, removed)| { - this.child( - div().flex_shrink_0().child( - DiffStat::new( + .child( + h_flex() + .gap_2() + .when_some(diff_stat, |this, (added, removed)| { + let ui_font_size = + theme_settings::ThemeSettings::get_global(cx) + .ui_font_size(cx); + this.child(WithRemSize::new(ui_font_size).child(DiffStat::new( ("buffer-header-diff-stat", buffer_id.to_proto()), added as usize, removed as usize, - ) - .label_size(LabelSize::Small), - ), - ) - }) - .when( - can_open_excerpts - && relative_path.is_some() - && (is_selected || header_hovered), - |this| { - this.child( - Button::new("open-file-button", "Open File") - .style(ButtonStyle::OutlinedGhost) - .when(is_selected, |this| { - this.key_binding(KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - cx, + ))) + }) + .when(show_open_file_button, |this| { + this.child( + Button::new("open-file-button", "Open File") + .style(ButtonStyle::OutlinedCustom( + cx.theme().colors().border.opacity(0.6), )) - }) - .on_click(window.listener_for(editor, { - let jump_data = jump_data.clone(); - move |editor, e: &ClickEvent, window, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, + .layer(ui::ElevationIndex::ElevatedSurface) + .when(is_selected, |this| { + this.key_binding(KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, cx, - ); - } - })), - ) - }, + )) + }) + .on_click(window.listener_for(editor, { + let jump_data = jump_data.clone(); + move |editor, e: &ClickEvent, window, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + } + })), + ) + }), ) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_click(window.listener_for(editor, { @@ -1086,7 +1087,7 @@ pub(crate) fn render_buffer_header( }) } -fn file_status_label_color(file_status: Option) -> Color { +pub fn file_status_label_color(file_status: Option) -> Color { file_status.map_or(Color::Default, |status| { if status.is_conflicted() { Color::Conflict diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index e568b886f73..dd34391e195 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -6,9 +6,10 @@ use std::{ use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use collections::HashMap; +use fs::Fs; use gpui::{ - Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Pixels, Subscription, - WeakEntity, canvas, + Action, Entity, EventEmitter, Focusable, Font, Pixels, Subscription, WeakEntity, canvas, + prelude::*, }; use itertools::Itertools; use language::{Buffer, Capability, HighlightedText}; @@ -18,12 +19,10 @@ use multi_buffer::{ }; use project::Project; use rope::Point; -use settings::{DiffViewStyle, SeedQuerySetting, Settings, SettingsStore}; +use settings::{DiffViewStyle, SeedQuerySetting, Settings, SettingsStore, update_settings_file}; use text::{Bias, BufferId, OffsetRangeExt as _, Patch, ToPoint as _}; -use ui::{ - App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render, - Styled as _, Window, div, -}; + +use ui::{Toggleable as _, Tooltip, prelude::*, render_modifiers}; use crate::{ display_map::CompanionExcerptPatch, @@ -41,7 +40,7 @@ use crate::{ actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint}, display_map::Companion, }; -use zed_actions::assistant::InlineAssist; +use zed_actions::{OpenSettingsAt, assistant::InlineAssist}; pub(crate) fn patches_for_lhs_range( rhs_snapshot: &MultiBufferSnapshot, @@ -401,6 +400,124 @@ fn patch_for_excerpt( #[action(namespace = editor)] pub struct ToggleSplitDiff; +/// Unified/split diff view toggle buttons, shared by the toolbars of every +/// diff view (project diff, branch diff, solo diff) so they can't drift apart. +#[derive(gpui::IntoElement)] +pub struct DiffStyleControls { + splittable_editor: Entity, +} + +impl DiffStyleControls { + pub fn new(splittable_editor: Entity) -> Self { + Self { splittable_editor } + } + + fn set_diff_view_style( + splittable_editor: &Entity, + diff_view_style: DiffViewStyle, + window: &mut Window, + cx: &mut App, + ) { + update_settings_file(::global(cx), cx, move |settings, _| { + settings.editor.diff_view_style = Some(diff_view_style); + }); + + splittable_editor.update(cx, |editor, cx| { + if editor.diff_view_style() != diff_view_style { + editor.toggle_split(&ToggleSplitDiff, window, cx); + } + }); + } +} + +impl RenderOnce for DiffStyleControls { + fn render(self, _window: &mut Window, cx: &mut App) -> impl gpui::IntoElement { + let editor = self.splittable_editor.read(cx); + let diff_view_style = editor.diff_view_style(); + let is_split_set = diff_view_style == DiffViewStyle::Split; + let is_split_pending = is_split_set && !editor.is_split(); + let min_columns = EditorSettings::get_global(cx).minimum_split_diff_width as u32; + + let split_icon = if is_split_pending { + IconName::DiffSplitAuto + } else { + IconName::DiffSplit + }; + + h_flex() + .gap_1() + .child( + IconButton::new("diff-style-unified", IconName::DiffUnified) + .icon_size(IconSize::Small) + .toggle_state(diff_view_style == DiffViewStyle::Unified) + .tooltip(Tooltip::text("Unified")) + .on_click({ + let splittable_editor = self.splittable_editor.clone(); + move |_, window, cx| { + Self::set_diff_view_style( + &splittable_editor, + DiffViewStyle::Unified, + window, + cx, + ); + } + }), + ) + .child( + IconButton::new("diff-style-split", split_icon) + .icon_size(IconSize::Small) + .toggle_state(is_split_set) + .tooltip(Tooltip::element(move |_, cx| { + let message = if is_split_pending { + format!("Split when wider than {} columns", min_columns).into() + } else { + SharedString::from("Split") + }; + + v_flex() + .child(message) + .child( + h_flex() + .gap_0p5() + .text_ui_sm(cx) + .text_color(Color::Muted.color(cx)) + .children(render_modifiers( + &gpui::Modifiers::secondary_key(), + PlatformStyle::platform(), + None, + Some(TextSize::Small.rems(cx).into()), + false, + )) + .child("click to change min width"), + ) + .into_any_element() + })) + .on_click({ + let splittable_editor = self.splittable_editor.clone(); + move |_, window, cx| { + if window.modifiers().secondary() { + window.dispatch_action( + OpenSettingsAt { + path: "minimum_split_diff_width".to_string(), + target: None, + } + .boxed_clone(), + cx, + ); + } else { + Self::set_diff_view_style( + &splittable_editor, + DiffViewStyle::Split, + window, + cx, + ); + } + } + }), + ) + } +} + pub struct SplittableEditor { rhs_multibuffer: Entity, rhs_editor: Entity, diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 50090a559ca..06c554e7b6e 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -8,7 +8,8 @@ use git::{Amend, Commit, GenerateCommitMessage, Signoff}; use project::DisableAiSettings; use settings::Settings; use ui::{ - ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*, + ButtonLike, ContextMenu, ElevationIndex, KeybindingHint, PopoverMenu, PopoverMenuHandle, + SplitButton, Tooltip, prelude::*, }; use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize}; @@ -268,17 +269,14 @@ impl CommitModal { id: impl Into, keybinding_target: Option, ) -> impl IntoElement { + let menu_open = self.commit_menu_handle.is_deployed(); + PopoverMenu::new(id.into()) - .trigger( - ui::ButtonLike::new_rounded_right("commit-split-button-right") - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::None) - .child( - div() - .px_1() - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), - ), - ) + .with_handle(self.commit_menu_handle.clone()) + .trigger(crate::render_split_button_chevron_trigger( + "modal-commit-split-button-right", + menu_open, + )) .menu({ let git_panel_entity = self.git_panel.clone(); move |window, cx| { @@ -327,7 +325,10 @@ impl CommitModal { })) } }) - .with_handle(self.commit_menu_handle.clone()) + .offset(gpui::Point { + x: px(0.), + y: px(2.), + }) .anchor(Anchor::TopRight) } @@ -370,13 +371,12 @@ impl CommitModal { .unwrap_or_else(|| "".to_owned()); let branch_picker_button = Button::new("branch_picker_button", branch) + .label_size(LabelSize::Small) .start_icon( Icon::new(IconName::GitBranch) .size(IconSize::Small) .color(Color::Placeholder), ) - .style(ButtonStyle::Transparent) - .color(Color::Muted) .on_click(cx.listener(|_, _, window, cx| { window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); })); @@ -401,6 +401,7 @@ impl CommitModal { x: px(0.0), y: px(-2.0), }); + let focus_handle = self.focus_handle(cx); let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, cx).map(|close_kb| { @@ -409,13 +410,12 @@ impl CommitModal { h_flex() .group("commit_editor_footer") - .flex_none() - .w_full() - .items_center() - .justify_between() .w_full() .h(px(self.properties.footer_height)) + .w_full() .gap_1() + .flex_none() + .justify_between() .child( h_flex() .gap_1() @@ -430,64 +430,53 @@ impl CommitModal { .children(generate_commit_message) .children(co_authors), ) - .child(div().flex_1()) .child( h_flex() - .items_center() - .justify_end() - .flex_none() - .px_1() .gap_4() .child(close_kb_hint) .child(SplitButton::new( - ui::ButtonLike::new_rounded_left(ElementId::Name( - format!("split-button-left-{}", commit_label).into(), - )) - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) - .child( - div() - .child(Label::new(commit_label).size(LabelSize::Small)) - .mr_0p5(), - ) - .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { - telemetry::event!("Git Committed", source = "Git Modal"); - this.git_panel.update(cx, |git_panel, cx| { - git_panel.commit_changes( - CommitOptions { - amend: is_amend_pending, - signoff: is_signoff_enabled, - allow_empty: false, - }, - window, - cx, - ) - }); - cx.emit(DismissEvent); - })) - .disabled(!can_commit) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |_window, cx| { - if can_commit { - Tooltip::with_meta_in( - tooltip, - Some(&git::Commit), - format!( - "git commit{}{}", - if is_amend_pending { " --amend" } else { "" }, - if is_signoff_enabled { " --signoff" } else { "" } - ), - &focus_handle.clone(), + ButtonLike::new_rounded_left(format!("split-button-left-{}", commit_label)) + .layer(ElevationIndex::ModalSurface) + .size(ButtonSize::Compact) + .disabled(!can_commit) + .child(Label::new(commit_label).size(LabelSize::Small).mr_0p5()) + .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { + telemetry::event!("Git Committed", source = "Git Modal"); + this.git_panel.update(cx, |git_panel, cx| { + git_panel.commit_changes( + CommitOptions { + amend: is_amend_pending, + signoff: is_signoff_enabled, + allow_empty: false, + }, + window, cx, ) - } else { - Tooltip::simple(tooltip, cx) + }); + cx.emit(DismissEvent); + })) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |_window, cx| { + if can_commit { + Tooltip::with_meta_in( + tooltip, + Some(&git::Commit), + format!( + "git commit{}{}", + if is_amend_pending { " --amend" } else { "" }, + if is_signoff_enabled { " --signoff" } else { "" } + ), + &focus_handle.clone(), + cx, + ) + } else { + Tooltip::simple(tooltip, cx) + } } - } - }), + }), self.render_git_commit_menu( - ElementId::Name(format!("split-button-right-{}", commit_label).into()), + format!("split-button-right-{}", commit_label), Some(focus_handle), ) .into_any_element(), diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 134aad4bc69..e2b04699b22 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -79,8 +79,9 @@ use theme_settings::ThemeSettings; use time::OffsetDateTime; use ui::{ ButtonLike, Checkbox, ContextMenu, ContextMenuEntry, Divider, ElevationIndex, - IndentGuideColors, KeyBinding, PopoverMenu, ProjectEmptyState, RenderedIndentGuide, ScrollAxes, - Scrollbars, SplitButton, Tab, TintColor, Tooltip, WithScrollbar, prelude::*, + IndentGuideColors, KeyBinding, PopoverMenu, PopoverMenuHandle, ProjectEmptyState, + RenderedIndentGuide, ScrollAxes, Scrollbars, SplitButton, Tab, TintColor, Tooltip, + WithScrollbar, prelude::*, }; use util::paths::PathStyle; use util::{ResultExt, TryFutureExt, markdown::MarkdownInlineCode, maybe, rel_path::RelPath}; @@ -801,9 +802,11 @@ pub struct GitPanel { history_keyboard_nav: bool, _commit_message_buffer_subscription: Option, _repo_subscriptions: Vec, - _settings_subscription: Subscription, git_access: Option, + commit_menu_handle: PopoverMenuHandle, + changes_actions_menu_handle: PopoverMenuHandle, + remote_action_menu_handle: PopoverMenuHandle, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -1084,6 +1087,9 @@ impl GitPanel { _repo_subscriptions: Vec::new(), _settings_subscription, git_access: None, + commit_menu_handle: PopoverMenuHandle::default(), + changes_actions_menu_handle: PopoverMenuHandle::default(), + remote_action_menu_handle: PopoverMenuHandle::default(), }; this.schedule_update(window, cx); @@ -4867,21 +4873,14 @@ impl GitPanel { keybinding_target: Option, cx: &mut Context, ) -> impl IntoElement { + let menu_open = self.commit_menu_handle.is_deployed(); + PopoverMenu::new(id.into()) - .trigger( - ui::ButtonLike::new_rounded_right("commit-split-button-right") - .layer(ui::ElevationIndex::ModalSurface) - .size(ButtonSize::None) - .child( - h_flex() - .px_1() - .h_full() - .justify_center() - .border_l_1() - .border_color(cx.theme().colors().border) - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), - ), - ) + .trigger(crate::render_split_button_chevron_trigger( + "commit-split-button-right", + menu_open, + )) + .with_handle(self.commit_menu_handle.clone()) .menu({ let git_panel = cx.entity(); let has_previous_commit = self.head_commit(cx).is_some(); @@ -4923,6 +4922,10 @@ impl GitPanel { } }) .anchor(Anchor::TopRight) + .offset(gpui::Point { + x: px(0.), + y: px(2.), + }) } pub fn configure_commit_button(&self, cx: &mut Context) -> (bool, &'static str) { @@ -5008,19 +5011,16 @@ impl GitPanel { let has_unstaged_changes = self.has_unstaged_changes(); let has_new_changes = self.new_count > 0; let has_stash_items = self.stash_entries.entries.len() > 0; + let focus_handle = self.focus_handle.clone(); + let menu_open = self.changes_actions_menu_handle.is_deployed(); PopoverMenu::new(id.into()) - .trigger( - ui::ButtonLike::new_rounded_right("git-changes-actions-split-button-right") - .layer(ui::ElevationIndex::ModalSurface) - .size(ButtonSize::None) - .child( - div() - .px_1() - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), - ), - ) + .trigger(crate::render_split_button_chevron_trigger( + "changes-actions-split-button-right", + menu_open, + )) + .with_handle(self.changes_actions_menu_handle.clone()) .menu(move |window, cx| { Some(git_panel_context_menu( has_tracked_changes, @@ -5034,6 +5034,10 @@ impl GitPanel { )) }) .anchor(Anchor::TopRight) + .offset(gpui::Point { + x: px(0.), + y: px(2.), + }) } fn render_git_changes_actions_button(&self, cx: &mut Context) -> impl IntoElement { @@ -5158,6 +5162,7 @@ impl GitPanel { focus_handle, true, self.pending_remote_operation, + self.remote_action_menu_handle.clone(), )) }) .into_any_element(), @@ -5359,7 +5364,7 @@ impl GitPanel { Color::Default }; - div() + h_flex() .id("commit-wrapper") .on_hover(cx.listener(move |this, hovered, _, cx| { this.show_placeholders = @@ -5367,57 +5372,55 @@ impl GitPanel { cx.notify() })) .child(SplitButton::new( - ButtonLike::new_rounded_left(ElementId::Name( - format!("split-button-left-{}", title).into(), - )) - .layer(ElevationIndex::ModalSurface) - .size(ButtonSize::Compact) - .child( - Label::new(title) - .size(LabelSize::Small) - .color(label_color) - .mr_0p5(), - ) - .on_click({ - let git_panel = cx.weak_entity(); - move |_, window, cx| { - telemetry::event!("Git Committed", source = "Git Panel"); - git_panel - .update(cx, |git_panel, cx| { - git_panel.commit_changes( - CommitOptions { - amend, - signoff, - allow_empty: false, - }, - window, - cx, - ); - }) - .ok(); - } - }) - .disabled(!can_commit || self.modal_open) - .tooltip({ - let handle = commit_tooltip_focus_handle.clone(); - move |_window, cx| { - if can_commit { - Tooltip::with_meta_in( - tooltip, - Some(&git::Commit), - format!( - "git commit{}{}", - if amend { " --amend" } else { "" }, - if signoff { " --signoff" } else { "" } - ), - &handle.clone(), - cx, - ) - } else { - Tooltip::simple(tooltip, cx) + ButtonLike::new_rounded_left(format!("split-button-left-{}", title)) + .layer(ElevationIndex::ModalSurface) + .size(ButtonSize::Compact) + .disabled(!can_commit || self.modal_open) + .child( + Label::new(title) + .size(LabelSize::Small) + .color(label_color) + .mr_0p5(), + ) + .on_click({ + let git_panel = cx.weak_entity(); + move |_, window, cx| { + telemetry::event!("Git Committed", source = "Git Panel"); + git_panel + .update(cx, |git_panel, cx| { + git_panel.commit_changes( + CommitOptions { + amend, + signoff, + allow_empty: false, + }, + window, + cx, + ); + }) + .ok(); } - } - }), + }) + .tooltip({ + let handle = commit_tooltip_focus_handle.clone(); + move |_window, cx| { + if can_commit { + Tooltip::with_meta_in( + tooltip, + Some(&git::Commit), + format!( + "git commit{}{}", + if amend { " --amend" } else { "" }, + if signoff { " --signoff" } else { "" } + ), + &handle.clone(), + cx, + ) + } else { + Tooltip::simple(tooltip, cx) + } + } + }), self.render_git_commit_menu( ElementId::Name(format!("split-button-right-{}", title).into()), Some(commit_tooltip_focus_handle), @@ -5615,7 +5618,11 @@ impl GitPanel { GitPanelTab::Changes, ActivateChangesTab.boxed_clone(), )) - .child(Divider::vertical().color(ui::DividerColor::BorderFaded)) + .child( + Divider::vertical() + .color(ui::DividerColor::BorderFaded) + .h_full(), + ) .child(tab( ElementId::Name("history-tab".into()), active_tab != GitPanelTab::Changes, @@ -6515,7 +6522,7 @@ impl GitPanel { ) .separator() .action("Open Diff", menu::Confirm.boxed_clone()) - .action("Open Diff (File)", menu::SecondaryConfirm.boxed_clone()) + .action("Open File Diff", menu::SecondaryConfirm.boxed_clone()) .action("View File", ViewFile.boxed_clone()) .when(!is_created, |context_menu| { context_menu diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index a5dc98c5049..08192d70c0f 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -2,10 +2,6 @@ use anyhow::anyhow; use commit_modal::CommitModal; use editor::{Editor, actions::DiffClipboardWithSelectionData}; -use ui::{ - Color, Headline, HeadlineSize, Icon, IconName, IconSize, IntoElement, ParentElement, Render, - Styled, StyledExt, div, h_flex, rems, v_flex, -}; use workspace::{Toast, notifications::NotificationId}; mod blame_ui; @@ -23,7 +19,7 @@ use menu::{Cancel, Confirm}; use project::git_store::Repository; use project_diff::ProjectDiff; use time::OffsetDateTime; -use ui::prelude::*; +use ui::{ButtonLike, ContextMenu, ElevationIndex, PopoverMenuHandle, TintColor, prelude::*}; use workspace::{ ModalView, OpenMode, Workspace, notifications::{DetachAndPromptErr, NotifyTaskExt}, @@ -766,6 +762,7 @@ fn render_remote_button( keybinding_target: Option, show_fetch_button: bool, in_progress_operation: Option, + menu_handle: PopoverMenuHandle, ) -> Option { let id = id.into(); let upstream = branch.upstream.as_ref(); @@ -778,6 +775,7 @@ fn render_remote_button( keybinding_target, id, in_progress_operation, + menu_handle, )), (0, 0) => None, (ahead, 0) => Some(remote_button::render_push_button( @@ -785,6 +783,7 @@ fn render_remote_button( id, ahead, in_progress_operation, + menu_handle, )), (ahead, behind) => Some(remote_button::render_pull_button( keybinding_target, @@ -792,6 +791,7 @@ fn render_remote_button( ahead, behind, in_progress_operation, + menu_handle, )), }, Some(Upstream { @@ -801,11 +801,13 @@ fn render_remote_button( keybinding_target, id, in_progress_operation, + menu_handle, )), None => Some(remote_button::render_publish_button( keybinding_target, id, in_progress_operation, + menu_handle, )), } } @@ -813,12 +815,16 @@ fn render_remote_button( mod remote_button { use crate::git_panel::RemoteOperationKind; use gpui::{Action, Anchor, AnyView, ClickEvent, FocusHandle}; - use ui::{CommonAnimationExt, ContextMenu, PopoverMenu, SplitButton, Tooltip, prelude::*}; + use ui::{ + ButtonLike, CommonAnimationExt, ContextMenu, ElevationIndex, PopoverMenu, + PopoverMenuHandle, SplitButton, Tooltip, prelude::*, + }; pub fn render_fetch_button( keybinding_target: Option, id: SharedString, in_progress_operation: Option, + menu_handle: PopoverMenuHandle, ) -> SplitButton { split_button( id, @@ -828,6 +834,7 @@ mod remote_button { Some(IconName::ArrowCircle), keybinding_target.clone(), in_progress_operation, + menu_handle, move |_, window, cx| { window.dispatch_action(Box::new(git::Fetch), cx); }, @@ -848,6 +855,7 @@ mod remote_button { id: SharedString, ahead: u32, in_progress_operation: Option, + menu_handle: PopoverMenuHandle, ) -> SplitButton { split_button( id, @@ -857,6 +865,7 @@ mod remote_button { None, keybinding_target.clone(), in_progress_operation, + menu_handle, move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); }, @@ -878,6 +887,7 @@ mod remote_button { ahead: u32, behind: u32, in_progress_operation: Option, + menu_handle: PopoverMenuHandle, ) -> SplitButton { split_button( id, @@ -887,6 +897,7 @@ mod remote_button { None, keybinding_target.clone(), in_progress_operation, + menu_handle, move |_, window, cx| { window.dispatch_action(Box::new(git::Pull), cx); }, @@ -906,6 +917,7 @@ mod remote_button { keybinding_target: Option, id: SharedString, in_progress_operation: Option, + menu_handle: PopoverMenuHandle, ) -> SplitButton { split_button( id, @@ -915,6 +927,7 @@ mod remote_button { Some(IconName::ExpandUp), keybinding_target.clone(), in_progress_operation, + menu_handle, move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); }, @@ -934,6 +947,7 @@ mod remote_button { keybinding_target: Option, id: SharedString, in_progress_operation: Option, + menu_handle: PopoverMenuHandle, ) -> SplitButton { split_button( id, @@ -943,6 +957,7 @@ mod remote_button { Some(IconName::ExpandUp), keybinding_target.clone(), in_progress_operation, + menu_handle, move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); }, @@ -986,18 +1001,16 @@ mod remote_button { fn render_git_action_menu( id: impl Into, keybinding_target: Option, + menu_handle: PopoverMenuHandle, ) -> impl IntoElement { + let menu_open = menu_handle.is_deployed(); + PopoverMenu::new(id.into()) - .trigger( - ui::ButtonLike::new_rounded_right("split-button-right") - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::None) - .child( - div() - .px_1() - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), - ), - ) + .trigger(crate::render_split_button_chevron_trigger( + "split-button-right", + menu_open, + )) + .with_handle(menu_handle) .menu(move |window, cx| { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu @@ -1015,6 +1028,10 @@ mod remote_button { })) }) .anchor(Anchor::TopRight) + .offset(gpui::Point { + x: px(0.), + y: px(2.), + }) } #[allow(clippy::too_many_arguments)] @@ -1026,6 +1043,7 @@ mod remote_button { left_icon: Option, keybinding_target: Option, in_progress_operation: Option, + menu_handle: PopoverMenuHandle, left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, ) -> SplitButton { @@ -1033,7 +1051,6 @@ mod remote_button { h_flex() .ml_neg_px() .h(rems(0.875)) - .items_center() .overflow_hidden() .px_0p5() .child( @@ -1046,58 +1063,57 @@ mod remote_button { let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0); let is_in_progress = in_progress_operation.is_some(); - let left = ui::ButtonLike::new_rounded_left(ElementId::Name( - format!("split-button-left-{}", id).into(), - )) - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) - .disabled(is_in_progress) - .when(should_render_counts, |this| { - this.child( - h_flex() - .ml_neg_0p5() - .when(behind_count > 0, |this| { - this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall)) - .child(count(behind_count)) - }) - .when(ahead_count > 0, |this| { - this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall)) - .child(count(ahead_count)) - }), - ) - }) - .when_some(left_icon, |this, left_icon| { - this.map(|this| { - if is_in_progress { - this.child( - Icon::new(IconName::LoadCircle) - .size(IconSize::XSmall) - .color(Color::Disabled) - .with_rotate_animation(2), - ) - } else { - this.child(Icon::new(left_icon).size(IconSize::XSmall)) - } + let left = ButtonLike::new_rounded_left(format!("split-button-left-{}", id)) + .layer(ElevationIndex::ModalSurface) + .size(ButtonSize::Compact) + .disabled(is_in_progress) + .when(should_render_counts, |this| { + this.child( + h_flex() + .ml_neg_0p5() + .when(behind_count > 0, |this| { + this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall)) + .child(count(behind_count)) + }) + .when(ahead_count > 0, |this| { + this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall)) + .child(count(ahead_count)) + }), + ) }) - }) - .child( - Label::new(left_label) - .size(LabelSize::Small) - .when(is_in_progress, |this| this.color(Color::Disabled)) - .mr_0p5(), - ) - .on_click(left_on_click) - .tooltip(move |window, cx| { - if let Some(operation) = in_progress_operation { - Tooltip::simple(in_progress_tooltip(operation), cx) - } else { - tooltip(window, cx) - } - }); + .when_some(left_icon, |this, left_icon| { + this.map(|this| { + if is_in_progress { + this.child( + Icon::new(IconName::LoadCircle) + .size(IconSize::XSmall) + .color(Color::Disabled) + .with_rotate_animation(2), + ) + } else { + this.child(Icon::new(left_icon).size(IconSize::XSmall)) + } + }) + }) + .child( + Label::new(left_label) + .size(LabelSize::Small) + .when(is_in_progress, |this| this.color(Color::Disabled)) + .mr_0p5(), + ) + .on_click(left_on_click) + .tooltip(move |window, cx| { + if let Some(operation) = in_progress_operation { + Tooltip::simple(in_progress_tooltip(operation), cx) + } else { + tooltip(window, cx) + } + }); let right = render_git_action_menu( - ElementId::Name(format!("split-button-right-{}", id).into()), + format!("split-button-right-{}", id), keybinding_target, + menu_handle, ) .into_any_element(); @@ -1105,6 +1121,25 @@ mod remote_button { } } +pub(crate) fn render_split_button_chevron_trigger( + id: impl Into, + menu_open: bool, +) -> ButtonLike { + let chevron_button_size = rems_from_px(20.); + let chevron_icon = if menu_open { + IconName::ChevronUp + } else { + IconName::ChevronDown + }; + + ButtonLike::new_rounded_right(id) + .layer(ElevationIndex::ModalSurface) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .width(chevron_button_size) + .height(chevron_button_size.into()) + .child(Icon::new(chevron_icon).size(IconSize::XSmall)) +} + /// A visual representation of a file's Git status. #[derive(IntoElement, RegisterComponent)] pub struct GitStatusIcon { diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 89f57e274f8..91f351bec29 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -40,7 +40,6 @@ use std::sync::Arc; use theme::ActiveTheme; use ui::{ CommonAnimationExt as _, DiffStat, Divider, KeyBinding, PopoverMenu, Tooltip, prelude::*, - vertical_divider, }; use util::{ResultExt as _, rel_path::RelPath}; use workspace::{ @@ -1739,16 +1738,59 @@ impl Render for ProjectDiffToolbar { let button_states = project_diff.read(cx).button_states(cx); let review_count = project_diff.read(cx).total_review_comment_count(); - h_group_xl() + let (additions, deletions) = project_diff.read(cx).calculate_changed_lines(cx); + let is_multibuffer_empty = project_diff.read(cx).multibuffer.read(cx).is_empty(); + + h_flex() .my_neg_1() .py_1() - .items_center() + .gap_1p5() .flex_wrap() .justify_between() + .when(!is_multibuffer_empty, |this| { + this.child(DiffStat::new( + "project-diff-stat", + additions as usize, + deletions as usize, + )) + .child(Divider::vertical().ml_1()) + }) + // n.b. the only reason these arrows are here is because we don't + // support "undo" for staging so we need a way to go back. .child( h_group_sm() - .when(button_states.selection, |el| { - el.child( + .child( + IconButton::new("up", IconName::ArrowUp) + .icon_size(IconSize::Small) + .disabled(!button_states.prev_next) + .tooltip(Tooltip::for_action_title_in( + "Go to Previous Hunk", + &GoToPreviousHunk, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&GoToPreviousHunk, window, cx) + })), + ) + .child( + IconButton::new("down", IconName::ArrowDown) + .icon_size(IconSize::Small) + .disabled(!button_states.prev_next) + .tooltip(Tooltip::for_action_title_in( + "Go to Next Hunk", + &GoToHunk, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&GoToHunk, window, cx) + })), + ), + ) + .child(Divider::vertical()) + .child( + h_group_sm() + .when(button_states.selection, |this| { + this.child( Button::new("stage", "Toggle Staged") .tooltip(Tooltip::for_action_title_in( "Toggle Staged", @@ -1761,127 +1803,83 @@ impl Render for ProjectDiffToolbar { })), ) }) - .when(!button_states.selection, |el| { - el.child( + .when(!button_states.selection, |this| { + this.child( Button::new("stage", "Stage") + .disabled(!button_states.stage) .tooltip(Tooltip::for_action_title_in( - "Stage and go to next hunk", + "Stage and Go to Next Hunk", &StageAndNext, &focus_handle, )) - .disabled( - !button_states.prev_next - && !button_states.stage_all - && !button_states.unstage_all, - ) .on_click(cx.listener(|this, _, window, cx| { this.dispatch_action(&StageAndNext, window, cx) })), ) .child( Button::new("unstage", "Unstage") + .disabled(!button_states.unstage) .tooltip(Tooltip::for_action_title_in( - "Unstage and go to next hunk", + "Unstage and Go to Next Hunk", &UnstageAndNext, &focus_handle, )) - .disabled( - !button_states.prev_next - && !button_states.stage_all - && !button_states.unstage_all, - ) .on_click(cx.listener(|this, _, window, cx| { this.dispatch_action(&UnstageAndNext, window, cx) })), ) }), ) - // n.b. the only reason these arrows are here is because we don't - // support "undo" for staging so we need a way to go back. - .child( - h_group_sm() - .child( - IconButton::new("up", IconName::ArrowUp) - .shape(ui::IconButtonShape::Square) + .child(Divider::vertical()) + .when( + button_states.unstage_all && !button_states.stage_all, + |this| { + this.child( + Button::new("unstage-all", "Unstage All") + .width(rems_from_px(80.)) .tooltip(Tooltip::for_action_title_in( - "Go to previous hunk", - &GoToPreviousHunk, + "Unstage All Changes", + &UnstageAll, &focus_handle, )) - .disabled(!button_states.prev_next) - .on_click(cx.listener(|this, _, window, cx| { - this.dispatch_action(&GoToPreviousHunk, window, cx) - })), + .on_click( + cx.listener(|this, _, window, cx| this.unstage_all(window, cx)), + ), ) - .child( - IconButton::new("down", IconName::ArrowDown) - .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::for_action_title_in( - "Go to next hunk", - &GoToHunk, - &focus_handle, - )) - .disabled(!button_states.prev_next) - .on_click(cx.listener(|this, _, window, cx| { - this.dispatch_action(&GoToHunk, window, cx) - })), - ), + }, ) - .child(vertical_divider()) - .child( - h_group_sm() - .when( - button_states.unstage_all && !button_states.stage_all, - |el| { - el.child( - Button::new("unstage-all", "Unstage All") - .tooltip(Tooltip::for_action_title_in( - "Unstage all changes", - &UnstageAll, - &focus_handle, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.unstage_all(window, cx) - })), - ) - }, - ) - .when( - !button_states.unstage_all || button_states.stage_all, - |el| { - el.child( - // todo make it so that changing to say "Unstaged" - // doesn't change the position. - div().child( - Button::new("stage-all", "Stage All") - .disabled(!button_states.stage_all) - .tooltip(Tooltip::for_action_title_in( - "Stage all changes", - &StageAll, - &focus_handle, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.stage_all(window, cx) - })), - ), - ) - }, - ) - .child( - Button::new("commit", "Commit") + .when( + !button_states.unstage_all || button_states.stage_all, + |this| { + this.child( + Button::new("stage-all", "Stage All") + .width(rems_from_px(80.)) + .disabled(!button_states.stage_all) .tooltip(Tooltip::for_action_title_in( - "Commit", - &Commit, + "Stage All Changes", + &StageAll, &focus_handle, )) - .on_click(cx.listener(|this, _, window, cx| { - this.dispatch_action(&Commit, window, cx); - })), - ), + .on_click( + cx.listener(|this, _, window, cx| this.stage_all(window, cx)), + ), + ) + }, + ) + .child(Divider::vertical()) + .child( + Button::new("commit", "Commit") + .tooltip(Tooltip::for_action_title_in( + "Commit", + &Commit, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&Commit, window, cx); + })), ) - // "Send Review to Agent" button (only shown when there are review comments) .when(review_count > 0, |el| { - el.child(vertical_divider()).child( + el.child(Divider::vertical()).child( render_send_review_to_agent_button(review_count, &focus_handle).on_click( cx.listener(|this, _, window, cx| { this.dispatch_action(&SendReviewToAgent, window, cx) @@ -1985,13 +1983,20 @@ impl Render for BranchDiffToolbar { let show_review_button = !is_multibuffer_empty && is_ai_enabled; - h_group_xl() + h_flex() .my_neg_1() .py_1() - .items_center() + .gap_1p5() .flex_wrap() - .justify_end() - .gap_2() + .justify_between() + .when(!is_multibuffer_empty, |this| { + this.child(DiffStat::new( + "branch-diff-stat", + additions as usize, + deletions as usize, + )) + }) + .child(Divider::vertical().ml_1()) .child( PopoverMenu::new("branch-diff-base-branch-picker") .menu(move |window, cx| { @@ -2013,6 +2018,7 @@ impl Render for BranchDiffToolbar { .ok(); }, ); + Some(branch_picker::select_popover( workspace.clone(), repository.clone(), @@ -2023,23 +2029,14 @@ impl Render for BranchDiffToolbar { )) }) .trigger_with_tooltip( - Button::new("branch-diff-base-branch", base_ref_label) - .color(Color::Muted) - .end_icon( - Icon::new(IconName::ChevronDown) - .size(IconSize::XSmall) - .color(Color::Muted), - ), - Tooltip::text("Select base branch"), + Button::new("branch-diff-base-branch", base_ref_label).end_icon( + Icon::new(IconName::ChevronDown) + .size(IconSize::XSmall) + .color(Color::Muted), + ), + Tooltip::text("Select Base Branch"), ), ) - .when(!is_multibuffer_empty, |this| { - this.child(DiffStat::new( - "branch-diff-stat", - additions as usize, - deletions as usize, - )) - }) .when(show_review_button, |this| { let focus_handle = focus_handle.clone(); this.child(Divider::vertical()).child( @@ -2049,7 +2046,6 @@ impl Render for BranchDiffToolbar { .size(IconSize::Small) .color(Color::Muted), ) - .key_binding(KeyBinding::for_action_in(&ReviewDiff, &focus_handle, cx)) .tooltip(move |_, cx| { Tooltip::with_meta_in( "Review Diff", @@ -2065,7 +2061,7 @@ impl Render for BranchDiffToolbar { ) }) .when(review_count > 0, |this| { - this.child(vertical_divider()).child( + this.child(Divider::vertical()).child( render_send_review_to_agent_button(review_count, &focus_handle).on_click( cx.listener(|this, _, window, cx| { this.dispatch_action(&SendReviewToAgent, window, cx) diff --git a/crates/git_ui/src/solo_diff_view.rs b/crates/git_ui/src/solo_diff_view.rs index 90bde497fa1..9ada614cfcf 100644 --- a/crates/git_ui/src/solo_diff_view.rs +++ b/crates/git_ui/src/solo_diff_view.rs @@ -1,18 +1,19 @@ -use crate::{git_panel::GitStatusEntry, git_status_icon}; +use crate::{git_panel::GitStatusEntry, git_panel_settings::GitPanelSettings, git_status_icon}; use anyhow::{Context as _, Result}; use buffer_diff::DiffHunkSecondaryStatus; use editor::{ - Direction, Editor, EditorEvent, EditorSettings, SplittableEditor, ToggleSplitDiff, + DiffStyleControls, Direction, Editor, EditorEvent, EditorSettings, SplittableEditor, + ToggleSplitDiff, actions::{GoToHunk, GoToPreviousHunk}, + file_status_label_color, }; -use fs::Fs; use git::{ Commit, Restore, StageAndNext, StageFile, ToggleStaged, UnstageAndNext, UnstageFile, repository::RepoPath, status::StageStatus, }; use gpui::{ - Action, AnyElement, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, - Focusable, IntoElement, Render, Subscription, Task, WeakEntity, Window, + Action, AnyElement, App, AppContext as _, Context, Empty, Entity, EventEmitter, FocusHandle, + Focusable, HighlightStyle, IntoElement, Render, Subscription, Task, WeakEntity, Window, }; use language::{Anchor, Buffer, HighlightedText, OffsetRangeExt as _, Point}; use multi_buffer::{MultiBuffer, PathKey, excerpt_context_lines}; @@ -20,16 +21,13 @@ use project::{ Project, git_store::{Repository, RepositoryId}, }; -use settings::{DiffViewStyle, Settings, SettingsStore, update_settings_file}; +use settings::{Settings, SettingsStore, StatusStyle}; use std::{ any::{Any, TypeId}, ops::Range, sync::Arc, }; -use ui::{ - Color, DiffStat, Divider, Icon, IconButton, IconButtonShape, IconName, Label, LabelCommon as _, - SharedString, Tooltip, prelude::*, vertical_divider, -}; +use ui::{DiffStat, Divider, Tooltip, prelude::*}; use util::paths::{PathExt as _, PathStyle}; use workspace::{ Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, @@ -479,16 +477,31 @@ impl Item for SoloDiffView { } fn breadcrumbs(&self, cx: &App) -> Option<(Vec, Option)> { + let text: SharedString = self + .repo_path + .as_ref() + .display(PathStyle::local()) + .into_owned() + .into(); + + // When the git panel is set to convey status via label color rather + // than an icon, tint the whole path like multibuffer headers do. + let mut highlights = Vec::new(); + if GitPanelSettings::get_global(cx).status_style == StatusStyle::LabelColor + && let Some(status) = self + .repository + .read(cx) + .status_for_path(&self.repo_path) + .map(|entry| entry.status) + { + highlights.push(( + 0..text.len(), + HighlightStyle::color(file_status_label_color(Some(status)).color(cx)), + )); + } + Some(( - vec![HighlightedText { - text: self - .repo_path - .as_ref() - .display(PathStyle::local()) - .into_owned() - .into(), - highlights: Vec::new(), - }], + vec![HighlightedText { text, highlights }], Some( theme_settings::ThemeSettings::get_global(cx) .buffer_font @@ -548,43 +561,6 @@ impl SoloDiffStyleToolbar { self.solo_diff.as_ref()?.upgrade() } - fn set_diff_view_style( - &mut self, - diff_view_style: DiffViewStyle, - window: &mut Window, - cx: &mut Context, - ) { - let Some(solo_diff) = self.solo_diff() else { - return; - }; - let workspace = solo_diff.read(cx).workspace.clone(); - - update_settings_file(::global(cx), cx, move |settings, _| { - settings.editor.diff_view_style = Some(diff_view_style); - }); - - if let Some(workspace) = workspace.upgrade() { - let splittable_editors = { - workspace - .read(cx) - .items(cx) - .filter_map(|item| item.act_as_type(TypeId::of::(), cx)) - .filter_map(|item| item.downcast::().ok()) - .collect::>() - }; - - for editor in splittable_editors { - editor.update(cx, |editor, cx| { - if editor.diff_view_style() != diff_view_style { - editor.toggle_split(&ToggleSplitDiff, window, cx); - } - }); - } - } - - cx.notify(); - } - fn toggle_showing_full_file(&mut self, cx: &mut Context) { if let Some(solo_diff) = self.solo_diff() { solo_diff.update(cx, |solo_diff, cx| { @@ -617,64 +593,49 @@ impl ToolbarItemView for SoloDiffStyleToolbar { impl Render for SoloDiffStyleToolbar { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let Some(solo_diff) = self.solo_diff() else { - return div(); + return Empty.into_any_element(); }; - let (editor_entity, showing_full_file) = { + + let (editor_entity, showing_full_file, status) = { let solo_diff = solo_diff.read(cx); - (solo_diff.editor.clone(), solo_diff.showing_full_file) + ( + solo_diff.editor.clone(), + solo_diff.showing_full_file, + solo_diff + .repository + .read(cx) + .status_for_path(&solo_diff.repo_path) + .map(|entry| entry.status), + ) }; - let editor = editor_entity.read(cx); - let diff_view_style = editor.diff_view_style(); - let is_split_set = diff_view_style == DiffViewStyle::Split; - let split_icon = if is_split_set && !editor.is_split() { - IconName::DiffSplitAuto + + let show_status_icon = + GitPanelSettings::get_global(cx).status_style != StatusStyle::LabelColor; + + let (expand_icon, expand_tooltip) = if showing_full_file { + (IconName::ChevronDownUp, "Show Changes Only") } else { - IconName::DiffSplit + (IconName::ChevronUpDown, "Show Full File") }; h_flex() - .h_8() - .items_center() + .pl_0p5() .gap_1() .child( - IconButton::new( - "solo-diff-toggle-excerpts", - if showing_full_file { - IconName::ChevronDownUp - } else { - IconName::ChevronUpDown - }, - ) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text(if showing_full_file { - "Show Changes Only" - } else { - "Show Full File" - })) - .on_click(cx.listener(|this, _, _, cx| { - this.toggle_showing_full_file(cx); - })), - ) - .child( - IconButton::new("solo-diff-unified", IconName::DiffUnified) + IconButton::new("solo-diff-toggle-excerpts", expand_icon) .icon_size(IconSize::Small) - .toggle_state(diff_view_style == DiffViewStyle::Unified) - .tooltip(Tooltip::text("Unified")) - .on_click(cx.listener(|this, _, window, cx| { - this.set_diff_view_style(DiffViewStyle::Unified, window, cx); + .tooltip(Tooltip::text(expand_tooltip)) + .on_click(cx.listener(|this, _, _, cx| { + this.toggle_showing_full_file(cx); })), ) - .child( - IconButton::new("solo-diff-split", split_icon) - .icon_size(IconSize::Small) - .toggle_state(diff_view_style == DiffViewStyle::Split) - .tooltip(Tooltip::text("Split")) - .on_click(cx.listener(|this, _, window, cx| { - this.set_diff_view_style(DiffViewStyle::Split, window, cx); - })), + .child(DiffStyleControls::new(editor_entity)) + .child(Divider::vertical().mr_1()) + .when_some( + show_status_icon.then_some(status).flatten(), + |this, status| this.child(git_status_icon(status)), ) - .child(vertical_divider()) - .child(div().w_1()) + .into_any_element() } } @@ -745,8 +706,9 @@ struct SoloDiffButtonStates { impl Render for SoloDiffGitToolbar { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let Some(solo_diff) = self.solo_diff() else { - return div(); + return gpui::Empty.into_any_element(); }; + let focus_handle = solo_diff.focus_handle(cx); let solo_diff = solo_diff.read(cx); let button_states = solo_diff.button_states(cx); @@ -754,32 +716,59 @@ impl Render for SoloDiffGitToolbar { .repository .read(cx) .status_for_path(&solo_diff.repo_path); - let status = status_entry.as_ref().map(|entry| entry.status); let diff_stat = status_entry.and_then(|entry| entry.diff_stat); - h_group_xl() + h_flex() .my_neg_1() .py_1() - .items_center() + .gap_1p5() .flex_wrap() .justify_between() - .children(status.map(|status| git_status_icon(status).into_any_element())) .children(diff_stat.map(|stat| { DiffStat::new("solo-diff-stat", stat.added as usize, stat.deleted as usize) - .into_any_element() })) + .child(Divider::vertical().ml_1()) + .child( + h_group_sm() + .child( + IconButton::new("up", IconName::ArrowUp) + .icon_size(IconSize::Small) + .disabled(!button_states.prev_next) + .tooltip(Tooltip::for_action_title_in( + "Go to Previous Hunk", + &GoToPreviousHunk, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&GoToPreviousHunk, window, cx) + })), + ) + .child( + IconButton::new("down", IconName::ArrowDown) + .icon_size(IconSize::Small) + .disabled(!button_states.prev_next) + .tooltip(Tooltip::for_action_title_in( + "Go to Next Hunk", + &GoToHunk, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&GoToHunk, window, cx) + })), + ), + ) .child(Divider::vertical()) .child( h_group_sm() .when(button_states.selection, |el| { el.child( Button::new("stage", "Toggle Staged") + .disabled(!button_states.stage && !button_states.unstage) .tooltip(Tooltip::for_action_title_in( "Toggle Staged", &ToggleStaged, &focus_handle, )) - .disabled(!button_states.stage && !button_states.unstage) .on_click(cx.listener(|this, _, window, cx| { this.dispatch_action(&ToggleStaged, window, cx) })), @@ -788,24 +777,24 @@ impl Render for SoloDiffGitToolbar { .when(!button_states.selection, |el| { el.child( Button::new("stage", "Stage") + .disabled(!button_states.stage) .tooltip(Tooltip::for_action_title_in( - "Stage and go to next hunk", + "Stage and Go to Next Hunk", &StageAndNext, &focus_handle, )) - .disabled(!button_states.stage) .on_click(cx.listener(|this, _, window, cx| { this.dispatch_action(&StageAndNext, window, cx) })), ) .child( Button::new("unstage", "Unstage") + .disabled(!button_states.unstage) .tooltip(Tooltip::for_action_title_in( - "Unstage and go to next hunk", + "Unstage and Go to Next Hunk", &UnstageAndNext, &focus_handle, )) - .disabled(!button_states.unstage) .on_click(cx.listener(|this, _, window, cx| { this.dispatch_action(&UnstageAndNext, window, cx) })), @@ -824,72 +813,40 @@ impl Render for SoloDiffGitToolbar { })), ), ) + .child(Divider::vertical()) + .child(h_group_sm().child(if button_states.stage_file { + Button::new("stage-file", "Stage All") + .width(rems_from_px(80.)) + .disabled(!button_states.stage_file) + .tooltip(Tooltip::for_action_title_in( + "Stage All", + &StageFile, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| this.stage_file(window, cx))) + } else { + Button::new("unstage-file", "Unstage All") + .width(rems_from_px(80.)) + .disabled(!button_states.unstage_file) + .tooltip(Tooltip::for_action_title_in( + "Unstage All", + &UnstageFile, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| this.unstage_file(window, cx))) + })) + .child(Divider::vertical()) .child( - h_group_sm() - .child( - IconButton::new("up", IconName::ArrowUp) - .shape(IconButtonShape::Square) - .tooltip(Tooltip::for_action_title_in( - "Go to previous hunk", - &GoToPreviousHunk, - &focus_handle, - )) - .disabled(!button_states.prev_next) - .on_click(cx.listener(|this, _, window, cx| { - this.dispatch_action(&GoToPreviousHunk, window, cx) - })), - ) - .child( - IconButton::new("down", IconName::ArrowDown) - .shape(IconButtonShape::Square) - .tooltip(Tooltip::for_action_title_in( - "Go to next hunk", - &GoToHunk, - &focus_handle, - )) - .disabled(!button_states.prev_next) - .on_click(cx.listener(|this, _, window, cx| { - this.dispatch_action(&GoToHunk, window, cx) - })), - ), - ) - .child(vertical_divider()) - .child( - h_group_sm() - .child(if button_states.stage_file { - Button::new("stage-file", "Stage File") - .tooltip(Tooltip::for_action_title_in( - "Stage file", - &StageFile, - &focus_handle, - )) - .disabled(!button_states.stage_file) - .on_click( - cx.listener(|this, _, window, cx| this.stage_file(window, cx)), - ) - } else { - Button::new("unstage-file", "Unstage File") - .tooltip(Tooltip::for_action_title_in( - "Unstage file", - &UnstageFile, - &focus_handle, - )) - .disabled(!button_states.unstage_file) - .on_click( - cx.listener(|this, _, window, cx| this.unstage_file(window, cx)), - ) - }) - .child( - Button::new("commit", "Commit") - .tooltip(Tooltip::for_action_title_in( - "Commit", - &Commit, - &focus_handle, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.dispatch_action(&Commit, window, cx); - })), - ), + Button::new("commit", "Commit") + .tooltip(Tooltip::for_action_title_in( + "Commit", + &Commit, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&Commit, window, cx); + })), ) + .into_any_element() } } diff --git a/crates/picker/src/footer.rs b/crates/picker/src/footer.rs index fc90c5f2530..ff0b11caa94 100644 --- a/crates/picker/src/footer.rs +++ b/crates/picker/src/footer.rs @@ -145,7 +145,7 @@ impl Picker { ), ) .when(preview_visible, |this| { - this.child(Divider::vertical().h_4().mx_1()) + this.child(Divider::vertical().mx_1()) .child( IconButton::new("picker-preview-right", IconName::DiffSplit) .icon_size(IconSize::Small) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 5e20f2e7ecf..d0c0a5385ff 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -14,15 +14,15 @@ use crate::{ use any_vec::AnyVec; use collections::HashMap; use editor::{ - Editor, EditorSettings, MultiBufferOffset, SplittableEditor, ToggleSplitDiff, + DiffStyleControls, Editor, EditorSettings, MultiBufferOffset, SplittableEditor, actions::{Backtab, FoldAll, Tab, ToggleFoldAll, UnfoldAll}, scroll::Autoscroll, }; use futures::channel::oneshot; use gpui::{ - Action as _, App, ClickEvent, Context, Entity, EventEmitter, Focusable, - InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, - Styled, Subscription, Task, TaskExt, WeakEntity, Window, div, + App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _, + IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, + TaskExt, WeakEntity, Window, div, }; use language::{Language, LanguageRegistry}; use project::{ @@ -30,17 +30,11 @@ use project::{ search_history::{SearchHistory, SearchHistoryCursor}, }; -use fs::Fs; -use settings::{DiffViewStyle, SeedQuerySetting, Settings, update_settings_file}; +use settings::{SeedQuerySetting, Settings}; use std::{any::TypeId, sync::Arc}; -use zed_actions::{ - OpenSettingsAt, outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath, -}; +use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath}; -use ui::{ - BASE_REM_SIZE_IN_PX, IconButtonShape, PlatformStyle, TextSize, Tooltip, prelude::*, - render_modifiers, utils::SearchInputWidth, -}; +use ui::{BASE_REM_SIZE_IN_PX, IconButtonShape, Tooltip, prelude::*, utils::SearchInputWidth}; use util::{ResultExt, paths::PathMatcher}; use workspace::{ ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -109,127 +103,11 @@ impl Render for BufferSearchBar { let focus_handle = self.focus_handle(cx); let has_splittable_editor = self.splittable_editor.is_some(); - let split_buttons = if has_splittable_editor { - self.splittable_editor - .as_ref() - .and_then(|weak| weak.upgrade()) - .map(|splittable_editor| { - let editor_ref = splittable_editor.read(cx); - let diff_view_style = editor_ref.diff_view_style(); - - let is_split_set = diff_view_style == DiffViewStyle::Split; - let is_split_active = editor_ref.is_split(); - let min_columns = - EditorSettings::get_global(cx).minimum_split_diff_width as u32; - - let split_icon = if is_split_set && !is_split_active { - IconName::DiffSplitAuto - } else { - IconName::DiffSplit - }; - - h_flex() - .gap_1() - .child( - IconButton::new("diff-unified", IconName::DiffUnified) - .icon_size(IconSize::Small) - .toggle_state(diff_view_style == DiffViewStyle::Unified) - .tooltip(Tooltip::text("Unified")) - .on_click({ - let splittable_editor = splittable_editor.downgrade(); - move |_, window, cx| { - update_settings_file( - ::global(cx), - cx, - |settings, _| { - settings.editor.diff_view_style = - Some(DiffViewStyle::Unified); - }, - ); - if diff_view_style == DiffViewStyle::Split { - splittable_editor - .update(cx, |editor, cx| { - editor.toggle_split( - &ToggleSplitDiff, - window, - cx, - ); - }) - .ok(); - } - } - }), - ) - .child( - IconButton::new("diff-split", split_icon) - .toggle_state(diff_view_style == DiffViewStyle::Split) - .icon_size(IconSize::Small) - .tooltip(Tooltip::element(move |_, cx| { - let message = if is_split_set && !is_split_active { - format!("Split when wider than {} columns", min_columns) - .into() - } else { - SharedString::from("Split") - }; - - v_flex() - .child(message) - .child( - h_flex() - .gap_0p5() - .text_ui_sm(cx) - .text_color(Color::Muted.color(cx)) - .children(render_modifiers( - &gpui::Modifiers::secondary_key(), - PlatformStyle::platform(), - None, - Some(TextSize::Small.rems(cx).into()), - false, - )) - .child("click to change min width"), - ) - .into_any() - })) - .on_click({ - let splittable_editor = splittable_editor.downgrade(); - move |_, window, cx| { - if window.modifiers().secondary() { - window.dispatch_action( - OpenSettingsAt { - path: "minimum_split_diff_width".to_string(), - target: None, - } - .boxed_clone(), - cx, - ); - } else { - update_settings_file( - ::global(cx), - cx, - |settings, _| { - settings.editor.diff_view_style = - Some(DiffViewStyle::Split); - }, - ); - if diff_view_style == DiffViewStyle::Unified { - splittable_editor - .update(cx, |editor, cx| { - editor.toggle_split( - &ToggleSplitDiff, - window, - cx, - ); - }) - .ok(); - } - } - } - }), - ) - }) - } else { - None - }; + let split_buttons = self + .splittable_editor + .as_ref() + .and_then(|weak| weak.upgrade()) + .map(DiffStyleControls::new); let collapse_expand_button = if self.needs_expand_collapse_option(cx) { let query_editor_focus = self.query_editor.focus_handle(cx); diff --git a/crates/ui/src/components/button/split_button.rs b/crates/ui/src/components/button/split_button.rs index 01cfc9a38b5..315bbc95f12 100644 --- a/crates/ui/src/components/button/split_button.rs +++ b/crates/ui/src/components/button/split_button.rs @@ -1,10 +1,11 @@ use gpui::{ - AnyElement, App, BoxShadow, IntoElement, ParentElement, RenderOnce, Styled, Window, div, hsla, - prelude::FluentBuilder, px, relative, + AnyElement, App, BoxShadow, ParentElement, RenderOnce, Styled, Window, div, hsla, prelude::*, + px, }; + use theme::ActiveTheme; -use crate::{ElevationIndex, prelude::*}; +use crate::{Divider, ElevationIndex, prelude::*}; use super::ButtonLike; @@ -80,12 +81,13 @@ impl RenderOnce for SplitButton { SplitButtonKind::ButtonLike(button) => button.into_any_element(), SplitButtonKind::IconButton(icon) => icon.into_any_element(), })) - .child( - div() - .h(relative(0.8)) - .w_px() - .bg(cx.theme().colors().border.opacity(0.5)), - ) + .child(Divider::vertical().map(|s| { + if self.style == SplitButtonStyle::Filled { + s.h_full().color(crate::DividerColor::Border) + } else { + s.h_4() + } + })) .child(self.right) .when(self.style == SplitButtonStyle::Filled, |this| { this.bg(ElevationIndex::Surface.on_elevation_bg(cx)) diff --git a/crates/ui/src/components/divider.rs b/crates/ui/src/components/divider.rs index 74c697a76ac..7e343867f14 100644 --- a/crates/ui/src/components/divider.rs +++ b/crates/ui/src/components/divider.rs @@ -2,26 +2,6 @@ use gpui::{Hsla, IntoElement, PathBuilder, Refineable as _, StyleRefinement, can use crate::prelude::*; -pub fn divider() -> Divider { - Divider { - line_style: DividerStyle::Solid, - direction: DividerDirection::Horizontal, - color: DividerColor::default(), - style: StyleRefinement::default(), - inset: false, - } -} - -pub fn vertical_divider() -> Divider { - Divider { - line_style: DividerStyle::Solid, - direction: DividerDirection::Vertical, - color: DividerColor::default(), - style: StyleRefinement::default(), - inset: false, - } -} - #[derive(Clone, Copy, PartialEq)] enum DividerStyle { Solid, @@ -166,7 +146,7 @@ impl RenderOnce for Divider { DividerDirection::Vertical => div() .min_w_0() .w_px() - .h_full() + .h_4() .when(self.inset, |this| this.my_1p5()), };