text_finder: Add collapsible file groups (#60193)
## Summary Add a chevron (`Disclosure`) to each file group header in Text Finder, letting you fold/unfold that file's matches. Fold state is cleared whenever a new search starts. ## Test plan - [x] Open Text Finder, run a search with matches in multiple files - [x] Click a header's chevron, confirm its matches hide/show and selection lands on a visible row - [x] Start a new search, confirm folded state doesn't carry over Release Notes: - Added the ability to collapse per-file match groups in Text Finder --------- Co-authored-by: ozacod <ozacod@users.noreply.github.com> Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
1 parent
d9ebd07d9a
commit
a378682f2f
5 files changed
+177
-25
No files matched your search
@@ -39,6 +39,9 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-cmd-f": "text_finder::ToProjectSearch",
|
||||
"alt-cmd-[": "text_finder::Fold",
|
||||
"alt-cmd-]": "text_finder::Unfold",
|
||||
"cmd-shift-enter": "text_finder::ToggleFoldAll",
|
||||
"cmd-j": "pane::SplitDown",
|
||||
"cmd-k": "pane::SplitUp",
|
||||
"cmd-h": "pane::SplitLeft",
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
"ctrl-k": "pane::SplitUp",
|
||||
"ctrl-h": "pane::SplitLeft",
|
||||
"ctrl-l": "pane::SplitRight",
|
||||
"ctrl-{": "text_finder::Fold",
|
||||
"ctrl-}": "text_finder::Unfold",
|
||||
"ctrl-shift-enter": "text_finder::ToggleFoldAll",
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -29,7 +29,7 @@ use util::ResultExt as _;
|
||||
|
||||
use crate::{ProjectSearchView, SearchOptions, text_finder::delegate::PopulateProjectSearch};
|
||||
|
||||
actions!(text_finder, [ToProjectSearch,]);
|
||||
actions!(text_finder, [ToProjectSearch, Fold, Unfold, ToggleFoldAll]);
|
||||
|
||||
pub struct TextFinder {
|
||||
picker: Entity<Picker<Delegate>>,
|
||||
@@ -259,6 +259,24 @@ impl TextFinder {
|
||||
self.open_in_split(workspace::SplitDirection::Down, window, cx);
|
||||
}
|
||||
|
||||
fn fold(&mut self, _: &Fold, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.set_selected_group_collapsed(true, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn unfold(&mut self, _: &Unfold, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.set_selected_group_collapsed(false, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_fold_all(&mut self, _: &ToggleFoldAll, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.toggle_all_collapsed(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn open_in_split(
|
||||
&mut self,
|
||||
direction: workspace::SplitDirection,
|
||||
|
||||
@@ -32,8 +32,8 @@ use editor::{MultiBufferSnapshot, PathKey, multibuffer_context_lines};
|
||||
use file_icons::FileIcons;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
AnyElement, AppContext, AsyncApp, DismissEvent, EntityId, HighlightStyle, StyledText, Task,
|
||||
TextStyle, prelude::*,
|
||||
AnyElement, AppContext, AsyncApp, ClickEvent, DismissEvent, EntityId, HighlightStyle,
|
||||
Modifiers, StyledText, Task, TextStyle, prelude::*,
|
||||
};
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use language::{Buffer, LanguageAwareStyling};
|
||||
@@ -45,15 +45,15 @@ use smol::future::yield_now;
|
||||
use text::Anchor;
|
||||
use theme_settings::ThemeSettings;
|
||||
use ui::{
|
||||
Divider, FluentBuilder, IconButtonShape, ListItem, ListItemSpacing, Toggleable, Tooltip,
|
||||
prelude::*,
|
||||
Disclosure, Divider, FluentBuilder, IconButtonShape, ListItem, ListItemSpacing, Toggleable,
|
||||
Tooltip, prelude::*, text_for_keystroke,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::SplitDirection;
|
||||
use workspace::Workspace;
|
||||
use workspace::item::ItemSettings;
|
||||
|
||||
use super::SearchMatch;
|
||||
use super::{Fold, SearchMatch, Unfold};
|
||||
use crate::project_search::{ActiveSettings, ProjectSearch};
|
||||
use crate::{ProjectSearchView, SearchOption, SearchOptions};
|
||||
|
||||
@@ -85,6 +85,7 @@ pub struct Delegate {
|
||||
/// column so every row's number right-aligns to the widest one. Recomputed in
|
||||
/// [`Delegate::rebuild_entries`].
|
||||
pub(crate) max_line_number: u32,
|
||||
pub(crate) collapsed_paths: HashSet<ProjectPath>,
|
||||
}
|
||||
|
||||
pub(crate) enum Entry {
|
||||
@@ -316,6 +317,7 @@ impl Delegate {
|
||||
in_progress_search,
|
||||
unique_files: HashSet::default(),
|
||||
max_line_number: 0,
|
||||
collapsed_paths: HashSet::default(),
|
||||
});
|
||||
|
||||
this
|
||||
@@ -368,7 +370,9 @@ impl Delegate {
|
||||
entries.push(Entry::Header(search_match.path.clone()));
|
||||
last_path = Some(&search_match.path);
|
||||
}
|
||||
entries.push(Entry::Match(match_index));
|
||||
if !self.collapsed_paths.contains(&search_match.path) {
|
||||
entries.push(Entry::Match(match_index));
|
||||
}
|
||||
}
|
||||
self.entries = entries;
|
||||
self.max_line_number = self
|
||||
@@ -394,6 +398,65 @@ impl Delegate {
|
||||
.position(|entry| matches!(entry, Entry::Match(_)))
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_group_collapsed(&mut self, path: &ProjectPath) {
|
||||
if !self.collapsed_paths.remove(path) {
|
||||
self.collapsed_paths.insert(path.clone());
|
||||
}
|
||||
self.rebuild_entries();
|
||||
}
|
||||
|
||||
pub(crate) fn set_selected_group_collapsed(
|
||||
&mut self,
|
||||
collapsed: bool,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
let path = match self.entries.get(self.selected_index) {
|
||||
Some(Entry::Match(match_index)) => self
|
||||
.matches
|
||||
.get(*match_index)
|
||||
.map(|search_match| search_match.path.clone()),
|
||||
Some(Entry::Header(path)) => Some(path.clone()),
|
||||
Some(Entry::Separator) | None => None,
|
||||
};
|
||||
let Some(path) = path else {
|
||||
return;
|
||||
};
|
||||
if collapsed == self.collapsed_paths.contains(&path) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.toggle_group_collapsed(&path);
|
||||
|
||||
if let Some(index) = self.entries.iter().position(|entry| match entry {
|
||||
Entry::Header(header_path) => collapsed && *header_path == path,
|
||||
Entry::Match(match_index) => {
|
||||
!collapsed
|
||||
&& self
|
||||
.matches
|
||||
.get(*match_index)
|
||||
.is_some_and(|search_match| search_match.path == path)
|
||||
}
|
||||
Entry::Separator => false,
|
||||
}) {
|
||||
self.selected_index = index;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_all_collapsed(&mut self, cx: &mut Context<Picker<Self>>) {
|
||||
if self.collapsed_paths.is_empty() {
|
||||
self.collapsed_paths = self
|
||||
.matches
|
||||
.iter()
|
||||
.map(|search_match| search_match.path.clone())
|
||||
.collect();
|
||||
} else {
|
||||
self.collapsed_paths.clear();
|
||||
}
|
||||
self.rebuild_entries();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn selected_search_match(&self) -> Option<&SearchMatch> {
|
||||
match self.entries.get(self.selected_index)? {
|
||||
Entry::Match(match_index) => self.matches.get(*match_index),
|
||||
@@ -626,7 +689,11 @@ impl PickerDelegate for Delegate {
|
||||
}
|
||||
|
||||
fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
|
||||
matches!(self.entries.get(ix), Some(Entry::Match(_)))
|
||||
match self.entries.get(ix) {
|
||||
Some(Entry::Match(_)) => true,
|
||||
Some(Entry::Header(path)) => self.collapsed_paths.contains(path),
|
||||
Some(Entry::Separator) | None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
@@ -674,6 +741,7 @@ impl PickerDelegate for Delegate {
|
||||
self.matches.clear();
|
||||
self.entries.clear();
|
||||
self.unique_files.clear();
|
||||
self.collapsed_paths.clear();
|
||||
self.selected_index = 0;
|
||||
self.active_query = None;
|
||||
cx.notify();
|
||||
@@ -841,27 +909,83 @@ impl PickerDelegate for Delegate {
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
});
|
||||
let is_collapsed = self.collapsed_paths.contains(path);
|
||||
let toggle_path = path.clone();
|
||||
let tooltip_focus_handle = self.focus_handle.clone();
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.py_1()
|
||||
.gap_1p5()
|
||||
.children(file_icon)
|
||||
div()
|
||||
.px_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(file_name).size(LabelSize::Small))
|
||||
.when(!directory.is_empty(), |this| {
|
||||
this.child(
|
||||
Label::new(directory)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate_start(),
|
||||
)
|
||||
}),
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.p_1()
|
||||
.gap_1p5()
|
||||
.rounded_sm()
|
||||
.when(selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Disclosure::new(
|
||||
("text-finder-fold", ix),
|
||||
!is_collapsed,
|
||||
)
|
||||
.tooltip(move |_window, cx| {
|
||||
let (label, action): (_, &dyn gpui::Action) =
|
||||
if is_collapsed {
|
||||
("Unfold", &Unfold)
|
||||
} else {
|
||||
("Fold", &Fold)
|
||||
};
|
||||
Tooltip::with_meta_in(
|
||||
label,
|
||||
Some(action),
|
||||
format!(
|
||||
"{} to toggle all",
|
||||
text_for_keystroke(
|
||||
&Modifiers::alt(),
|
||||
"click",
|
||||
cx
|
||||
)
|
||||
),
|
||||
&tooltip_focus_handle,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(
|
||||
move |this, event: &ClickEvent, _window, cx| {
|
||||
if event.modifiers().alt {
|
||||
this.delegate.toggle_all_collapsed(cx);
|
||||
} else {
|
||||
this.delegate.toggle_group_collapsed(
|
||||
&toggle_path,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.children(file_icon),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(file_name).size(LabelSize::Small))
|
||||
.when(!directory.is_empty(), |this| {
|
||||
this.child(
|
||||
Label::new(directory)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate_start(),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
@@ -958,6 +1082,7 @@ async fn stream_results_to_picker(
|
||||
delegate.matches.clear();
|
||||
delegate.entries.clear();
|
||||
delegate.unique_files.clear();
|
||||
delegate.collapsed_paths.clear();
|
||||
delegate.selected_index = 0;
|
||||
clear_existing = false;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ impl Render for TextFinder {
|
||||
.on_action(cx.listener(Self::split_right))
|
||||
.on_action(cx.listener(Self::split_up))
|
||||
.on_action(cx.listener(Self::split_down))
|
||||
.on_action(cx.listener(Self::fold))
|
||||
.on_action(cx.listener(Self::unfold))
|
||||
.on_action(cx.listener(Self::toggle_fold_all))
|
||||
.child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user