Detailed changes
@@ -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",
]
@@ -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",
@@ -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",
@@ -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",
@@ -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<Vec<BreadcrumbText>> {
- self.editor.breadcrumbs(theme, cx)
- }
-
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
@@ -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"] }
@@ -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<ToolbarItemEvent> for Breadcrumbs {}
impl Render for Breadcrumbs {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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::<Editor>()
- .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<Breadcrumbs>,
-) -> Option<gpui::AnyElement> {
- 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(),
- )
-}
@@ -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"] }
@@ -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<Vec<BreadcrumbText>> {
- self.editor.breadcrumbs(theme, cx)
- }
-
fn can_save(&self, _cx: &App) -> bool {
true
}
@@ -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<Vec<BreadcrumbText>> {
- self.editor.breadcrumbs(theme, cx)
- }
-
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
@@ -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<Box<dyn DiagnosticsToolbarEditor>>,
@@ -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 {
@@ -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<Self>,
) {
- 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<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>(
@@ -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<BreadcrumbText>,
+ prefix: Option<gpui::AnyElement>,
+ 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::<Editor>()
+ .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<gpui::AnyElement> {
+ 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<FileStatus>) -> Color {
file_status.map_or(Color::Default, |status| {
if status.is_conflicted() {
@@ -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<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 {
+ None
+ }
}
fn added_to_workspace(
@@ -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<Vec<BreadcrumbText>> {
- None
- }
-
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
@@ -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<Vec<BreadcrumbText>> {
- self.editor
- .read(cx)
- .last_selected_editor()
- .read(cx)
- .breadcrumbs(theme, cx)
- }
-
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
@@ -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<Vec<BreadcrumbText>> {
- self.diff_editor.breadcrumbs(theme, cx)
- }
-
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
@@ -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<Arc<Language>>,
+ is_collapsed: bool,
}
impl EventEmitter<Event> for BufferSearchBar {}
impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
impl Render for BufferSearchBar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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>) {
+ self.toggle_fold_all_in_item(window, cx);
+ }
+
+ fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context<Self>) {
+ let is_collapsed = self.is_collapsed;
+ if let Some(item) = &self.active_searchable_item {
+ if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
+ let editor = item.downcast::<Editor>().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<Self>) {
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<Editor>,
+ Entity<BufferSearchBar>,
+ &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]
@@ -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<Vec<BreadcrumbText>> {
- self.results_editor.breadcrumbs(theme, cx)
- }
-
- fn breadcrumb_prefix(
- &self,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Option<gpui::AnyElement> {
- 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>,
) {
- 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<Self>) {
+ fn update_results_visibility(&mut self, window: &mut Window, cx: &mut Context<Self>) {
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<Self>) -> 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]
@@ -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<Editor>,
@@ -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()
+}
@@ -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<Vec<(Range<usize>, HighlightStyle)>>,
@@ -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)]
@@ -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())),
)