diff --git a/Cargo.lock b/Cargo.lock index add19f89af3869fe4fbd203087c9ef45c644df28..9c96d4000c29f668fc8113c3329cd51b56420cbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2334,12 +2334,9 @@ version = "0.1.0" dependencies = [ "editor", "gpui", - "itertools 0.14.0", - "settings", "theme", "ui", "workspace", - "zed_actions", ] [[package]] @@ -4887,6 +4884,7 @@ dependencies = [ "pretty_assertions", "project", "rand 0.9.2", + "search", "serde", "serde_json", "settings", @@ -4896,6 +4894,7 @@ dependencies = [ "unindent", "util", "workspace", + "zed_actions", "zlog", ] diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 83b2939746a3c2c8e09c8f86f4b40817b085884f..fe4f8233a085c42641edeaaada891cc6e6196c12 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -360,6 +360,7 @@ "tab": "buffer_search::FocusEditor", "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPreviousMatch", + "ctrl-shift-enter": "editor::ToggleFoldAll", "alt-enter": "search::SelectAllMatches", "find": "search::FocusSearch", "ctrl-f": "search::FocusSearch", @@ -386,7 +387,7 @@ "bindings": { "escape": "project_search::ToggleFocus", "shift-find": "search::FocusSearch", - "shift-enter": "project_search::ToggleAllSearchResults", + "ctrl-shift-enter": "project_search::ToggleAllSearchResults", "ctrl-shift-f": "search::FocusSearch", "ctrl-shift-h": "search::ToggleReplace", "alt-ctrl-g": "search::ToggleRegex", @@ -459,7 +460,7 @@ "alt-w": "search::ToggleWholeWord", "alt-find": "project_search::ToggleFilters", "alt-ctrl-f": "project_search::ToggleFilters", - "shift-enter": "project_search::ToggleAllSearchResults", + "ctrl-shift-enter": "project_search::ToggleAllSearchResults", "ctrl-alt-shift-r": "search::ToggleRegex", "ctrl-alt-shift-x": "search::ToggleRegex", "alt-r": "search::ToggleRegex", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 867dbc3b6f71119f57d3b27ae9d840026213c52e..a88db27045fa7a6c0922d336b6c6b19dd53391b2 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -415,6 +415,7 @@ "tab": "buffer_search::FocusEditor", "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPreviousMatch", + "cmd-shift-enter": "editor::ToggleFoldAll", "alt-enter": "search::SelectAllMatches", "cmd-f": "search::FocusSearch", "cmd-alt-f": "search::ToggleReplace", @@ -444,7 +445,7 @@ "bindings": { "escape": "project_search::ToggleFocus", "cmd-shift-j": "project_search::ToggleFilters", - "shift-enter": "project_search::ToggleAllSearchResults", + "cmd-shift-enter": "project_search::ToggleAllSearchResults", "cmd-shift-f": "search::FocusSearch", "cmd-shift-h": "search::ToggleReplace", "alt-cmd-g": "search::ToggleRegex", @@ -473,7 +474,7 @@ "bindings": { "escape": "project_search::ToggleFocus", "cmd-shift-j": "project_search::ToggleFilters", - "shift-enter": "project_search::ToggleAllSearchResults", + "cmd-shift-enter": "project_search::ToggleAllSearchResults", "cmd-shift-h": "search::ToggleReplace", "alt-cmd-g": "search::ToggleRegex", "alt-cmd-x": "search::ToggleRegex", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 6fa1f73343da6238f065865ff33816834158abc7..125840a906fd57591e2f3e82d7d8167bb28e7a25 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -365,6 +365,7 @@ "tab": "buffer_search::FocusEditor", "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPreviousMatch", + "ctrl-shift-enter": "editor::ToggleFoldAll", "alt-enter": "search::SelectAllMatches", "ctrl-f": "search::FocusSearch", "ctrl-h": "search::ToggleReplace", @@ -464,7 +465,7 @@ "alt-c": "search::ToggleCaseSensitive", "alt-w": "search::ToggleWholeWord", "alt-f": "project_search::ToggleFilters", - "shift-enter": "project_search::ToggleAllSearchResults", + "ctrl-shift-enter": "project_search::ToggleAllSearchResults", "alt-r": "search::ToggleRegex", // "ctrl-shift-alt-x": "search::ToggleRegex", "ctrl-k shift-enter": "pane::TogglePinTab", diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 041a0e8d044523cf4538973278105319880fd3a8..e783ecf9c1985c7b6bffa2d4de535448d50dbfdc 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -32,7 +32,7 @@ use util::ResultExt; use workspace::{ Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams}, + item::{ItemEvent, SaveOptions, TabContentParams}, searchable::SearchableItemHandle, }; use zed_actions::assistant::ToggleFocus; @@ -595,14 +595,6 @@ impl Item for AgentDiffPane { } } - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.editor.breadcrumbs(theme, cx) - } - fn added_to_workspace( &mut self, workspace: &mut Workspace, diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index 16d0ff10e1cfef058422ed79934bec53f74c4804..ff1dd8f1f3534f283620e91de2d8d1fa85bf04d7 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -15,12 +15,9 @@ doctest = false [dependencies] editor.workspace = true gpui.workspace = true -itertools.workspace = true -settings.workspace = true theme.workspace = true ui.workspace = true workspace.workspace = true -zed_actions.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 00c1c0939bbfaf18ea6d0550633f2ae05e16ef25..40436fcf7b87bbdb330fa27a76773c823179a96f 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -1,16 +1,10 @@ -use editor::Editor; -use gpui::{ - Context, Element, EventEmitter, Focusable, FontWeight, IntoElement, ParentElement, Render, - StyledText, Subscription, Window, -}; -use itertools::Itertools; -use settings::Settings; -use std::cmp; +use editor::render_breadcrumb_text; +use gpui::{Context, EventEmitter, IntoElement, Render, Subscription, Window}; use theme::ActiveTheme; -use ui::{ButtonLike, ButtonStyle, Label, Tooltip, prelude::*}; +use ui::prelude::*; use workspace::{ - TabBarSettings, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, - item::{BreadcrumbText, ItemEvent, ItemHandle}, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, + item::{ItemEvent, ItemHandle}, }; pub struct Breadcrumbs { @@ -39,118 +33,32 @@ impl EventEmitter for Breadcrumbs {} impl Render for Breadcrumbs { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - const MAX_SEGMENTS: usize = 12; - let element = h_flex() .id("breadcrumb-container") .flex_grow() + .h_8() .overflow_x_scroll() .text_ui(cx); let Some(active_item) = self.active_item.as_ref() else { - return element; + return element.into_any_element(); }; - let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else { - return element; + let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else { + return element.into_any_element(); }; - let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2); - let suffix_start_ix = cmp::max( - prefix_end_ix, - segments.len().saturating_sub(MAX_SEGMENTS / 2), - ); - - if suffix_start_ix > prefix_end_ix { - segments.splice( - prefix_end_ix..suffix_start_ix, - Some(BreadcrumbText { - text: "⋯".into(), - highlights: None, - font: None, - }), - ); - } - - let highlighted_segments = segments.into_iter().enumerate().map(|(index, segment)| { - let mut text_style = window.text_style(); - if let Some(ref font) = segment.font { - text_style.font_family = font.family.clone(); - text_style.font_features = font.features.clone(); - text_style.font_style = font.style; - text_style.font_weight = font.weight; - } - text_style.color = Color::Muted.color(cx); - - if index == 0 - && !TabBarSettings::get_global(cx).show - && active_item.is_dirty(cx) - && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx) - { - return styled_element; - } - - StyledText::new(segment.text.replace('\n', "⏎")) - .with_default_highlights(&text_style, segment.highlights.unwrap_or_default()) - .into_any() - }); - let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { - Label::new("›").color(Color::Placeholder).into_any_element() - }); - - let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs); - let prefix_element = active_item.breadcrumb_prefix(window, cx); - let breadcrumbs = if let Some(prefix) = prefix_element { - h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack) - } else { - breadcrumbs_stack - }; - - match active_item - .downcast::() - .map(|editor| editor.downgrade()) - { - Some(editor) => element.child( - ButtonLike::new("toggle outline view") - .child(breadcrumbs) - .style(ButtonStyle::Transparent) - .on_click({ - let editor = editor.clone(); - move |_, window, cx| { - if let Some((editor, callback)) = editor - .upgrade() - .zip(zed_actions::outline::TOGGLE_OUTLINE.get()) - { - callback(editor.to_any_view(), window, cx); - } - } - }) - .tooltip(move |_window, cx| { - if let Some(editor) = editor.upgrade() { - let focus_handle = editor.read(cx).focus_handle(cx); - Tooltip::for_action_in( - "Show Symbol Outline", - &zed_actions::outline::ToggleOutline, - &focus_handle, - cx, - ) - } else { - Tooltip::for_action( - "Show Symbol Outline", - &zed_actions::outline::ToggleOutline, - cx, - ) - } - }), - ), - None => element - // Match the height and padding of the `ButtonLike` in the other arm. - .h(rems_from_px(22.)) - .pl_1() - .child(breadcrumbs), - } + render_breadcrumb_text( + segments, + prefix_element, + active_item.as_ref(), + false, + window, + cx, + ) + .into_any_element() } } @@ -199,46 +107,3 @@ impl ToolbarItemView for Breadcrumbs { self.pane_focused = pane_focused; } } - -fn apply_dirty_filename_style( - segment: &BreadcrumbText, - text_style: &gpui::TextStyle, - cx: &mut Context, -) -> Option { - let text = segment.text.replace('\n', "⏎"); - - let filename_position = std::path::Path::new(&segment.text) - .file_name() - .and_then(|f| { - let filename_str = f.to_string_lossy(); - segment.text.rfind(filename_str.as_ref()) - })?; - - let bold_weight = FontWeight::BOLD; - let default_color = Color::Default.color(cx); - - if filename_position == 0 { - let mut filename_style = text_style.clone(); - filename_style.font_weight = bold_weight; - filename_style.color = default_color; - - return Some( - StyledText::new(text) - .with_default_highlights(&filename_style, []) - .into_any(), - ); - } - - let highlight_style = gpui::HighlightStyle { - font_weight: Some(bold_weight), - color: Some(default_color), - ..Default::default() - }; - - let highlight = vec![(filename_position..text.len(), highlight_style)]; - Some( - StyledText::new(text) - .with_default_highlights(text_style, highlight) - .into_any(), - ) -} diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 0eccf44c357125e5e11fcdccda8280c22006c6fa..3d59d0d0fda229e3db806725f24c0590fe9067fb 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -20,12 +20,14 @@ ctor.workspace = true editor.workspace = true gpui.workspace = true indoc.workspace = true +itertools.workspace = true language.workspace = true log.workspace = true lsp.workspace = true markdown.workspace = true project.workspace = true rand.workspace = true +search.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true @@ -34,7 +36,7 @@ theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true -itertools.workspace = true +zed_actions.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/diagnostics/src/buffer_diagnostics.rs b/crates/diagnostics/src/buffer_diagnostics.rs index adea53af17d81f691b5503effe781c6ab039a676..ef701241c5c50e2b5b42d9488a7d47985edbe4fd 100644 --- a/crates/diagnostics/src/buffer_diagnostics.rs +++ b/crates/diagnostics/src/buffer_diagnostics.rs @@ -6,7 +6,7 @@ use crate::{ use anyhow::Result; use collections::HashMap; use editor::{ - Editor, EditorEvent, EditorSettings, ExcerptRange, MultiBuffer, PathKey, + Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, multibuffer_context_lines, }; @@ -29,8 +29,8 @@ use std::{ use text::{Anchor, BufferSnapshot, OffsetRangeExt}; use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*}; use workspace::{ - ItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace, - item::{BreadcrumbText, Item, ItemEvent, TabContentParams}, + ItemHandle, ItemNavHistory, Workspace, + item::{Item, ItemEvent, TabContentParams}, }; actions!( @@ -701,18 +701,6 @@ impl Item for BufferDiagnosticsEditor { }); } - fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation { - if EditorSettings::get_global(cx).toolbar.breadcrumbs { - ToolbarItemLocation::PrimaryLeft - } else { - ToolbarItemLocation::Hidden - } - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.editor.breadcrumbs(theme, cx) - } - fn can_save(&self, _cx: &App) -> bool { true } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index d85eb07f68619e15bfe44d26282db3a3e49df4f3..afa3ebf42abd2968e779596aa6131b5f5a0c4c94 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -12,7 +12,7 @@ use buffer_diagnostics::BufferDiagnosticsEditor; use collections::{BTreeSet, HashMap, HashSet}; use diagnostic_renderer::DiagnosticBlock; use editor::{ - Editor, EditorEvent, EditorSettings, ExcerptRange, MultiBuffer, PathKey, + Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, multibuffer_context_lines, }; @@ -45,8 +45,8 @@ pub use toolbar_controls::ToolbarControls; use ui::{Icon, IconName, Label, h_flex, prelude::*}; use util::ResultExt; use workspace::{ - ItemNavHistory, ToolbarItemLocation, Workspace, - item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, + ItemNavHistory, Workspace, + item::{Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, searchable::SearchableItemHandle, }; @@ -894,18 +894,6 @@ impl Item for ProjectDiagnosticsEditor { Some(Box::new(self.editor.clone())) } - fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation { - if EditorSettings::get_global(cx).toolbar.breadcrumbs { - ToolbarItemLocation::PrimaryLeft - } else { - ToolbarItemLocation::Hidden - } - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.editor.breadcrumbs(theme, cx) - } - fn added_to_workspace( &mut self, workspace: &mut Workspace, diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 2ba64d39dfd63fc246bf3dedf5974909c0d67a6f..a1212c3dc50844a23ce080f41a6027598fbd9813 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,10 +1,11 @@ use crate::{BufferDiagnosticsEditor, ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh}; use gpui::{Context, EventEmitter, ParentElement, Render, Window}; use language::DiagnosticEntry; +use search::buffer_search; use text::{Anchor, BufferId}; -use ui::prelude::*; -use ui::{IconButton, IconButtonShape, IconName, Tooltip}; +use ui::{Tooltip, prelude::*}; use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, item::ItemHandle}; +use zed_actions::assistant::InlineAssist; pub struct ToolbarControls { editor: Option>, @@ -45,28 +46,44 @@ impl Render for ToolbarControls { None => {} } - let warning_tooltip = if include_warnings { - "Exclude Warnings" + let (warning_tooltip, warning_color) = if include_warnings { + ("Exclude Warnings", Color::Warning) } else { - "Include Warnings" - }; - - let warning_color = if include_warnings { - Color::Warning - } else { - Color::Muted + ("Include Warnings", Color::Disabled) }; h_flex() .gap_1() + .child({ + IconButton::new("toggle_search", IconName::MagnifyingGlass) + .icon_size(IconSize::Small) + .tooltip(Tooltip::for_action_title( + "Buffer Search", + &buffer_search::Deploy::find(), + )) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(buffer_search::Deploy::find()), cx); + }) + }) + .child({ + IconButton::new("inline_assist", IconName::ZedAssistant) + .icon_size(IconSize::Small) + .tooltip(Tooltip::for_action_title( + "Inline Assist", + &InlineAssist::default(), + )) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(InlineAssist::default()), cx); + }) + }) .map(|div| { if is_updating { div.child( IconButton::new("stop-updating", IconName::Stop) - .icon_color(Color::Info) - .shape(IconButtonShape::Square) + .icon_color(Color::Error) + .icon_size(IconSize::Small) .tooltip(Tooltip::for_action_title( - "Stop diagnostics update", + "Stop Siagnostics Update", &ToggleDiagnosticsRefresh, )) .on_click(cx.listener(move |toolbar_controls, _, _, cx| { @@ -79,10 +96,9 @@ impl Render for ToolbarControls { } else { div.child( IconButton::new("refresh-diagnostics", IconName::ArrowCircle) - .icon_color(Color::Info) - .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) .tooltip(Tooltip::for_action_title( - "Refresh diagnostics", + "Refresh Diagnostics", &ToggleDiagnosticsRefresh, )) .on_click(cx.listener({ @@ -98,7 +114,7 @@ impl Render for ToolbarControls { .child( IconButton::new("toggle-warnings", IconName::Warning) .icon_color(warning_color) - .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) .tooltip(Tooltip::text(warning_tooltip)) .on_click(cx.listener(|this, _, window, cx| { if let Some(editor) = &this.editor { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ac077844a634ed4f29d04e8f2878717b25393a4b..c6914a5a8667a8c5185dd03e2d1dc99752af0cb6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -58,6 +58,7 @@ pub use editor_settings::{ }; pub use element::{ CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, + render_breadcrumb_text, }; pub use git::blame::BlameRenderer; pub use hover_popover::hover_markdown_style; @@ -204,9 +205,9 @@ use workspace::{ CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, ViewId, Workspace, WorkspaceId, WorkspaceSettings, - item::{ItemBufferKind, ItemHandle, PreviewTabsSettings, SaveOptions}, + item::{BreadcrumbText, ItemBufferKind, ItemHandle, PreviewTabsSettings, SaveOptions}, notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, - searchable::SearchEvent, + searchable::{CollapseDirection, SearchEvent}, }; use crate::{ @@ -19257,37 +19258,25 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.buffer.read(cx).is_singleton() { + let has_folds = if self.buffer.read(cx).is_singleton() { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let has_folds = display_map .folds_in_range(MultiBufferOffset(0)..display_map.buffer_snapshot().len()) .next() .is_some(); - - if has_folds { - self.unfold_all(&actions::UnfoldAll, window, cx); - } else { - self.fold_all(&actions::FoldAll, window, cx); - } + has_folds } else { let buffer_ids = self.buffer.read(cx).excerpt_buffer_ids(); - let should_unfold = buffer_ids + let has_folds = buffer_ids .iter() .any(|buffer_id| self.is_buffer_folded(*buffer_id, cx)); + has_folds + }; - self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| { - editor - .update_in(cx, |editor, _, cx| { - for buffer_id in buffer_ids { - if should_unfold { - editor.unfold_buffer(buffer_id, cx); - } else { - editor.fold_buffer(buffer_id, cx); - } - } - }) - .ok(); - }); + if has_folds { + self.unfold_all(&actions::UnfoldAll, window, cx); + } else { + self.fold_all(&actions::FoldAll, window, cx); } } @@ -19452,6 +19441,9 @@ impl Editor { .ok(); }); } + cx.emit(SearchEvent::ResultsCollapsedChanged( + CollapseDirection::Collapsed, + )); } pub fn fold_function_bodies( @@ -19640,6 +19632,9 @@ impl Editor { .ok(); }); } + cx.emit(SearchEvent::ResultsCollapsedChanged( + CollapseDirection::Expanded, + )); } pub fn fold_selected_ranges( @@ -23330,6 +23325,53 @@ impl Editor { show_underlines: self.diagnostics_enabled(), } } + fn breadcrumbs_inner(&self, variant: &Theme, cx: &App) -> Option> { + let cursor = self.selections.newest_anchor().head(); + let multibuffer = self.buffer().read(cx); + let is_singleton = multibuffer.is_singleton(); + let (buffer_id, symbols) = multibuffer + .read(cx) + .symbols_containing(cursor, Some(variant.syntax()))?; + let buffer = multibuffer.buffer(buffer_id)?; + + let buffer = buffer.read(cx); + let settings = ThemeSettings::get_global(cx); + // In a multi-buffer layout, we don't want to include the filename in the breadcrumbs + let mut breadcrumbs = if is_singleton { + let text = self.breadcrumb_header.clone().unwrap_or_else(|| { + buffer + .snapshot() + .resolve_file_path( + self.project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(), + cx, + ) + .unwrap_or_else(|| { + if multibuffer.is_singleton() { + multibuffer.title(cx).to_string() + } else { + "untitled".to_string() + } + }) + }); + vec![BreadcrumbText { + text, + highlights: None, + font: Some(settings.buffer_font.clone()), + }] + } else { + vec![] + }; + + breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText { + text: symbol.text, + highlights: Some(symbol.highlight_ranges), + font: Some(settings.buffer_font.clone()), + })); + Some(breadcrumbs) + } } fn edit_for_markdown_paste<'a>( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b31d20f0da2ed89ee0dcb4f2d3419b3105dd5c98..2c3eb5b996f254a3734cfd95742e3f63a11c027f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -41,14 +41,14 @@ use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatu use gpui::{ Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, - DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, + DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, FontWeight, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, KeybindingKeystroke, Length, Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, - Size, StatefulInteractiveElement, Style, Styled, TextAlign, TextRun, TextStyleRefinement, - WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, - point, px, quad, relative, size, solid_background, transparent_black, + Size, StatefulInteractiveElement, Style, Styled, StyledText, TextAlign, TextRun, + TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, + linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, }; use itertools::Itertools; use language::{IndentGuideSettings, language_settings::ShowWhitespaceSetting}; @@ -94,8 +94,9 @@ use unicode_segmentation::UnicodeSegmentation; use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; use workspace::{ - CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, - item::{Item, ItemBufferKind}, + CollaboratorId, ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, + Workspace, + item::{BreadcrumbText, Item, ItemBufferKind}, notifications::NotifyTaskExt, }; @@ -3878,6 +3879,13 @@ impl EditorElement { let editor = self.editor.read(cx); let multi_buffer = editor.buffer.read(cx); let is_read_only = self.editor.read(cx).read_only(cx); + let editor_handle: &dyn ItemHandle = &self.editor; + + let breadcrumbs = if is_selected { + editor.breadcrumbs_inner(cx.theme(), cx) + } else { + None + }; let file_status = multi_buffer .all_diff_hunks_expanded() @@ -4035,57 +4043,103 @@ impl EditorElement { .id("path_header_block") .min_w_0() .size_full() + .gap_1() .justify_between() .overflow_hidden() - .child(h_flex().min_w_0().flex_1().gap_0p5().map(|path_header| { - let filename = filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()); - - path_header - .when(ItemSettings::get_global(cx).file_icons, |el| { - let path = path::Path::new(filename.as_str()); - let icon = - FileIcons::get_icon(path, cx).unwrap_or_default(); - - el.child(Icon::from_path(icon).color(Color::Muted)) - }) - .child( - ButtonLike::new("filename-button") - .child( - Label::new(filename) - .single_line() - .color(file_status_label_color(file_status)) - .when( - file_status.is_some_and(|s| s.is_deleted()), - |label| label.strikethrough(), + .child(h_flex().min_w_0().flex_1().gap_0p5().overflow_hidden().map( + |path_header| { + let filename = filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()); + + let full_path = match parent_path.as_deref() { + Some(parent) if !parent.is_empty() => { + format!("{}{}", parent, filename.as_str()) + } + _ => filename.as_str().to_string(), + }; + + path_header + .child( + ButtonLike::new("filename-button") + .when( + ItemSettings::get_global(cx).file_icons, + |this| { + let path = + path::Path::new(filename.as_str()); + let icon = FileIcons::get_icon(path, cx) + .unwrap_or_default(); + + this.child( + Icon::from_path(icon) + .color(Color::Muted), + ) + }, + ) + .child( + Label::new(filename) + .single_line() + .color(file_status_label_color(file_status)) + .buffer_font(cx) + .when( + file_status + .is_some_and(|s| s.is_deleted()), + |label| label.strikethrough(), + ), + ) + .tooltip(move |_, cx| { + Tooltip::with_meta( + "Open File", + None, + full_path.clone(), + cx, + ) + }) + .on_click(window.listener_for(&self.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, + ); + } + })), + ) + .when_some(parent_path, |then, path| { + then.child( + Label::new(path) + .buffer_font(cx) + .truncate_start() + .color( + if file_status + .is_some_and(FileStatus::is_deleted) + { + Color::Custom(colors.text_disabled) + } else { + Color::Custom(colors.text_muted) + }, ), ) - .on_click(window.listener_for(&self.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, - ); - } - })), - ) - .when(!for_excerpt.buffer.capability.editable(), |el| { - el.child(Icon::new(IconName::FileLock).color(Color::Muted)) - }) - .when_some(parent_path, |then, path| { - then.child(Label::new(path).truncate().color( - if file_status.is_some_and(FileStatus::is_deleted) { - Color::Custom(colors.text_disabled) - } else { - Color::Custom(colors.text_muted) - }, - )) - }) - })) + }) + .when(!for_excerpt.buffer.capability.editable(), |el| { + el.child( + Icon::new(IconName::FileLock).color(Color::Muted), + ) + }) + .when_some(breadcrumbs, |then, breadcrumbs| { + then.child(render_breadcrumb_text( + breadcrumbs, + None, + editor_handle, + true, + window, + cx, + )) + }) + }, + )) .when( can_open_excerpts && is_selected && relative_path.is_some(), |el| { @@ -7875,6 +7929,168 @@ impl EditorElement { } } +pub fn render_breadcrumb_text( + mut segments: Vec, + prefix: Option, + active_item: &dyn ItemHandle, + multibuffer_header: bool, + window: &mut Window, + cx: &App, +) -> impl IntoElement { + const MAX_SEGMENTS: usize = 12; + + let element = h_flex().flex_grow().text_ui(cx); + + let prefix_end_ix = cmp::min(segments.len(), MAX_SEGMENTS / 2); + let suffix_start_ix = cmp::max( + prefix_end_ix, + segments.len().saturating_sub(MAX_SEGMENTS / 2), + ); + + if suffix_start_ix > prefix_end_ix { + segments.splice( + prefix_end_ix..suffix_start_ix, + Some(BreadcrumbText { + text: "⋯".into(), + highlights: None, + font: None, + }), + ); + } + + let highlighted_segments = segments.into_iter().enumerate().map(|(index, segment)| { + let mut text_style = window.text_style(); + if let Some(ref font) = segment.font { + text_style.font_family = font.family.clone(); + text_style.font_features = font.features.clone(); + text_style.font_style = font.style; + text_style.font_weight = font.weight; + } + text_style.color = Color::Muted.color(cx); + + if index == 0 + && !workspace::TabBarSettings::get_global(cx).show + && active_item.is_dirty(cx) + && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx) + { + return styled_element; + } + + StyledText::new(segment.text.replace('\n', "⏎")) + .with_default_highlights(&text_style, segment.highlights.unwrap_or_default()) + .into_any() + }); + + let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { + Label::new("›").color(Color::Placeholder).into_any_element() + }); + + let breadcrumbs_stack = h_flex() + .gap_1() + .when(multibuffer_header, |this| { + this.pl_2() + .border_l_1() + .border_color(cx.theme().colors().border.opacity(0.6)) + }) + .children(breadcrumbs); + + let breadcrumbs = if let Some(prefix) = prefix { + h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack) + } else { + breadcrumbs_stack + }; + + let editor = active_item + .downcast::() + .map(|editor| editor.downgrade()); + + match editor { + Some(editor) => element + .id("breadcrumb_container") + .when(!multibuffer_header, |this| this.overflow_x_scroll()) + .child( + ButtonLike::new("toggle outline view") + .child(breadcrumbs) + .when(multibuffer_header, |this| { + this.style(ButtonStyle::Transparent) + }) + .when(!multibuffer_header, |this| { + let focus_handle = editor.upgrade().unwrap().focus_handle(&cx); + + this.tooltip(move |_window, cx| { + Tooltip::for_action_in( + "Show Symbol Outline", + &zed_actions::outline::ToggleOutline, + &focus_handle, + cx, + ) + }) + .on_click({ + let editor = editor.clone(); + move |_, window, cx| { + if let Some((editor, callback)) = editor + .upgrade() + .zip(zed_actions::outline::TOGGLE_OUTLINE.get()) + { + callback(editor.to_any_view(), window, cx); + } + } + }) + }), + ) + .into_any_element(), + None => element + // Match the height and padding of the `ButtonLike` in the other arm. + .h(rems_from_px(22.)) + .pl_1() + .child(breadcrumbs) + .into_any_element(), + } +} + +fn apply_dirty_filename_style( + segment: &BreadcrumbText, + text_style: &gpui::TextStyle, + cx: &App, +) -> Option { + let text = segment.text.replace('\n', "⏎"); + + let filename_position = std::path::Path::new(&segment.text) + .file_name() + .and_then(|f| { + let filename_str = f.to_string_lossy(); + segment.text.rfind(filename_str.as_ref()) + })?; + + let bold_weight = FontWeight::BOLD; + let default_color = Color::Default.color(cx); + + if filename_position == 0 { + let mut filename_style = text_style.clone(); + filename_style.font_weight = bold_weight; + filename_style.color = default_color; + + return Some( + StyledText::new(text) + .with_default_highlights(&filename_style, []) + .into_any(), + ); + } + + let highlight_style = gpui::HighlightStyle { + font_weight: Some(bold_weight), + color: Some(default_color), + ..Default::default() + }; + + let highlight = vec![(filename_position..text.len(), highlight_style)]; + Some( + StyledText::new(text) + .with_default_highlights(text_style, highlight) + .into_any(), + ) +} + fn file_status_label_color(file_status: Option) -> Color { file_status.map_or(Color::Default, |status| { if status.is_conflicted() { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 278379a0caf5a177105674ab889666a34815752f..4578e3b5fd99700b9a981785501be363876e6c06 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -38,7 +38,7 @@ use std::{ sync::Arc, }; use text::{BufferId, BufferSnapshot, Selection}; -use theme::{Theme, ThemeSettings}; +use theme::Theme; use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ @@ -963,56 +963,21 @@ impl Item for Editor { self.pixel_position_of_newest_cursor } - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - if self.show_breadcrumbs { + fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation { + if self.show_breadcrumbs && self.buffer().read(cx).is_singleton() { ToolbarItemLocation::PrimaryLeft } else { ToolbarItemLocation::Hidden } } + // In a non-singleton case, the breadcrumbs are actually shown on sticky file headers of the multibuffer. fn breadcrumbs(&self, variant: &Theme, cx: &App) -> Option> { - let cursor = self.selections.newest_anchor().head(); - let multibuffer = self.buffer().read(cx); - let (buffer_id, symbols) = multibuffer - .read(cx) - .symbols_containing(cursor, Some(variant.syntax()))?; - let buffer = multibuffer.buffer(buffer_id)?; - - let buffer = buffer.read(cx); - let text = self.breadcrumb_header.clone().unwrap_or_else(|| { - buffer - .snapshot() - .resolve_file_path( - self.project - .as_ref() - .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) - .unwrap_or_default(), - cx, - ) - .unwrap_or_else(|| { - if multibuffer.is_singleton() { - multibuffer.title(cx).to_string() - } else { - "untitled".to_string() - } - }) - }); - - let settings = ThemeSettings::get_global(cx); - - let mut breadcrumbs = vec![BreadcrumbText { - text, - highlights: None, - font: Some(settings.buffer_font.clone()), - }]; - - breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText { - text: symbol.text, - highlights: Some(symbol.highlight_ranges), - font: Some(settings.buffer_font.clone()), - })); - Some(breadcrumbs) + if self.buffer.read(cx).is_singleton() { + self.breadcrumbs_inner(variant, cx) + } else { + None + } } fn added_to_workspace( diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 77e063d2ed3acd6583b175a81b107bfe83e95e99..51c8403a0a41698053075d6f6f2cb6eed05fc72c 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -30,7 +30,7 @@ use workspace::item::TabTooltipContent; use workspace::{ Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - item::{BreadcrumbText, ItemEvent, TabContentParams}, + item::{ItemEvent, TabContentParams}, notifications::NotifyTaskExt, pane::SaveIntent, searchable::SearchableItemHandle, @@ -925,14 +925,6 @@ impl Item for CommitView { .update(cx, |editor, cx| editor.navigate(data, window, cx)) } - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - ToolbarItemLocation::Hidden - } - - fn breadcrumbs(&self, _theme: &theme::Theme, _cx: &App) -> Option> { - None - } - fn added_to_workspace( &mut self, workspace: &mut Workspace, diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 9c5a6f9f3cf82d1578a3e69cf48290a17088d079..5298f9c271cc5aa2c688e68b9f6e5a52ad58981f 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -41,7 +41,7 @@ use util::{ResultExt as _, rel_path::RelPath}; use workspace::{ CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, + item::{Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, notifications::NotifyTaskExt, searchable::SearchableItemHandle, }; @@ -912,18 +912,6 @@ impl Item for ProjectDiff { } } - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.editor - .read(cx) - .last_selected_editor() - .read(cx) - .breadcrumbs(theme, cx) - } - fn added_to_workspace( &mut self, workspace: &mut Workspace, diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index 38d7ed2ef1064da655988c251b664564e4f763a8..1f80e2ace7fc67d1ffcefe220ac95a94b04b088f 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -22,8 +22,8 @@ use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString}; use util::paths::PathExt; use workspace::{ - Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, - item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams}, + Item, ItemHandle as _, ItemNavHistory, Workspace, + item::{ItemEvent, SaveOptions, TabContentParams}, searchable::SearchableItemHandle, }; @@ -375,14 +375,6 @@ impl Item for TextDiffView { .update(cx, |editor, cx| editor.navigate(data, window, cx)) } - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.diff_editor.breadcrumbs(theme, cx) - } - fn added_to_workspace( &mut self, workspace: &mut Workspace, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 7ca1d9d909feb20c2a5cf3e350958a35dca83668..1d745a84ec10d8b296ebd8e5b2be8b61aeb98e80 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -5,13 +5,16 @@ use crate::{ SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, buffer_search::registrar::WithResultsOrExternalQuery, - search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, + search_bar::{ + ActionButtonState, alignment_element, filter_search_results_input, input_base_styles, + render_action_button, render_text_input, + }, }; use any_vec::AnyVec; use collections::HashMap; use editor::{ DisplayPoint, Editor, EditorSettings, MultiBufferOffset, - actions::{Backtab, Tab}, + actions::{Backtab, FoldAll, Tab, ToggleFoldAll, UnfoldAll}, }; use futures::channel::oneshot; use gpui::{ @@ -27,19 +30,17 @@ use project::{ use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; -use std::sync::Arc; +use std::{any::TypeId, sync::Arc}; use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath}; -use ui::{ - BASE_REM_SIZE_IN_PX, IconButton, IconButtonShape, IconName, Tooltip, h_flex, prelude::*, - 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, - item::ItemHandle, + item::{ItemBufferKind, ItemHandle}, searchable::{ - Direction, FilteredSearchRange, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle, + CollapseDirection, Direction, FilteredSearchRange, SearchEvent, SearchableItemHandle, + WeakSearchableItemHandle, }, }; @@ -129,18 +130,72 @@ pub struct BufferSearchBar { editor_scroll_handle: ScrollHandle, editor_needed_width: Pixels, regex_language: Option>, + is_collapsed: bool, } impl EventEmitter for BufferSearchBar {} impl EventEmitter for BufferSearchBar {} impl Render for BufferSearchBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.dismissed { - return div().id("search_bar"); - } - let focus_handle = self.focus_handle(cx); + 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", + ) + } else { + ( + IconName::ChevronDownUp, + "Collapse All", + "Collapse All Search Results", + ) + }; + + if self.dismissed { + let button = 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(), + cx, + ) + }) + .on_click(|_event, window, cx| { + 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) + }) + .into_any_element(), + ) + } else { + None + }; + let narrow_mode = self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX; let hide_inline_icons = self.editor_needed_width @@ -203,7 +258,13 @@ impl Render for BufferSearchBar { let input_base_styles = |border_color| input_base_styles(border_color, |div| div.w(input_width)); - let query_column = input_base_styles(query_border) + let input_style = if find_in_results { + filter_search_results_input(query_border, |div| div.w(input_width), cx) + } else { + input_base_styles(query_border) + }; + + let query_column = input_style .id("editor-scroll") .track_scroll(&self.editor_scroll_handle) .child(render_text_input(&self.query_editor, color_override, cx)) @@ -336,11 +397,14 @@ impl Render for BufferSearchBar { )) }); + let has_collapse_button = collapse_expand_button.is_some(); + let search_line = h_flex() .w_full() .gap_2() - .when(find_in_results, |el| { - el.child(Label::new("Find in results").color(Color::Hint)) + .when(find_in_results, |el| el.child(alignment_element())) + .when(!find_in_results && has_collapse_button, |el| { + el.pl_0p5().child(collapse_expand_button.expect("button")) }) .child(query_column) .child(mode_column); @@ -370,9 +434,11 @@ impl Render for BufferSearchBar { &ReplaceAll, focus_handle, )); + h_flex() .w_full() .gap_2() + .when(has_collapse_button, |this| this.child(alignment_element())) .child(replace_column) .child(replace_actions) }); @@ -395,26 +461,36 @@ impl Render for BufferSearchBar { h_flex() .relative() .child(search_line) - .when(!narrow_mode && !find_in_results, |div| { - div.child(h_flex().absolute().right_0().child(render_action_button( - "buffer-search", - IconName::Close, - Default::default(), - "Close Search Bar", - &Dismiss, - focus_handle.clone(), - ))) - .w_full() + .when(!narrow_mode && !find_in_results, |this| { + this.child( + h_flex() + .absolute() + .right_0() + .when(has_collapse_button, |this| { + this.pr_2() + .border_r_1() + .border_color(cx.theme().colors().border_variant) + }) + .child(render_action_button( + "buffer-search", + IconName::Close, + Default::default(), + "Close Search Bar", + &Dismiss, + focus_handle.clone(), + )), + ) }); + v_flex() .id("buffer_search") .gap_2() - .py(px(1.0)) .w_full() .track_scroll(&self.scroll_handle) .key_context(key_context) .capture_action(cx.listener(Self::tab)) .capture_action(cx.listener(Self::backtab)) + .capture_action(cx.listener(Self::toggle_fold_all)) .on_action(cx.listener(Self::previous_history_query)) .on_action(cx.listener(Self::next_history_query)) .on_action(cx.listener(Self::dismiss)) @@ -455,6 +531,7 @@ impl Render for BufferSearchBar { .child(search_line) .children(query_error_line) .children(replace_line) + .into_any_element() } } @@ -547,7 +624,9 @@ impl ToolbarItemView for BufferSearchBar { let is_project_search = searchable_item_handle.supported_options(cx).find_in_results; self.active_searchable_item = Some(searchable_item_handle); drop(self.update_matches(true, false, window, cx)); - if !self.dismissed { + if self.needs_expand_collapse_option(cx) { + return ToolbarItemLocation::PrimaryLeft; + } else if !self.is_dismissed() { if is_project_search { self.dismiss(&Default::default(), window, cx); } else { @@ -737,6 +816,7 @@ impl BufferSearchBar { editor_scroll_handle: ScrollHandle::new(), editor_needed_width: px(0.), regex_language: None, + is_collapsed: false, } } @@ -756,6 +836,9 @@ impl BufferSearchBar { searchable_item.clear_matches(window, cx); } } + + let needs_collapse_expand = self.needs_expand_collapse_option(cx); + if let Some(active_editor) = self.active_searchable_item.as_mut() { self.selection_search_enabled = None; self.replace_enabled = false; @@ -765,6 +848,14 @@ impl BufferSearchBar { self.focus(&handle, window, cx); } + if needs_collapse_expand { + cx.emit(Event::UpdateLocation); + cx.emit(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::PrimaryLeft, + )); + cx.notify(); + return; + } cx.emit(Event::UpdateLocation); cx.emit(ToolbarItemEvent::ChangeLocation( ToolbarItemLocation::Hidden, @@ -845,7 +936,11 @@ impl BufferSearchBar { cx.notify(); cx.emit(Event::UpdateLocation); cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Secondary, + if self.needs_expand_collapse_option(cx) { + ToolbarItemLocation::PrimaryLeft + } else { + ToolbarItemLocation::Secondary + }, )); true } @@ -857,6 +952,45 @@ impl BufferSearchBar { .unwrap_or_default() } + // We provide an expand/collapse button if we are in a multibuffer + // and not doing a project search. + fn needs_expand_collapse_option(&self, cx: &App) -> bool { + if let Some(item) = &self.active_searchable_item { + let buffer_kind = item.buffer_kind(cx); + + if buffer_kind == ItemBufferKind::Singleton { + return false; + } + + let workspace::searchable::SearchOptions { + find_in_results, .. + } = item.supported_options(cx); + !find_in_results + } else { + false + } + } + + fn toggle_fold_all(&mut self, _: &ToggleFoldAll, window: &mut Window, cx: &mut Context) { + self.toggle_fold_all_in_item(window, cx); + } + + fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context) { + let is_collapsed = self.is_collapsed; + if let Some(item) = &self.active_searchable_item { + if let Some(item) = item.act_as_type(TypeId::of::(), cx) { + let editor = item.downcast::().expect("Is an editor"); + editor.update(cx, |editor, cx| { + if is_collapsed { + editor.unfold_all(&UnfoldAll, window, cx); + } else { + editor.fold_all(&FoldAll, window, cx); + } + }) + } + } + } + pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context) { let search = self.query_suggestion(window, cx).map(|suggestion| { self.search(&suggestion, Some(self.default_options), true, window, cx) @@ -1223,6 +1357,15 @@ impl BufferSearchBar { drop(self.update_matches(false, false, window, cx)); } SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx), + SearchEvent::ResultsCollapsedChanged(collapse_direction) => { + if self.needs_expand_collapse_option(cx) { + match collapse_direction { + CollapseDirection::Collapsed => self.is_collapsed = true, + CollapseDirection::Expanded => self.is_collapsed = false, + } + } + cx.notify(); + } } } @@ -1659,7 +1802,7 @@ mod tests { use super::*; use editor::{ - DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects, + DisplayPoint, Editor, ExcerptRange, MultiBuffer, SearchSettings, SelectionEffects, display_map::DisplayRow, test::editor_test_context::EditorTestContext, }; use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext}; @@ -1680,6 +1823,79 @@ mod tests { }); } + fn init_multibuffer_test( + cx: &mut TestAppContext, + ) -> ( + Entity, + Entity, + &mut VisualTestContext, + ) { + init_globals(cx); + + let buffer1 = cx.new(|cx| { + Buffer::local( + r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(), + cx, + ) + }); + + let buffer2 = cx.new(|cx| { + Buffer::local( + r#" + Some Additional text with the term regular expression in it. + There two lines. + "# + .unindent(), + cx, + ) + }); + + let multibuffer = cx.new(|cx| { + let mut buffer = MultiBuffer::new(language::Capability::ReadWrite); + + //[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))] + buffer.push_excerpts( + buffer1, + [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))], + cx, + ); + buffer.push_excerpts( + buffer2, + [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], + cx, + ); + + buffer + }); + let mut editor = None; + let window = cx.add_window(|window, cx| { + let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure( + "keymaps/default-macos.json", + cx, + ) + .unwrap(); + cx.bind_keys(default_key_bindings); + editor = + Some(cx.new(|cx| Editor::for_multibuffer(multibuffer.clone(), None, window, cx))); + + let mut search_bar = BufferSearchBar::new(None, window, cx); + search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx); + search_bar.show(window, cx); + search_bar + }); + let search_bar = window.root(cx).unwrap(); + + let cx = VisualTestContext::from_window(*window, cx).into_mut(); + + (editor.unwrap(), search_bar, cx) + } + fn init_test( cx: &mut TestAppContext, ) -> ( @@ -2978,38 +3194,140 @@ mod tests { #[perf] #[gpui::test] - async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) { + async fn test_hides_and_uses_secondary_when_in_singleton_buffer(cx: &mut TestAppContext) { let (editor, search_bar, cx) = init_test(cx); - // Search using valid regexp - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.enable_search_option(SearchOptions::REGEX, window, cx); - search_bar.search("expression", None, true, window, cx) - }) - .await - .unwrap(); + + let initial_location = search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.set_active_pane_item(Some(&editor), window, cx) + }); + + assert_eq!(initial_location, ToolbarItemLocation::Secondary); + + let mut events = cx.events(&search_bar); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.dismiss(&Dismiss, window, cx); + }); + + assert_eq!( + events.try_next().unwrap(), + Some(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Hidden + )) + ); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.show(window, cx); + }); + + assert_eq!( + events.try_next().unwrap(), + Some(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Secondary + )) + ); + } + + #[perf] + #[gpui::test] + async fn test_uses_primary_left_when_in_multi_buffer(cx: &mut TestAppContext) { + let (editor, search_bar, cx) = init_multibuffer_test(cx); + + let initial_location = search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.set_active_pane_item(Some(&editor), window, cx) + }); + + assert_eq!(initial_location, ToolbarItemLocation::PrimaryLeft); + + let mut events = cx.events(&search_bar); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.dismiss(&Dismiss, window, cx); + }); + + assert_eq!( + events.try_next().unwrap(), + Some(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::PrimaryLeft + )) + ); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.show(window, cx); + }); + + assert_eq!( + events.try_next().unwrap(), + Some(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::PrimaryLeft + )) + ); + } + + #[perf] + #[gpui::test] + async fn test_hides_and_uses_secondary_when_part_of_project_search(cx: &mut TestAppContext) { + let (editor, search_bar, cx) = init_multibuffer_test(cx); + + editor.update(cx, |editor, _| { + editor.set_in_project_search(true); + }); + + let initial_location = search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.set_active_pane_item(Some(&editor), window, cx) + }); + + assert_eq!(initial_location, ToolbarItemLocation::Hidden); + + let mut events = cx.events(&search_bar); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.dismiss(&Dismiss, window, cx); + }); + + assert_eq!( + events.try_next().unwrap(), + Some(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Hidden + )) + ); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.show(window, cx); + }); + + assert_eq!( + events.try_next().unwrap(), + Some(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Secondary + )) + ); + } + + #[perf] + #[gpui::test] + async fn test_sets_collapsed_when_editor_fold_events_emitted(cx: &mut TestAppContext) { + let (editor, search_bar, cx) = init_multibuffer_test(cx); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.set_active_pane_item(Some(&editor), window, cx); + }); + editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[ - DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20), - DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19), - ], - ); + editor.fold_all(&FoldAll, window, cx); }); - // Now, the expression is invalid - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("expression (", None, true, window, cx) - }) - .await - .unwrap_err(); + let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed); + + assert!(is_collapsed); + editor.update_in(cx, |editor, window, cx| { - assert!( - display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(), - ); + editor.unfold_all(&UnfoldAll, window, cx); }); + + let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed); + + assert!(!is_collapsed); } #[perf] diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index f27e109e68cd4ac01fb091a16ce2565dcd495ec7..cacf6b0dec4db0c823f92fd0ed93e0dd681900f0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3,14 +3,17 @@ use crate::{ SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy, - search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, + search_bar::{ + ActionButtonState, alignment_element, input_base_styles, render_action_button, + render_text_input, + }, }; use anyhow::Context as _; use collections::HashMap; use editor::{ Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, PathKey, SelectionEffects, - actions::{Backtab, SelectAll, Tab}, + actions::{Backtab, FoldAll, SelectAll, Tab, UnfoldAll}, items::active_match_index, multibuffer_context_lines, scroll::Autoscroll, @@ -42,8 +45,8 @@ use util::{ResultExt as _, paths::PathMatcher, rel_path::RelPath}; use workspace::{ DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, - item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions}, - searchable::{Direction, SearchableItem, SearchableItemHandle}, + item::{Item, ItemEvent, ItemHandle, SaveOptions}, + searchable::{CollapseDirection, Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; actions!( @@ -660,56 +663,6 @@ impl Item for ProjectSearchView { _ => {} } } - - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - if self.has_matches() { - ToolbarItemLocation::Secondary - } else { - ToolbarItemLocation::Hidden - } - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.results_editor.breadcrumbs(theme, cx) - } - - fn breadcrumb_prefix( - &self, - _window: &mut Window, - cx: &mut Context, - ) -> Option { - if !self.has_matches() { - return None; - } - - let is_collapsed = self.results_collapsed; - - let (icon, tooltip_label) = if is_collapsed { - (IconName::ChevronUpDown, "Expand All Search Results") - } else { - (IconName::ChevronDownUp, "Collapse All Search Results") - }; - - let focus_handle = self.query_editor.focus_handle(cx); - - Some( - IconButton::new("project-search-collapse-expand", icon) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip(move |_, cx| { - Tooltip::for_action_in( - tooltip_label, - &ToggleAllSearchResults, - &focus_handle, - cx, - ) - }) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_all_search_results(&ToggleAllSearchResults, window, cx); - })) - .into_any_element(), - ) - } } impl ProjectSearchView { @@ -815,26 +768,18 @@ impl ProjectSearchView { fn toggle_all_search_results( &mut self, _: &ToggleAllSearchResults, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) { - self.results_collapsed = !self.results_collapsed; - self.update_results_visibility(cx); + self.update_results_visibility(window, cx); } - fn update_results_visibility(&mut self, cx: &mut Context) { + fn update_results_visibility(&mut self, window: &mut Window, cx: &mut Context) { self.results_editor.update(cx, |editor, cx| { - let multibuffer = editor.buffer().read(cx); - let buffer_ids = multibuffer.excerpt_buffer_ids(); - if self.results_collapsed { - for buffer_id in buffer_ids { - editor.fold_buffer(buffer_id, cx); - } + editor.unfold_all(&UnfoldAll, window, cx); } else { - for buffer_id in buffer_ids { - editor.unfold_buffer(buffer_id, cx); - } + editor.fold_all(&FoldAll, window, cx); } }); cx.notify(); @@ -924,6 +869,21 @@ impl ProjectSearchView { cx.emit(ViewEvent::EditorEvent(event.clone())); }), ); + subscriptions.push(cx.subscribe( + &results_editor, + |this, _editor, event: &SearchEvent, cx| { + match event { + SearchEvent::ResultsCollapsedChanged(collapsed_direction) => { + match collapsed_direction { + CollapseDirection::Collapsed => this.results_collapsed = true, + CollapseDirection::Expanded => this.results_collapsed = false, + } + } + _ => (), + }; + cx.notify(); + }, + )); let included_files_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); @@ -2036,7 +1996,7 @@ impl ProjectSearchBar { impl Render for ProjectSearchBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let Some(search) = self.active_project_search.clone() else { - return div(); + return div().into_any_element(); }; let search = search.read(cx); let focus_handle = search.focus_handle(cx); @@ -2138,7 +2098,7 @@ impl Render for ProjectSearchBar { .then_some(ActionButtonState::Disabled), "Select Next Match", &SelectNextMatch, - query_focus, + query_focus.clone(), )) .child( div() @@ -2202,9 +2162,37 @@ impl Render for ProjectSearchBar { )) .child(matches_column); + let is_collapsed = search.results_collapsed; + + let (icon, tooltip_label) = if is_collapsed { + (IconName::ChevronUpDown, "Expand All Search Results") + } else { + (IconName::ChevronDownUp, "Collapse All Search Results") + }; + + let expand_button = IconButton::new("project-search-collapse-expand", icon) + .shape(IconButtonShape::Square) + .tooltip(move |_, cx| { + Tooltip::for_action_in( + tooltip_label, + &ToggleAllSearchResults, + &query_focus.clone(), + cx, + ) + }) + .on_click(cx.listener(|this, _, window, cx| { + if let Some(active_view) = &this.active_project_search { + active_view.update(cx, |active_view, cx| { + active_view.toggle_all_search_results(&ToggleAllSearchResults, window, cx); + }) + } + })); + let search_line = h_flex() + .pl_0p5() .w_full() .gap_2() + .child(expand_button) .child(query_column) .child(mode_column); @@ -2237,6 +2225,7 @@ impl Render for ProjectSearchBar { h_flex() .w_full() .gap_2() + .child(alignment_element()) .child(replace_column) .child(replace_actions) }); @@ -2273,8 +2262,9 @@ impl Render for ProjectSearchBar { .child(SearchOption::IncludeIgnored.as_button( search.search_options, SearchSource::Project(cx), - focus_handle.clone(), + focus_handle, )); + h_flex() .w_full() .gap_2() @@ -2282,6 +2272,7 @@ impl Render for ProjectSearchBar { h_flex() .gap_2() .w(input_width) + .child(alignment_element()) .child(include) .child(exclude), ) @@ -2323,7 +2314,6 @@ impl Render for ProjectSearchBar { v_flex() .gap_2() - .py(px(1.0)) .w_full() .key_context(key_context) .on_action(cx.listener(|this, _: &ToggleFocus, window, cx| { @@ -2370,6 +2360,7 @@ impl Render for ProjectSearchBar { .children(replace_line) .children(filter_line) .children(filter_error_line) + .into_any_element() } } @@ -2720,6 +2711,32 @@ pub mod tests { (dp(5, 6)..dp(5, 9), "match"), ], ); + search_view + .update(cx, |search_view, window, cx| { + search_view.results_editor.update(cx, |editor, cx| { + editor.fold_all(&FoldAll, window, cx); + }) + }) + .expect("Should fold fine"); + + let results_collapsed = search_view + .read_with(cx, |search_view, _| search_view.results_collapsed) + .expect("got results_collapsed"); + + assert!(results_collapsed); + search_view + .update(cx, |search_view, window, cx| { + search_view.results_editor.update(cx, |editor, cx| { + editor.unfold_all(&UnfoldAll, window, cx); + }) + }) + .expect("Should unfold fine"); + + let results_collapsed = search_view + .read_with(cx, |search_view, _| search_view.results_collapsed) + .expect("got results_collapsed"); + + assert!(!results_collapsed); } #[perf] diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index a1f6c070724c4d57b438c452ef4b4ae3cf20e66d..690b2eb927ce7384b7e6e313aeb5c825c544cdc9 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -50,6 +50,22 @@ pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div .border_color(border_color) .rounded_md() } +pub(crate) fn filter_search_results_input( + border_color: Hsla, + map: impl FnOnce(Div) -> Div, + cx: &App, +) -> Div { + input_base_styles(border_color, map).pl_0().child( + h_flex() + .mr_2() + .px_2() + .h_full() + .border_r_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().text_accent.opacity(0.05)) + .child(Label::new("Find in Results").color(Color::Muted)), + ) +} pub(crate) fn render_text_input( editor: &Entity, @@ -89,3 +105,8 @@ pub(crate) fn render_text_input( EditorElement::new(editor, editor_style) } + +/// This element makes all search inputs align as if they were in the same column +pub(crate) fn alignment_element() -> Div { + div().size_5().flex_none().ml_0p5() +} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 93a148c62ab6e395ccc63ac5c38204e5643b706d..06126823aba5e36b361b0514698d950a430694ea 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -125,6 +125,7 @@ pub enum ItemEvent { } // TODO: Combine this with existing HighlightedText struct? +#[derive(Debug)] pub struct BreadcrumbText { pub text: String, pub highlights: Option, HighlightStyle)>>, diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index badfe7d2437424c1ce18a1afde19507e7d6e1d3b..097bd7abe16978bf1b7b5d99b146f600ff602546 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -12,10 +12,17 @@ use crate::{ item::{Item, WeakItemHandle}, }; +#[derive(Clone, Debug)] +pub enum CollapseDirection { + Collapsed, + Expanded, +} + #[derive(Clone, Debug)] pub enum SearchEvent { MatchesInvalidated, ActiveMatchChanged, + ResultsCollapsedChanged(CollapseDirection), } #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 6e26be6dc7a09dd1ed8963579ae27d8f6cc8c50c..e4432a46dc1530572b71a376fbdac35ad43e28e1 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -6,6 +6,7 @@ use gpui::{ use ui::prelude::*; use ui::{h_flex, v_flex}; +#[derive(Copy, Clone, Debug, PartialEq)] pub enum ToolbarItemEvent { ChangeLocation(ToolbarItemLocation), } @@ -109,9 +110,10 @@ impl Render for Toolbar { v_flex() .group("toolbar") .relative() - .p(DynamicSpacing::Base08.rems(cx)) + .py(DynamicSpacing::Base06.rems(cx)) + .px(DynamicSpacing::Base08.rems(cx)) .when(has_left_items || has_right_items, |this| { - this.gap(DynamicSpacing::Base08.rems(cx)) + this.gap(DynamicSpacing::Base06.rems(cx)) }) .border_b_1() .border_color(cx.theme().colors().border_variant) @@ -119,12 +121,13 @@ impl Render for Toolbar { .when(has_left_items || has_right_items, |this| { this.child( h_flex() - .min_h_6() + .items_start() .justify_between() .gap(DynamicSpacing::Base08.rems(cx)) .when(has_left_items, |this| { this.child( h_flex() + .min_h_8() .flex_auto() .justify_start() .overflow_x_hidden() @@ -134,17 +137,9 @@ impl Render for Toolbar { .when(has_right_items, |this| { this.child( h_flex() - .h_full() + .h_8() .flex_row_reverse() - .map(|el| { - if has_left_items { - // We're using `flex_none` here to prevent some flickering that can occur when the - // size of the left items container changes. - el.flex_none() - } else { - el.flex_auto() - } - }) + .when(has_left_items, |this| this.flex_none()) .justify_end() .children(self.right_items().map(|item| item.to_any())), )