diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 00c1c0939bbfaf18ea6d0550633f2ae05e16ef25..ba91b522b76d8a33f18f18c52930e9335d0a6164 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -5,12 +5,12 @@ use gpui::{ }; use itertools::Itertools; use settings::Settings; -use std::cmp; +use std::{any::Any, cmp}; use theme::ActiveTheme; use ui::{ButtonLike, ButtonStyle, Label, Tooltip, prelude::*}; use workspace::{ TabBarSettings, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, - item::{BreadcrumbText, ItemEvent, ItemHandle}, + item::{BreadcrumbText, ItemBufferKind, ItemEvent, ItemHandle}, }; pub struct Breadcrumbs { @@ -51,6 +51,7 @@ impl Render for Breadcrumbs { return element; }; + // Begin - logic we should copy/move let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else { return element; }; @@ -151,6 +152,7 @@ impl Render for Breadcrumbs { .pl_1() .child(breadcrumbs), } + // End } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3a6fc630e650ecfbd6f95cf0df30ac9f0228f050..5867f3b2d6f8f20a3579592cc72c457eb49c16c6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -207,7 +207,7 @@ 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, }; @@ -1216,6 +1216,7 @@ pub struct Editor { accent_data: Option, fetched_tree_sitter_chunks: HashMap>>, use_base_text_line_numbers: bool, + is_multibuffer_collapsed: bool, } #[derive(Debug, PartialEq)] @@ -2417,6 +2418,7 @@ impl Editor { accent_data: None, fetched_tree_sitter_chunks: HashMap::default(), use_base_text_line_numbers: false, + is_multibuffer_collapsed: false, }; if is_minimap { @@ -23248,6 +23250,54 @@ impl Editor { .map(|vim_mode| vim_mode.0) .unwrap_or(false) } + + 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 efb0459b15b7b7e19a485a81753d39d7dd20b5de..711888820e52ae030dcbae661e056da3e4d1c4d9 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -50,9 +50,9 @@ use gpui::{ KeybindingKeystroke, Length, Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, - Size, StatefulInteractiveElement, Style, Styled, 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, 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}; @@ -98,8 +98,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, }; @@ -3956,6 +3957,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 weak_editor = self.editor.downgrade(); + + let breadcrumbs = if is_selected { + editor.breadcrumbs_inner(cx.theme(), cx) + } else { + None + }; let file_status = multi_buffer .all_diff_hunks_expanded() @@ -4160,6 +4168,15 @@ impl EditorElement { }, )) }) + .when_some(breadcrumbs, |then, breadcrumbs| { + then.child(self.render_breadcrumb_text( + breadcrumbs, + None, // TODO gotta figure this out somehow + weak_editor, + window, + cx, + )) + }) })) .when( can_open_excerpts && is_selected && relative_path.is_some(), @@ -4313,6 +4330,108 @@ impl EditorElement { }) } + fn render_breadcrumb_text( + &self, + mut segments: Vec, + prefix: Option, + editor: WeakEntity, + window: &mut Window, + cx: &App, + ) -> impl IntoElement { + const MAX_SEGMENTS: usize = 12; + + let element = h_flex() + .id("breadcrumb-container") + .flex_grow() + .overflow_x_scroll() + .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); + + // TODO this shouldn't apply here, but will in the formal breadcrumb (e.g. singleton buffer). Need to resolve the difference. + // 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 breadcrumbs = if let Some(prefix) = prefix { + h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack) + } else { + breadcrumbs_stack + }; + 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, + ) + } + }), + ) + } + fn render_blocks( &self, rows: Range, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index cfbb7c975c844f08d76a5568f1e02dfe3d7d74f1..51613260dfdc52b61bfd36caa1d4880f40a5ec35 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,7 +1,7 @@ use crate::{ Anchor, Autoscroll, BufferSerialization, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget, MultiBuffer, MultiBufferSnapshot, NavigationData, - ReportEditorEvent, SearchWithinRange, SelectionEffects, ToPoint as _, + ReportEditorEvent, SearchWithinRange, SelectionEffects, ToPoint as _, ToggleFoldAll, display_map::HighlightKey, editor_settings::SeedQuerySetting, persistence::{DB, SerializedEditor}, @@ -39,7 +39,7 @@ use std::{ }; use text::{BufferId, BufferSnapshot, Selection}; use theme::{Theme, ThemeSettings}; -use ui::{IconDecorationKind, prelude::*}; +use ui::{IconButtonShape, IconDecorationKind, Tooltip, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, @@ -940,7 +940,50 @@ impl Item for Editor { self.pixel_position_of_newest_cursor } - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { + fn breadcrumb_prefix( + &self, + _window: &mut Window, + cx: &mut Context, + ) -> Option { + if self.buffer().read(cx).is_singleton() { + return None; + } + + let is_collapsed = self.is_multibuffer_collapsed; + + let (icon, label, tooltip_label) = if is_collapsed { + ( + IconName::ChevronUpDown, + "Expand All", + "Expand All Search Results", + ) + } else { + ( + IconName::ChevronDownUp, + "Collapse All", + "Collapse All Search Results", + ) + }; + + let focus_handle = self.focus_handle.clone(); + + Some( + Button::new("multibuffer-collapse-expand", label) + .icon(icon) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in(tooltip_label, &ToggleFoldAll, &focus_handle, cx) + }) + .on_click(cx.listener(|this, _, window, cx| { + this.is_multibuffer_collapsed = !this.is_multibuffer_collapsed; + this.toggle_fold_all(&ToggleFoldAll, window, cx); + })) + .into_any_element(), + ) + } + + fn breadcrumb_location(&self, _cx: &App) -> ToolbarItemLocation { if self.show_breadcrumbs { ToolbarItemLocation::PrimaryLeft } else { @@ -948,48 +991,14 @@ impl Item for Editor { } } + // In a non-singleton case, the breadcrumbs are actually shown on sticky file headers of the multibuffer. + // In those cases, the toolbar breadcrumbs should be an empty vector. 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 { + Some(vec![]) + } } fn added_to_workspace( diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 4d7a27354b1b4b6e972579e73c48bcd4c2448a5c..ceb57bc6d6212c4bf08109cb1a80d71cad0542cd 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -9,7 +9,7 @@ use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus}; use collections::{HashMap, HashSet}; use editor::{ Addon, Editor, EditorEvent, SelectionEffects, SplittableEditor, - actions::{GoToHunk, GoToPreviousHunk}, + actions::{GoToHunk, GoToPreviousHunk, ToggleFoldAll}, multibuffer_context_lines, scroll::Autoscroll, }; @@ -70,6 +70,7 @@ pub struct ProjectDiff { workspace: WeakEntity, focus_handle: FocusHandle, pending_scroll: Option, + is_collapsed: bool, _task: Task>, _subscription: Subscription, } @@ -329,6 +330,7 @@ impl ProjectDiff { focus_handle, editor, multibuffer, + is_collapsed: false, buffer_diff_subscriptions: Default::default(), pending_scroll: None, _task: task, @@ -910,6 +912,49 @@ impl Item for ProjectDiff { } } + fn breadcrumb_prefix( + &self, + _window: &mut Window, + cx: &mut Context, + ) -> Option { + let is_collapsed = self.is_collapsed; + + let (icon, label, tooltip_label) = if is_collapsed { + ( + IconName::ChevronUpDown, + "Expand All", + "Expand All Search Results", + ) + } else { + ( + IconName::ChevronDownUp, + "Collapse All", + "Collapse All Search Results", + ) + }; + + let focus_handle = self.editor.focus_handle(cx); + + Some( + Button::new("multibuffer-collapse-expand", label) + .icon(icon) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in(tooltip_label, &ToggleFoldAll, &focus_handle, cx) + }) + .on_click(cx.listener(|this, _, window, cx| { + this.is_collapsed = !this.is_collapsed; + this.editor.update(cx, |splittable, cx| { + splittable.last_selected_editor().update(cx, |editor, cx| { + editor.toggle_fold_all(&ToggleFoldAll, window, cx); + }) + }) + })) + .into_any_element(), + ) + } + fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 278f2e86b7b13fd5a82777054c12ff2e1b6239bb..be82cbbacf23ea3063e44c4db61896406e9939a8 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -667,8 +667,9 @@ impl Item for ProjectSearchView { } } - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.results_editor.breadcrumbs(theme, cx) + fn breadcrumbs(&self, _theme: &theme::Theme, _cx: &App) -> Option> { + // This should only show the prefix - hence it cannot be none right now + Some(vec![]) } fn breadcrumb_prefix( @@ -682,17 +683,26 @@ impl Item for ProjectSearchView { let is_collapsed = self.results_collapsed; - let (icon, tooltip_label) = if is_collapsed { - (IconName::ChevronUpDown, "Expand All Search Results") + let (icon, label, tooltip_label) = if is_collapsed { + ( + IconName::ChevronUpDown, + "Expand All", + "Expand All Search Results", + ) } else { - (IconName::ChevronDownUp, "Collapse All Search Results") + ( + IconName::ChevronDownUp, + "Collapse All", + "Collapse All Search Results", + ) }; let focus_handle = self.query_editor.focus_handle(cx); Some( - IconButton::new("project-search-collapse-expand", icon) - .shape(IconButtonShape::Square) + Button::new("project-search-collapse-expand", label) + .icon(icon) + .icon_position(IconPosition::Start) .icon_size(IconSize::Small) .tooltip(move |_, cx| { Tooltip::for_action_in(