Detailed changes
@@ -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
}
}
@@ -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<AccentData>,
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
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<Vec<BreadcrumbText>> {
+ 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>(
@@ -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<BreadcrumbText>,
+ prefix: Option<gpui::AnyElement>,
+ editor: WeakEntity<Editor>,
+ 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<DisplayRow>,
@@ -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<Self>,
+ ) -> Option<gpui::AnyElement> {
+ 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<Vec<BreadcrumbText>> {
- 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(
@@ -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<Workspace>,
focus_handle: FocusHandle,
pending_scroll: Option<PathKey>,
+ is_collapsed: bool,
_task: Task<Result<()>>,
_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<Self>,
+ ) -> Option<gpui::AnyElement> {
+ 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
}
@@ -667,8 +667,9 @@ impl Item for ProjectSearchView {
}
}
- fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
- self.results_editor.breadcrumbs(theme, cx)
+ fn breadcrumbs(&self, _theme: &theme::Theme, _cx: &App) -> Option<Vec<BreadcrumbText>> {
+ // 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(