Cargo.lock 🔗
@@ -7355,6 +7355,7 @@ dependencies = [
"db",
"editor",
"file_icons",
+ "fuzzy",
"gpui",
"itertools 0.11.0",
"language",
Kirill Bulatov created
https://github.com/zed-industries/zed/assets/2690773/145a7cf2-332c-46c9-ab2f-42a77504f54f
Adds a way to filter entries in the outline panel, by showing all
entries (even if their parents were collapsed) that fuzzy match a given
query.
Release Notes:
- Added a way to filter items in the outline panel
Cargo.lock | 1
assets/keymaps/default-linux.json | 1
assets/keymaps/default-macos.json | 1
crates/language/src/outline.rs | 10
crates/outline/src/outline.rs | 13
crates/outline_panel/Cargo.toml | 1
crates/outline_panel/src/outline_panel.rs | 789 +++++++++++-------------
7 files changed, 385 insertions(+), 431 deletions(-)
@@ -7355,6 +7355,7 @@ dependencies = [
"db",
"editor",
"file_icons",
+ "fuzzy",
"gpui",
"itertools 0.11.0",
"language",
@@ -502,6 +502,7 @@
{
"context": "OutlinePanel",
"bindings": {
+ "escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"ctrl-alt-c": "outline_panel::CopyPath",
@@ -523,6 +523,7 @@
{
"context": "OutlinePanel",
"bindings": {
+ "escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"cmd-alt-c": "outline_panel::CopyPath",
@@ -5,7 +5,7 @@ use gpui::{
};
use settings::Settings;
use std::ops::Range;
-use theme::{ActiveTheme, ThemeSettings};
+use theme::{color_alpha, ActiveTheme, ThemeSettings};
/// An outline of all the symbols contained in a buffer.
#[derive(Debug)]
@@ -146,9 +146,15 @@ impl<T> Outline<T> {
pub fn render_item<T>(
outline_item: &OutlineItem<T>,
- custom_highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+ match_ranges: impl IntoIterator<Item = Range<usize>>,
cx: &AppContext,
) -> StyledText {
+ let mut highlight_style = HighlightStyle::default();
+ highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3));
+ let custom_highlights = match_ranges
+ .into_iter()
+ .map(|range| (range, highlight_style));
+
let settings = ThemeSettings::get_global(cx);
// TODO: We probably shouldn't need to build a whole new text style here
@@ -3,9 +3,8 @@ use editor::{
};
use fuzzy::StringMatch;
use gpui::{
- div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, HighlightStyle,
- ParentElement, Point, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
- WindowContext,
+ div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, ParentElement,
+ Point, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use language::Outline;
use ordered_float::OrderedFloat;
@@ -15,7 +14,7 @@ use std::{
sync::Arc,
};
-use theme::{color_alpha, ActiveTheme};
+use theme::ActiveTheme;
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{DismissDecision, ModalView};
@@ -272,10 +271,6 @@ impl PickerDelegate for OutlineViewDelegate {
let mat = self.matches.get(ix)?;
let outline_item = self.outline.items.get(mat.candidate_id)?;
- let mut highlight_style = HighlightStyle::default();
- highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3));
- let custom_highlights = mat.ranges().map(|range| (range, highlight_style));
-
Some(
ListItem::new(ix)
.inset(true)
@@ -285,7 +280,7 @@ impl PickerDelegate for OutlineViewDelegate {
div()
.text_ui(cx)
.pl(rems(outline_item.depth as f32))
- .child(language::render_item(outline_item, custom_highlights, cx)),
+ .child(language::render_item(outline_item, mat.ranges(), cx)),
),
)
}
@@ -18,6 +18,7 @@ collections.workspace = true
db.workspace = true
editor.workspace = true
file_icons.workspace = true
+fuzzy.workspace = true
itertools.workspace = true
gpui.workspace = true
language.workspace = true
@@ -4,7 +4,7 @@ use std::{
cmp,
ops::Range,
path::{Path, PathBuf},
- sync::Arc,
+ sync::{atomic::AtomicBool, Arc},
time::Duration,
};
@@ -18,6 +18,7 @@ use editor::{
DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange,
};
use file_icons::FileIcons;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
actions, anchored, deferred, div, px, uniform_list, Action, AnyElement, AppContext,
AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, EntityId,
@@ -28,7 +29,7 @@ use gpui::{
};
use itertools::Itertools;
use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
-use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
+use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrev};
use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings};
use project::{File, Fs, Item, Project};
@@ -39,8 +40,9 @@ use workspace::{
dock::{DockPosition, Panel, PanelEvent},
item::ItemHandle,
ui::{
- h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, Icon, IconName, IconSize,
- Label, LabelCommon, ListItem, Selectable, Spacing, StyledTypography,
+ h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, HighlightedLabel, Icon,
+ IconName, IconSize, Label, LabelCommon, ListItem, Selectable, Spacing, StyledExt,
+ StyledTypography,
},
OpenInTerminal, Workspace,
};
@@ -51,6 +53,7 @@ actions!(
[
ExpandSelectedEntry,
CollapseSelectedEntry,
+ ExpandAllEntries,
CollapseAllEntries,
CopyPath,
CopyRelativePath,
@@ -64,7 +67,7 @@ actions!(
);
const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
-const UPDATE_DEBOUNCE_MILLIS: u64 = 80;
+const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
type Outline = OutlineItem<language::Anchor>;
@@ -79,17 +82,38 @@ pub struct OutlinePanel {
pending_serialization: Task<Option<()>>,
fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
fs_entries: Vec<FsEntry>,
+ fs_children_count: HashMap<WorktreeId, HashMap<Arc<Path>, FsChildren>>,
collapsed_entries: HashSet<CollapsedEntry>,
unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
- last_visible_range: Range<usize>,
selected_entry: Option<EntryOwned>,
active_item: Option<ActiveItem>,
_subscriptions: Vec<Subscription>,
- loading_outlines: bool,
- update_task: Task<()>,
+ updating_fs_entries: bool,
+ fs_entries_update_task: Task<()>,
+ cached_entries_update_task: Task<()>,
outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
- cached_entries_with_depth: Option<Vec<(usize, EntryOwned)>>,
+ cached_entries_with_depth: Vec<CachedEntry>,
+ filter_editor: View<Editor>,
+}
+
+#[derive(Debug, Clone, Copy, Default)]
+struct FsChildren {
+ files: usize,
+ dirs: usize,
+}
+
+impl FsChildren {
+ fn may_be_fold_part(&self) -> bool {
+ self.dirs == 0 || (self.dirs == 1 && self.files == 0)
+ }
+}
+
+#[derive(Clone, Debug)]
+struct CachedEntry {
+ depth: usize,
+ string_match: Option<StringMatch>,
+ entry: EntryOwned,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@@ -274,6 +298,18 @@ impl OutlinePanel {
fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let project = workspace.project().clone();
let outline_panel = cx.new_view(|cx| {
+ let filter_editor = cx.new_view(|cx| {
+ let mut editor = Editor::single_line(cx);
+ editor.set_placeholder_text("Filter...", cx);
+ editor
+ });
+ let filter_update_subscription =
+ cx.subscribe(&filter_editor, |outline_panel: &mut Self, _, event, cx| {
+ if let editor::EditorEvent::BufferEdited = event {
+ outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
+ }
+ });
+
let focus_handle = cx.focus_handle();
let focus_subscription = cx.on_focus(&focus_handle, Self::focus_in);
let workspace_subscription = cx.subscribe(
@@ -298,7 +334,7 @@ impl OutlinePanel {
outline_panel.replace_visible_entries(new_active_editor, cx);
}
} else {
- outline_panel.clear_previous();
+ outline_panel.clear_previous(cx);
cx.notify();
}
}
@@ -324,8 +360,10 @@ impl OutlinePanel {
fs: workspace.app_state().fs.clone(),
scroll_handle: UniformListScrollHandle::new(),
focus_handle,
+ filter_editor,
fs_entries: Vec::new(),
fs_entries_depth: HashMap::default(),
+ fs_children_count: HashMap::default(),
collapsed_entries: HashSet::default(),
unfolded_dirs: HashMap::default(),
selected_entry: None,
@@ -333,17 +371,18 @@ impl OutlinePanel {
width: None,
active_item: None,
pending_serialization: Task::ready(None),
- loading_outlines: false,
- update_task: Task::ready(()),
+ updating_fs_entries: false,
+ fs_entries_update_task: Task::ready(()),
+ cached_entries_update_task: Task::ready(()),
outline_fetch_tasks: HashMap::default(),
excerpts: HashMap::default(),
- last_visible_range: 0..0,
- cached_entries_with_depth: None,
+ cached_entries_with_depth: Vec::new(),
_subscriptions: vec![
settings_subscription,
icons_subscription,
focus_subscription,
workspace_subscription,
+ filter_update_subscription,
],
};
if let Some(editor) = workspace
@@ -383,31 +422,16 @@ impl OutlinePanel {
}
fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
- let Some(editor) = self
- .active_item
- .as_ref()
- .and_then(|item| item.active_editor.upgrade())
- else {
- return;
- };
if let Some(EntryOwned::FoldedDirs(worktree_id, entries)) = &self.selected_entry {
self.unfolded_dirs
.entry(*worktree_id)
.or_default()
.extend(entries.iter().map(|entry| entry.id));
- self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
+ self.update_cached_entries(None, cx);
}
}
fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext<Self>) {
- let Some(editor) = self
- .active_item
- .as_ref()
- .and_then(|item| item.active_editor.upgrade())
- else {
- return;
- };
-
let (worktree_id, entry) = match &self.selected_entry {
Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, entry))) => {
(worktree_id, Some(entry))
@@ -424,38 +448,12 @@ impl OutlinePanel {
.read(cx)
.worktree_for_id(*worktree_id, cx)
.map(|w| w.read(cx).snapshot());
- let Some((worktree, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
+ let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
return;
};
unfolded_dirs.remove(&entry.id);
- let mut parent = entry.path.parent();
- while let Some(parent_path) = parent {
- let removed = worktree.entry_for_path(parent_path).map_or(false, |entry| {
- if worktree.root_entry().map(|entry| entry.id) == Some(entry.id) {
- false
- } else {
- unfolded_dirs.remove(&entry.id)
- }
- });
-
- if removed {
- parent = parent_path.parent();
- } else {
- break;
- }
- }
- for child_dir in worktree
- .child_entries(&entry.path)
- .filter(|entry| entry.is_dir())
- {
- let removed = unfolded_dirs.remove(&child_dir.id);
- if !removed {
- break;
- }
- }
-
- self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
+ self.update_cached_entries(None, cx);
}
fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
@@ -464,6 +462,23 @@ impl OutlinePanel {
}
}
+ fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+ if self.filter_editor.focus_handle(cx).is_focused(cx) {
+ self.filter_editor.update(cx, |editor, cx| {
+ if editor.buffer().read(cx).len(cx) > 0 {
+ editor.set_text("", cx);
+ }
+ });
+ } else {
+ cx.focus_view(&self.filter_editor);
+ }
+
+ if self.context_menu.is_some() {
+ self.context_menu.take();
+ cx.notify();
+ }
+ }
+
fn open_entry(&mut self, entry: &EntryOwned, cx: &mut ViewContext<OutlinePanel>) {
let Some(active_editor) = self
.active_item
@@ -578,9 +593,9 @@ impl OutlinePanel {
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
- self.entries_with_depths(cx)
+ self.cached_entries_with_depth
.iter()
- .map(|(_, entry)| entry)
+ .map(|cached_entry| &cached_entry.entry)
.skip_while(|entry| entry != &&selected_entry)
.skip(1)
.next()
@@ -596,10 +611,10 @@ impl OutlinePanel {
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
- self.entries_with_depths(cx)
+ self.cached_entries_with_depth
.iter()
.rev()
- .map(|(_, entry)| entry)
+ .map(|cached_entry| &cached_entry.entry)
.skip_while(|entry| entry != &&selected_entry)
.skip(1)
.next()
@@ -616,10 +631,10 @@ impl OutlinePanel {
fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext<Self>) {
if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
let mut previous_entries = self
- .entries_with_depths(cx)
+ .cached_entries_with_depth
.iter()
.rev()
- .map(|(_, entry)| entry)
+ .map(|cached_entry| &cached_entry.entry)
.skip_while(|entry| entry != &&selected_entry)
.skip(1);
match &selected_entry {
@@ -697,8 +712,8 @@ impl OutlinePanel {
}
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
- if let Some((_, first_entry)) = self.entries_with_depths(cx).iter().next() {
- self.selected_entry = Some(first_entry.clone());
+ if let Some(first_entry) = self.cached_entries_with_depth.iter().next() {
+ self.selected_entry = Some(first_entry.entry.clone());
self.autoscroll(cx);
cx.notify();
}
@@ -706,10 +721,10 @@ impl OutlinePanel {
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
if let Some(new_selection) = self
- .entries_with_depths(cx)
+ .cached_entries_with_depth
.iter()
.rev()
- .map(|(_, entry)| entry)
+ .map(|cached_entry| &cached_entry.entry)
.next()
{
self.selected_entry = Some(new_selection.clone());
@@ -721,9 +736,9 @@ impl OutlinePanel {
fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
if let Some(selected_entry) = self.selected_entry.clone() {
let index = self
- .entries_with_depths(cx)
+ .cached_entries_with_depth
.iter()
- .position(|(_, entry)| entry == &selected_entry);
+ .position(|cached_entry| cached_entry.entry == selected_entry);
if let Some(index) = index {
self.scroll_handle.scroll_to_item(index);
cx.notify();
@@ -811,9 +826,6 @@ impl OutlinePanel {
EntryRef::Entry(FsEntry::Directory(directory_worktree, directory_entry)) => {
(*directory_worktree, Some(directory_entry))
}
- EntryRef::FoldedDirs(directory_worktree, entries) => {
- (directory_worktree, entries.last())
- }
_ => return false,
};
let Some(directory_entry) = directory_entry else {
@@ -823,45 +835,24 @@ impl OutlinePanel {
if self
.unfolded_dirs
.get(&directory_worktree)
- .map_or(false, |unfolded_dirs| {
- unfolded_dirs.contains(&directory_entry.id)
+ .map_or(true, |unfolded_dirs| {
+ !unfolded_dirs.contains(&directory_entry.id)
})
{
- return true;
+ return false;
}
- let child_entries = self
- .fs_entries
- .iter()
- .skip_while(|entry| {
- if let FsEntry::Directory(worktree_id, entry) = entry {
- worktree_id != &directory_worktree || entry.id != directory_entry.id
- } else {
- true
- }
- })
- .skip(1)
- .filter(|next_entry| match next_entry {
- FsEntry::ExternalFile(..) => false,
- FsEntry::Directory(worktree_id, entry) | FsEntry::File(worktree_id, entry, ..) => {
- worktree_id == &directory_worktree
- && entry.path.parent() == Some(directory_entry.path.as_ref())
- }
- })
- .collect::<Vec<_>>();
+ let children = self
+ .fs_children_count
+ .get(&directory_worktree)
+ .and_then(|entries| entries.get(&directory_entry.path))
+ .copied()
+ .unwrap_or_default();
- child_entries.len() == 1 && matches!(child_entries.first(), Some(FsEntry::Directory(..)))
+ children.may_be_fold_part() && children.dirs > 0
}
fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
- let Some(editor) = self
- .active_item
- .as_ref()
- .and_then(|item| item.active_editor.upgrade())
- else {
- return;
- };
-
let entry_to_expand = match &self.selected_entry {
Some(EntryOwned::FoldedDirs(worktree_id, dir_entries)) => dir_entries
.last()
@@ -890,58 +881,33 @@ impl OutlinePanel {
project.expand_entry(worktree_id, dir_entry_id, cx);
});
}
- self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
+ self.update_cached_entries(None, cx);
} else {
self.select_next(&SelectNext, cx)
}
}
fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
- let Some(editor) = self
- .active_item
- .as_ref()
- .and_then(|item| item.active_editor.upgrade())
- else {
- return;
- };
match &self.selected_entry {
Some(
dir_entry @ EntryOwned::Entry(FsEntry::Directory(worktree_id, selected_dir_entry)),
) => {
self.collapsed_entries
.insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id));
- self.update_fs_entries(
- &editor,
- HashSet::default(),
- Some(dir_entry.clone()),
- None,
- false,
- cx,
- );
+ self.selected_entry = Some(dir_entry.clone());
+ self.update_cached_entries(None, cx);
}
Some(file_entry @ EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => {
self.collapsed_entries
.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
- self.update_fs_entries(
- &editor,
- HashSet::default(),
- Some(file_entry.clone()),
- None,
- false,
- cx,
- );
+ self.selected_entry = Some(file_entry.clone());
+ self.update_cached_entries(None, cx);
}
Some(file_entry @ EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => {
self.collapsed_entries
.insert(CollapsedEntry::ExternalFile(*buffer_id));
- self.update_fs_entries(
- &editor,
- HashSet::default(),
- Some(file_entry.clone()),
- None,
- false,
- cx,
- );
+ self.selected_entry = Some(file_entry.clone());
+ self.update_cached_entries(None, cx);
}
Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => {
if let Some(dir_entry) = dir_entries.last() {
@@ -949,14 +915,8 @@ impl OutlinePanel {
.collapsed_entries
.insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
{
- self.update_fs_entries(
- &editor,
- HashSet::default(),
- Some(dirs_entry.clone()),
- None,
- false,
- cx,
- );
+ self.selected_entry = Some(dirs_entry.clone());
+ self.update_cached_entries(None, cx);
}
}
}
@@ -965,33 +925,56 @@ impl OutlinePanel {
.collapsed_entries
.insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
{
- self.update_fs_entries(
- &editor,
- HashSet::default(),
- Some(excerpt_entry.clone()),
- None,
- false,
- cx,
- );
+ self.selected_entry = Some(excerpt_entry.clone());
+ self.update_cached_entries(None, cx);
}
}
None | Some(EntryOwned::Outline(..)) => {}
}
}
- pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
- let Some(editor) = self
- .active_item
- .as_ref()
- .and_then(|item| item.active_editor.upgrade())
- else {
- return;
- };
+ pub fn expand_all_entries(&mut self, _: &ExpandAllEntries, cx: &mut ViewContext<Self>) {
+ let expanded_entries =
+ self.fs_entries
+ .iter()
+ .fold(HashSet::default(), |mut entries, fs_entry| {
+ match fs_entry {
+ FsEntry::ExternalFile(buffer_id, _) => {
+ entries.insert(CollapsedEntry::ExternalFile(*buffer_id));
+ entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
+ |excerpts| {
+ excerpts.iter().map(|(excerpt_id, _)| {
+ CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
+ })
+ },
+ ));
+ }
+ FsEntry::Directory(worktree_id, entry) => {
+ entries.insert(CollapsedEntry::Dir(*worktree_id, entry.id));
+ }
+ FsEntry::File(worktree_id, _, buffer_id, _) => {
+ entries.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
+ entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
+ |excerpts| {
+ excerpts.iter().map(|(excerpt_id, _)| {
+ CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
+ })
+ },
+ ));
+ }
+ }
+ entries
+ });
+ self.collapsed_entries
+ .retain(|entry| !expanded_entries.contains(entry));
+ self.update_cached_entries(None, cx);
+ }
+ pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
let new_entries = self
- .entries_with_depths(cx)
+ .cached_entries_with_depth
.iter()
- .flat_map(|(_, entry)| match entry {
+ .flat_map(|cached_entry| match &cached_entry.entry {
EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => {
Some(CollapsedEntry::Dir(*worktree_id, entry.id))
}
@@ -1011,18 +994,10 @@ impl OutlinePanel {
})
.collect::<Vec<_>>();
self.collapsed_entries.extend(new_entries);
- self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
+ self.update_cached_entries(None, cx);
}
fn toggle_expanded(&mut self, entry: &EntryOwned, cx: &mut ViewContext<Self>) {
- let Some(editor) = self
- .active_item
- .as_ref()
- .and_then(|item| item.active_editor.upgrade())
- else {
- return;
- };
-
match entry {
EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry)) => {
let entry_id = dir_entry.id;
@@ -1074,14 +1049,8 @@ impl OutlinePanel {
EntryOwned::Outline(..) => return,
}
- self.update_fs_entries(
- &editor,
- HashSet::default(),
- Some(entry.clone()),
- None,
- false,
- cx,
- );
+ self.selected_entry = Some(entry.clone());
+ self.update_cached_entries(None, cx);
}
fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
@@ -1101,9 +1070,7 @@ impl OutlinePanel {
.as_ref()
.and_then(|entry| match entry {
EntryOwned::Entry(entry) => self.relative_path(&entry, cx),
- EntryOwned::FoldedDirs(_, dirs) => {
- dirs.last().map(|entry| entry.path.to_path_buf())
- }
+ EntryOwned::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.clone()),
EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
})
.map(|p| p.to_string_lossy().to_string())
@@ -1233,14 +1200,8 @@ impl OutlinePanel {
}
}
- self.update_fs_entries(
- &editor,
- HashSet::default(),
- Some(entry_with_selection),
- None,
- false,
- cx,
- );
+ self.selected_entry = Some(entry_with_selection);
+ self.update_cached_entries(None, cx);
}
fn render_excerpt(
@@ -1307,6 +1268,7 @@ impl OutlinePanel {
excerpt_id: ExcerptId,
rendered_outline: &Outline,
depth: usize,
+ string_match: Option<&StringMatch>,
cx: &mut ViewContext<Self>,
) -> Stateful<Div> {
let (item_id, label_element) = (
@@ -1314,7 +1276,14 @@ impl OutlinePanel {
"{buffer_id:?}|{excerpt_id:?}{:?}|{:?}",
rendered_outline.range, &rendered_outline.text,
))),
- language::render_item(&rendered_outline, None, cx).into_any_element(),
+ language::render_item(
+ &rendered_outline,
+ string_match
+ .map(|string_match| string_match.ranges().collect::<Vec<_>>())
+ .unwrap_or_default(),
+ cx,
+ )
+ .into_any_element(),
);
let is_active = match &self.selected_entry {
Some(EntryOwned::Outline(selected_buffer_id, selected_excerpt_id, selected_entry)) => {
@@ -1344,6 +1313,7 @@ impl OutlinePanel {
&self,
rendered_entry: &FsEntry,
depth: usize,
+ string_match: Option<&StringMatch>,
cx: &mut ViewContext<Self>,
) -> Stateful<Div> {
let settings = OutlinePanelSettings::get_global(cx);
@@ -1364,10 +1334,14 @@ impl OutlinePanel {
};
(
ElementId::from(entry.id.to_proto() as usize),
- Label::new(name)
- .single_line()
- .color(color)
- .into_any_element(),
+ HighlightedLabel::new(
+ name,
+ string_match
+ .map(|string_match| string_match.positions.clone())
+ .unwrap_or_default(),
+ )
+ .color(color)
+ .into_any_element(),
icon.unwrap_or_else(empty_icon),
)
}
@@ -1388,10 +1362,14 @@ impl OutlinePanel {
.map(|icon| icon.color(color).into_any_element());
(
ElementId::from(entry.id.to_proto() as usize),
- Label::new(name)
- .single_line()
- .color(color)
- .into_any_element(),
+ HighlightedLabel::new(
+ name,
+ string_match
+ .map(|string_match| string_match.positions.clone())
+ .unwrap_or_default(),
+ )
+ .color(color)
+ .into_any_element(),
icon.unwrap_or_else(empty_icon),
)
}
@@ -1416,10 +1394,14 @@ impl OutlinePanel {
};
(
ElementId::from(buffer_id.to_proto() as usize),
- Label::new(name)
- .single_line()
- .color(color)
- .into_any_element(),
+ HighlightedLabel::new(
+ name,
+ string_match
+ .map(|string_match| string_match.positions.clone())
+ .unwrap_or_default(),
+ )
+ .color(color)
+ .into_any_element(),
icon.unwrap_or_else(empty_icon),
)
}
@@ -1441,6 +1423,7 @@ impl OutlinePanel {
worktree_id: WorktreeId,
dir_entries: &[Entry],
depth: usize,
+ string_match: Option<&StringMatch>,
cx: &mut ViewContext<OutlinePanel>,
) -> Stateful<Div> {
let settings = OutlinePanelSettings::get_global(cx);
@@ -1451,13 +1434,7 @@ impl OutlinePanel {
_ => false,
};
let (item_id, label_element, icon) = {
- let name = dir_entries.iter().fold(String::new(), |mut name, entry| {
- if !name.is_empty() {
- name.push(std::path::MAIN_SEPARATOR)
- }
- name.push_str(&self.entry_name(&worktree_id, entry, cx));
- name
- });
+ let name = self.dir_names_string(dir_entries, worktree_id, cx);
let is_expanded = dir_entries.iter().all(|dir| {
!self
@@ -1481,10 +1458,14 @@ impl OutlinePanel {
.map(|entry| entry.id.to_proto())
.unwrap_or_else(|| worktree_id.to_proto()) as usize,
),
- Label::new(name)
- .single_line()
- .color(color)
- .into_any_element(),
+ HighlightedLabel::new(
+ name,
+ string_match
+ .map(|string_match| string_match.positions.clone())
+ .unwrap_or_default(),
+ )
+ .color(color)
+ .into_any_element(),
icon.unwrap_or_else(empty_icon),
)
};
@@ -1563,12 +1544,7 @@ impl OutlinePanel {
})
}
- fn entry_name(
- &self,
- worktree_id: &WorktreeId,
- entry: &Entry,
- cx: &ViewContext<OutlinePanel>,
- ) -> String {
+ fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &AppContext) -> String {
let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
Some(worktree) => {
let worktree = worktree.read(cx);
@@ -1600,7 +1576,6 @@ impl OutlinePanel {
new_entries: HashSet<ExcerptId>,
new_selected_entry: Option<EntryOwned>,
debounce: Option<Duration>,
- prefetch: bool,
cx: &mut ViewContext<Self>,
) {
if !self.active {
@@ -1658,237 +1633,211 @@ impl OutlinePanel {
},
);
- self.loading_outlines = true;
- self.update_task = cx.spawn(|outline_panel, mut cx| async move {
+ self.updating_fs_entries = true;
+ self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
if let Some(debounce) = debounce {
cx.background_executor().timer(debounce).await;
}
- let Some((new_collapsed_entries, new_unfolded_dirs, new_fs_entries, new_depth_map)) =
- cx.background_executor()
- .spawn(async move {
- let mut processed_external_buffers = HashSet::default();
- let mut new_worktree_entries =
- HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
- let mut worktree_excerpts = HashMap::<
- WorktreeId,
- HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
- >::default();
- let mut external_excerpts = HashMap::default();
-
- for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
- if is_new {
- match &worktree {
- Some(worktree) => {
- new_collapsed_entries
- .insert(CollapsedEntry::File(worktree.id(), buffer_id));
- }
- None => {
- new_collapsed_entries
- .insert(CollapsedEntry::ExternalFile(buffer_id));
- }
+ let Some((
+ new_collapsed_entries,
+ new_unfolded_dirs,
+ new_fs_entries,
+ new_depth_map,
+ new_children_count,
+ )) = cx
+ .background_executor()
+ .spawn(async move {
+ let mut processed_external_buffers = HashSet::default();
+ let mut new_worktree_entries =
+ HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
+ let mut worktree_excerpts = HashMap::<
+ WorktreeId,
+ HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
+ >::default();
+ let mut external_excerpts = HashMap::default();
+
+ for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
+ if is_new {
+ match &worktree {
+ Some(worktree) => {
+ new_collapsed_entries
+ .insert(CollapsedEntry::File(worktree.id(), buffer_id));
}
-
- for excerpt_id in &excerpts {
+ None => {
new_collapsed_entries
- .insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
+ .insert(CollapsedEntry::ExternalFile(buffer_id));
}
}
- if let Some(worktree) = worktree {
- let worktree_id = worktree.id();
- let unfolded_dirs =
- new_unfolded_dirs.entry(worktree_id).or_default();
-
- match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
- Some(entry) => {
- let mut traversal = worktree.traverse_from_path(
- true,
- true,
- true,
- entry.path.as_ref(),
- );
-
- let mut entries_to_add = HashSet::default();
- worktree_excerpts
- .entry(worktree_id)
- .or_default()
- .insert(entry.id, (buffer_id, excerpts));
- let mut current_entry = entry;
- loop {
- if current_entry.is_dir() {
- let is_root =
- worktree.root_entry().map(|entry| entry.id)
- == Some(current_entry.id);
- if is_root {
- root_entries.insert(current_entry.id);
- if auto_fold_dirs {
- unfolded_dirs.insert(current_entry.id);
- }
- }
+ for excerpt_id in &excerpts {
+ new_collapsed_entries
+ .insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
+ }
+ }
- if is_new {
- new_collapsed_entries.remove(
- &CollapsedEntry::Dir(
- worktree_id,
- current_entry.id,
- ),
- );
- } else if new_collapsed_entries.contains(
- &CollapsedEntry::Dir(
- worktree_id,
- current_entry.id,
- ),
- ) {
- entries_to_add.clear();
+ if let Some(worktree) = worktree {
+ let worktree_id = worktree.id();
+ let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
+
+ match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
+ Some(entry) => {
+ let mut traversal = worktree.traverse_from_path(
+ true,
+ true,
+ true,
+ entry.path.as_ref(),
+ );
+
+ let mut entries_to_add = HashSet::default();
+ worktree_excerpts
+ .entry(worktree_id)
+ .or_default()
+ .insert(entry.id, (buffer_id, excerpts));
+ let mut current_entry = entry;
+ loop {
+ if current_entry.is_dir() {
+ let is_root =
+ worktree.root_entry().map(|entry| entry.id)
+ == Some(current_entry.id);
+ if is_root {
+ root_entries.insert(current_entry.id);
+ if auto_fold_dirs {
+ unfolded_dirs.insert(current_entry.id);
}
}
+ if is_new {
+ new_collapsed_entries.remove(&CollapsedEntry::Dir(
+ worktree_id,
+ current_entry.id,
+ ));
+ }
+ }
- let new_entry_added =
- entries_to_add.insert(current_entry);
- if new_entry_added && traversal.back_to_parent() {
- if let Some(parent_entry) = traversal.entry() {
- current_entry = parent_entry.clone();
- continue;
- }
+ let new_entry_added = entries_to_add.insert(current_entry);
+ if new_entry_added && traversal.back_to_parent() {
+ if let Some(parent_entry) = traversal.entry() {
+ current_entry = parent_entry.clone();
+ continue;
}
- break;
}
- new_worktree_entries
- .entry(worktree_id)
- .or_insert_with(|| {
- (worktree.clone(), HashSet::default())
- })
- .1
- .extend(entries_to_add);
+ break;
}
- None => {
- if processed_external_buffers.insert(buffer_id) {
- external_excerpts
- .entry(buffer_id)
- .or_insert_with(|| Vec::new())
- .extend(excerpts);
- }
+ new_worktree_entries
+ .entry(worktree_id)
+ .or_insert_with(|| (worktree.clone(), HashSet::default()))
+ .1
+ .extend(entries_to_add);
+ }
+ None => {
+ if processed_external_buffers.insert(buffer_id) {
+ external_excerpts
+ .entry(buffer_id)
+ .or_insert_with(|| Vec::new())
+ .extend(excerpts);
}
}
- } else if processed_external_buffers.insert(buffer_id) {
- external_excerpts
- .entry(buffer_id)
- .or_insert_with(|| Vec::new())
- .extend(excerpts);
}
+ } else if processed_external_buffers.insert(buffer_id) {
+ external_excerpts
+ .entry(buffer_id)
+ .or_insert_with(|| Vec::new())
+ .extend(excerpts);
}
+ }
- #[derive(Clone, Copy, Default)]
- struct Children {
- files: usize,
- dirs: usize,
- }
- let mut children_count =
- HashMap::<WorktreeId, HashMap<PathBuf, Children>>::default();
-
- let worktree_entries = new_worktree_entries
- .into_iter()
- .map(|(worktree_id, (worktree_snapshot, entries))| {
- let mut entries = entries.into_iter().collect::<Vec<_>>();
- // For a proper git status propagation, we have to keep the entries sorted lexicographically.
- entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
- worktree_snapshot.propagate_git_statuses(&mut entries);
- project::sort_worktree_entries(&mut entries);
- (worktree_id, entries)
- })
- .flat_map(|(worktree_id, entries)| {
- {
- entries
- .into_iter()
- .filter_map(|entry| {
- if auto_fold_dirs {
- if let Some(parent) = entry.path.parent() {
- let children = children_count
- .entry(worktree_id)
- .or_default()
- .entry(parent.to_path_buf())
- .or_default();
- if entry.is_dir() {
- children.dirs += 1;
- } else {
- children.files += 1;
- }
+ let mut new_children_count =
+ HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
+
+ let worktree_entries = new_worktree_entries
+ .into_iter()
+ .map(|(worktree_id, (worktree_snapshot, entries))| {
+ let mut entries = entries.into_iter().collect::<Vec<_>>();
+ // For a proper git status propagation, we have to keep the entries sorted lexicographically.
+ entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
+ worktree_snapshot.propagate_git_statuses(&mut entries);
+ project::sort_worktree_entries(&mut entries);
+ (worktree_id, entries)
+ })
+ .flat_map(|(worktree_id, entries)| {
+ {
+ entries
+ .into_iter()
+ .filter_map(|entry| {
+ if auto_fold_dirs {
+ if let Some(parent) = entry.path.parent() {
+ let children = new_children_count
+ .entry(worktree_id)
+ .or_default()
+ .entry(Arc::from(parent))
+ .or_default();
+ if entry.is_dir() {
+ children.dirs += 1;
+ } else {
+ children.files += 1;
}
}
+ }
- if entry.is_dir() {
- Some(FsEntry::Directory(worktree_id, entry))
- } else {
- let (buffer_id, excerpts) = worktree_excerpts
- .get_mut(&worktree_id)
- .and_then(|worktree_excerpts| {
- worktree_excerpts.remove(&entry.id)
- })?;
- Some(FsEntry::File(
- worktree_id,
- entry,
- buffer_id,
- excerpts,
- ))
- }
- })
- .collect::<Vec<_>>()
- }
- })
- .collect::<Vec<_>>();
-
- let mut visited_dirs = Vec::new();
- let mut new_depth_map = HashMap::default();
- let new_visible_entries = external_excerpts
- .into_iter()
- .sorted_by_key(|(id, _)| *id)
- .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
- .chain(worktree_entries)
- .filter(|visible_item| {
- match visible_item {
- FsEntry::Directory(worktree_id, dir_entry) => {
- let parent_id = back_to_common_visited_parent(
- &mut visited_dirs,
- worktree_id,
- dir_entry,
- );
-
- visited_dirs.push((dir_entry.id, dir_entry.path.clone()));
- let depth = if root_entries.contains(&dir_entry.id) {
- 0
- } else if auto_fold_dirs {
- let (parent_folded, parent_depth) = match parent_id {
- Some((worktree_id, id)) => (
- new_unfolded_dirs.get(&worktree_id).map_or(
- true,
- |unfolded_dirs| {
- !unfolded_dirs.contains(&id)
- },
- ),
- new_depth_map
- .get(&(worktree_id, id))
- .copied()
- .unwrap_or(0),
- ),
-
- None => (false, 0),
- };
-
- let children = children_count
+ if entry.is_dir() {
+ Some(FsEntry::Directory(worktree_id, entry))
+ } else {
+ let (buffer_id, excerpts) = worktree_excerpts
+ .get_mut(&worktree_id)
+ .and_then(|worktree_excerpts| {
+ worktree_excerpts.remove(&entry.id)
+ })?;
+ Some(FsEntry::File(
+ worktree_id,
+ entry,
+ buffer_id,
+ excerpts,
+ ))
+ }
+ })
+ .collect::<Vec<_>>()
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let mut visited_dirs = Vec::new();
+ let mut new_depth_map = HashMap::default();
+ let new_visible_entries = external_excerpts
+ .into_iter()
+ .sorted_by_key(|(id, _)| *id)
+ .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
+ .chain(worktree_entries)
+ .filter(|visible_item| {
+ match visible_item {
+ FsEntry::Directory(worktree_id, dir_entry) => {
+ let parent_id = back_to_common_visited_parent(
+ &mut visited_dirs,
+ worktree_id,
+ dir_entry,
+ );
+
+ let depth = if root_entries.contains(&dir_entry.id) {
+ 0
+ } else {
+ if auto_fold_dirs {
+ let children = new_children_count
.get(&worktree_id)
.and_then(|children_count| {
- children_count
- .get(&dir_entry.path.to_path_buf())
+ children_count.get(&dir_entry.path)
})
.copied()
.unwrap_or_default();
- let folded = if children.dirs > 1
- || (children.dirs == 1 && children.files > 0)
+
+ if !children.may_be_fold_part()
|| (children.dirs == 0
&& visited_dirs
.last()
.map(|(parent_dir_id, _)| {
- root_entries.contains(parent_dir_id)
+ new_unfolded_dirs
+ .get(&worktree_id)
+ .map_or(true, |unfolded_dirs| {
+ unfolded_dirs
+ .contains(&parent_dir_id)
+ })
})
.unwrap_or(true))
{