mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-10 00:13:29 +00:00
Multibuffer breadcrumbs toolbar redesign (#45547)
Breadcrumb visual reorchestration which makes a couple of key changes: - Breadcrumbs are now displayed in file headers for multibuffer views. Singleton buffers are unchanged. - Multibuffer views now have collapse/expand all buttons available in the toolbar - Search in multibuffer views now takes up less space, by occupying the primary toolbar location erstwhile taken up by breadcrumbs <img width="1721" height="823" alt="Screenshot 2025-12-22 at 4 15 41 PM" src="https://github.com/user-attachments/assets/38e924f7-fe9e-4c83-84fb-2eee98137f43" /> <img width="1722" height="762" alt="Screenshot 2025-12-22 at 4 15 51 PM" src="https://github.com/user-attachments/assets/ccc98ca3-75ce-4aca-bceb-890817b0f04d" /> Release Notes: - Moved breadcrumbs to file headers in multibuffer views - Added collapse/expand all options for multibuffer views - Improved use of space for multibuffer search by taking the place of the former breadcrumb toolbar location --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com> Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
This commit is contained in:
parent
9fcb36a87f
commit
216933f0b8
23 changed files with 918 additions and 514 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -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());
|
||||
.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());
|
||||
|
||||
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();
|
||||
let full_path = match parent_path.as_deref() {
|
||||
Some(parent) if !parent.is_empty() => {
|
||||
format!("{}{}", parent, filename.as_str())
|
||||
}
|
||||
_ => filename.as_str().to_string(),
|
||||
};
|
||||
|
||||
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(),
|
||||
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();
|
||||
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),
|
||||
],
|
||||
);
|
||||
|
||||
let initial_location = search_bar.update_in(cx, |search_bar, window, cx| {
|
||||
search_bar.set_active_pane_item(Some(&editor), 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();
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
assert!(
|
||||
display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
|
||||
);
|
||||
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| {
|
||||
editor.fold_all(&FoldAll, window, cx);
|
||||
});
|
||||
|
||||
let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
|
||||
|
||||
assert!(is_collapsed);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
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())),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue