Cargo.lock 🔗
@@ -7160,8 +7160,9 @@ dependencies = [
"db",
"editor",
"file_icons",
- "git",
+ "futures 0.3.28",
"gpui",
+ "itertools 0.11.0",
"language",
"log",
"menu",
Kirill Bulatov created
Follow-up of https://github.com/zed-industries/zed/pull/12637
Adds excerpt items into the outline panel: now all outline items are
initially hidden under excerpt items that could be toggled open/closed
similar to directories.

On active editor's selection change, a corresponding outline will be
revealed still, expanding the corresponding excerpt

Release Notes:
- N/A
Cargo.lock | 3
crates/collab_ui/src/channel_view.rs | 2
crates/editor/src/editor.rs | 10
crates/editor/src/items.rs | 2
crates/language_tools/src/syntax_tree_view.rs | 2
crates/multi_buffer/src/multi_buffer.rs | 14
crates/outline_panel/Cargo.toml | 3
crates/outline_panel/src/outline_panel.rs | 738 +++++++++-----------
8 files changed, 352 insertions(+), 422 deletions(-)
@@ -7160,8 +7160,9 @@ dependencies = [
"db",
"editor",
"file_icons",
- "git",
+ "futures 0.3.28",
"gpui",
+ "itertools 0.11.0",
"language",
"log",
"menu",
@@ -228,7 +228,7 @@ impl ChannelView {
&self.editor,
move |this, _, e: &EditorEvent, cx| {
match e {
- EditorEvent::Reparsed => {
+ EditorEvent::Reparsed(_) => {
this.focus_position_from_link(position.clone(), false, cx);
this._reparse_subscription.take();
}
@@ -10888,14 +10888,14 @@ impl Editor {
multi_buffer::Event::ExcerptsExpanded { ids } => {
cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() })
}
- multi_buffer::Event::Reparsed => {
+ multi_buffer::Event::Reparsed(buffer_id) => {
self.tasks_update_task = Some(self.refresh_runnables(cx));
- cx.emit(EditorEvent::Reparsed);
+ cx.emit(EditorEvent::Reparsed(*buffer_id));
}
- multi_buffer::Event::LanguageChanged => {
+ multi_buffer::Event::LanguageChanged(buffer_id) => {
linked_editing_ranges::refresh_linked_ranges(self, cx);
- cx.emit(EditorEvent::Reparsed);
+ cx.emit(EditorEvent::Reparsed(*buffer_id));
cx.notify();
}
multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
@@ -11818,7 +11818,7 @@ pub enum EditorEvent {
Edited {
transaction_id: clock::Lamport,
},
- Reparsed,
+ Reparsed(BufferId),
Focused,
Blurred,
DirtyChanged,
@@ -903,7 +903,7 @@ impl Item for Editor {
f(ItemEvent::UpdateBreadcrumbs);
}
- EditorEvent::Reparsed => {
+ EditorEvent::Reparsed(_) => {
f(ItemEvent::UpdateBreadcrumbs);
}
@@ -110,7 +110,7 @@ impl SyntaxTreeView {
let subscription = cx.subscribe(&editor, |this, _, event, cx| {
let did_reparse = match event {
- editor::EditorEvent::Reparsed => true,
+ editor::EditorEvent::Reparsed(_) => true,
editor::EditorEvent::SelectionsChanged { .. } => false,
_ => return,
};
@@ -94,9 +94,9 @@ pub enum Event {
DiffUpdated {
buffer: Model<Buffer>,
},
- LanguageChanged,
+ LanguageChanged(BufferId),
CapabilityChanged,
- Reparsed,
+ Reparsed(BufferId),
Saved,
FileHandleChanged,
Closed,
@@ -538,9 +538,13 @@ impl MultiBuffer {
});
if let Some(buffer) = self.as_singleton() {
- return buffer.update(cx, |buffer, cx| {
+ buffer.update(cx, |buffer, cx| {
buffer.edit(edits, autoindent_mode, cx);
});
+ cx.emit(Event::ExcerptsEdited {
+ ids: self.excerpt_ids(),
+ });
+ return;
}
let original_indent_columns = match &mut autoindent_mode {
@@ -1639,8 +1643,8 @@ impl MultiBuffer {
language::Event::Reloaded => Event::Reloaded,
language::Event::DiffBaseChanged => Event::DiffBaseChanged,
language::Event::DiffUpdated => Event::DiffUpdated { buffer },
- language::Event::LanguageChanged => Event::LanguageChanged,
- language::Event::Reparsed => Event::Reparsed,
+ language::Event::LanguageChanged => Event::LanguageChanged(buffer.read(cx).remote_id()),
+ language::Event::Reparsed => Event::Reparsed(buffer.read(cx).remote_id()),
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
language::Event::Closed => Event::Closed,
language::Event::CapabilityChanged => {
@@ -18,7 +18,8 @@ collections.workspace = true
db.workspace = true
editor.workspace = true
file_icons.workspace = true
-git.workspace = true
+futures.workspace = true
+itertools.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
@@ -2,7 +2,6 @@ mod outline_panel_settings;
use std::{
cmp,
- hash::Hash,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
@@ -15,10 +14,10 @@ use db::kvp::KEY_VALUE_STORE;
use editor::{
items::{entry_git_aware_label_color, entry_label_color},
scroll::ScrollAnchor,
- Editor, EditorEvent, ExcerptId,
+ Editor, EditorEvent, ExcerptId, ExcerptRange,
};
use file_icons::FileIcons;
-use git::repository::GitFileStatus;
+use futures::{stream::FuturesUnordered, StreamExt};
use gpui::{
actions, anchored, deferred, div, px, uniform_list, Action, AppContext, AssetSource,
AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, EntityId, EventEmitter,
@@ -27,11 +26,12 @@ use gpui::{
Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView,
WindowContext,
};
-use language::{BufferId, OffsetRangeExt, OutlineItem};
+use itertools::Itertools;
+use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings};
-use project::{EntryKind, File, Fs, Project};
+use project::{File, Fs, Project};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use unicase::UniCase;
@@ -80,7 +80,7 @@ pub struct OutlinePanel {
pending_serialization: Task<Option<()>>,
fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), (bool, usize)>,
fs_entries: Vec<FsEntry>,
- collapsed_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
+ collapsed_entries: HashSet<CollapsedEntry>,
unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
last_visible_range: Range<usize>,
selected_entry: Option<EntryOwned>,
@@ -88,15 +88,57 @@ pub struct OutlinePanel {
_subscriptions: Vec<Subscription>,
update_task: Task<()>,
outline_fetch_tasks: Vec<Task<()>>,
- outlines: HashMap<OutlinesContainer, Vec<Outline>>,
+ excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
cached_entries_with_depth: Option<Vec<(usize, EntryOwned)>>,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+enum CollapsedEntry {
+ Dir(WorktreeId, ProjectEntryId),
+ Excerpt(BufferId, ExcerptId),
+}
+
+struct Excerpt {
+ range: ExcerptRange<language::Anchor>,
+ outlines: ExcerptOutlines,
+}
+
+impl Excerpt {
+ fn invalidate_outlines(&mut self) {
+ if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines {
+ self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines));
+ }
+ }
+
+ fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {
+ match &self.outlines {
+ ExcerptOutlines::Outlines(outlines) => outlines.iter(),
+ ExcerptOutlines::Invalidated(outlines) => outlines.iter(),
+ ExcerptOutlines::NotFetched => [].iter(),
+ }
+ }
+
+ fn should_fetch_outlines(&self) -> bool {
+ match &self.outlines {
+ ExcerptOutlines::Outlines(_) => false,
+ ExcerptOutlines::Invalidated(_) => true,
+ ExcerptOutlines::NotFetched => true,
+ }
+ }
+}
+
+enum ExcerptOutlines {
+ Outlines(Vec<Outline>),
+ Invalidated(Vec<Outline>),
+ NotFetched,
+}
+
#[derive(Clone, Debug, PartialEq, Eq)]
enum EntryOwned {
Entry(FsEntry),
FoldedDirs(WorktreeId, Vec<Entry>),
- Outline(OutlinesContainer, Outline),
+ Excerpt(BufferId, ExcerptId, ExcerptRange<language::Anchor>),
+ Outline(BufferId, ExcerptId, Outline),
}
impl EntryOwned {
@@ -104,7 +146,12 @@ impl EntryOwned {
match self {
Self::Entry(entry) => EntryRef::Entry(entry),
Self::FoldedDirs(worktree_id, dirs) => EntryRef::FoldedDirs(*worktree_id, dirs),
- Self::Outline(container, outline) => EntryRef::Outline(*container, outline),
+ Self::Excerpt(buffer_id, excerpt_id, range) => {
+ EntryRef::Excerpt(*buffer_id, *excerpt_id, range)
+ }
+ Self::Outline(buffer_id, excerpt_id, outline) => {
+ EntryRef::Outline(*buffer_id, *excerpt_id, outline)
+ }
}
}
@@ -117,15 +164,7 @@ impl EntryOwned {
.worktree_for_id(*worktree_id, cx)
.and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
}),
- Self::Outline(..) => None,
- }
- }
-
- fn outlines_container(&self) -> Option<OutlinesContainer> {
- match self {
- Self::Entry(entry) => entry.outlines_container(),
- Self::FoldedDirs(..) => None,
- Self::Outline(container, _) => Some(*container),
+ Self::Excerpt(..) | Self::Outline(..) => None,
}
}
}
@@ -134,7 +173,8 @@ impl EntryOwned {
enum EntryRef<'a> {
Entry(&'a FsEntry),
FoldedDirs(WorktreeId, &'a [Entry]),
- Outline(OutlinesContainer, &'a Outline),
+ Excerpt(BufferId, ExcerptId, &'a ExcerptRange<language::Anchor>),
+ Outline(BufferId, ExcerptId, &'a Outline),
}
impl EntryRef<'_> {
@@ -144,34 +184,34 @@ impl EntryRef<'_> {
&Self::FoldedDirs(worktree_id, dirs) => {
EntryOwned::FoldedDirs(worktree_id, dirs.to_vec())
}
- &Self::Outline(container, outline) => EntryOwned::Outline(container, outline.clone()),
+ &Self::Excerpt(buffer_id, excerpt_id, range) => {
+ EntryOwned::Excerpt(buffer_id, excerpt_id, range.clone())
+ }
+ &Self::Outline(buffer_id, excerpt_id, outline) => {
+ EntryOwned::Outline(buffer_id, excerpt_id, outline.clone())
+ }
}
}
}
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
-enum OutlinesContainer {
- ExternalFile(BufferId),
- File(WorktreeId, ProjectEntryId),
-}
-
#[derive(Clone, Debug, Eq)]
enum FsEntry {
- ExternalFile(BufferId),
+ ExternalFile(BufferId, Vec<ExcerptId>),
Directory(WorktreeId, Entry),
- File(WorktreeId, Entry),
+ File(WorktreeId, Entry, BufferId, Vec<ExcerptId>),
}
impl PartialEq for FsEntry {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
- (Self::ExternalFile(id_a), Self::ExternalFile(id_b)) => id_a == id_b,
+ (Self::ExternalFile(id_a, _), Self::ExternalFile(id_b, _)) => id_a == id_b,
(Self::Directory(id_a, entry_a), Self::Directory(id_b, entry_b)) => {
id_a == id_b && entry_a.id == entry_b.id
}
- (Self::File(worktree_a, entry_a), Self::File(worktree_b, entry_b)) => {
- worktree_a == worktree_b && entry_a.id == entry_b.id
- }
+ (
+ Self::File(worktree_a, entry_a, id_a, ..),
+ Self::File(worktree_b, entry_b, id_b, ..),
+ ) => worktree_a == worktree_b && entry_a.id == entry_b.id && id_a == id_b,
_ => false,
}
}
@@ -180,7 +220,7 @@ impl PartialEq for FsEntry {
impl FsEntry {
fn abs_path(&self, project: &Model<Project>, cx: &AppContext) -> Option<PathBuf> {
match self {
- Self::ExternalFile(buffer_id) => project
+ Self::ExternalFile(buffer_id, _) => project
.read(cx)
.buffer_for_id(*buffer_id)
.and_then(|buffer| File::from_dyn(buffer.read(cx).file()))
@@ -191,7 +231,7 @@ impl FsEntry {
.read(cx)
.absolutize(&entry.path)
.ok(),
- Self::File(worktree_id, entry) => project
+ Self::File(worktree_id, entry, _, _) => project
.read(cx)
.worktree_for_id(*worktree_id, cx)?
.read(cx)
@@ -206,21 +246,13 @@ impl FsEntry {
cx: &'a AppContext,
) -> Option<&'a Path> {
match self {
- Self::ExternalFile(buffer_id) => project
+ Self::ExternalFile(buffer_id, _) => project
.read(cx)
.buffer_for_id(*buffer_id)
.and_then(|buffer| buffer.read(cx).file())
.map(|file| file.path().as_ref()),
Self::Directory(_, entry) => Some(entry.path.as_ref()),
- Self::File(_, entry) => Some(entry.path.as_ref()),
- }
- }
-
- fn outlines_container(&self) -> Option<OutlinesContainer> {
- match self {
- Self::ExternalFile(buffer_id) => Some(OutlinesContainer::ExternalFile(*buffer_id)),
- Self::File(worktree_id, entry) => Some(OutlinesContainer::File(*worktree_id, entry.id)),
- Self::Directory(..) => None,
+ Self::File(_, entry, ..) => Some(entry.path.as_ref()),
}
}
}
@@ -228,7 +260,7 @@ impl FsEntry {
struct ActiveItem {
item_id: EntityId,
active_editor: WeakView<Editor>,
- _editor_subscrpiption: Option<Subscription>,
+ _editor_subscrpiption: Subscription,
}
#[derive(Debug)]
@@ -241,22 +273,6 @@ struct SerializedOutlinePanel {
width: Option<Pixels>,
}
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct EntryDetails {
- filename: String,
- icon: Option<Arc<str>>,
- path: Arc<Path>,
- depth: usize,
- kind: EntryKind,
- is_ignored: bool,
- is_expanded: bool,
- is_selected: bool,
- git_status: Option<GitFileStatus>,
- is_private: bool,
- worktree_id: WorktreeId,
- canonical_path: Option<PathBuf>,
-}
-
pub fn init_settings(cx: &mut AppContext) {
OutlinePanelSettings::register(cx);
}
@@ -357,7 +373,7 @@ impl OutlinePanel {
focus_handle,
fs_entries: Vec::new(),
fs_entries_depth: HashMap::default(),
- collapsed_dirs: HashMap::default(),
+ collapsed_entries: HashSet::default(),
unfolded_dirs: HashMap::default(),
selected_entry: None,
context_menu: None,
@@ -366,7 +382,7 @@ impl OutlinePanel {
pending_serialization: Task::ready(None),
update_task: Task::ready(()),
outline_fetch_tasks: Vec::new(),
- outlines: HashMap::default(),
+ excerpts: HashMap::default(),
last_visible_range: 0..0,
cached_entries_with_depth: None,
_subscriptions: vec![
@@ -509,8 +525,8 @@ impl OutlinePanel {
Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
};
- match &entry {
- EntryOwned::Entry(FsEntry::ExternalFile(buffer_id)) => {
+ match entry {
+ EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
let scroll_target = multi_buffer_snapshot.excerpts().find_map(
|(excerpt_id, buffer_snapshot, excerpt_range)| {
if &buffer_snapshot.remote_id() == buffer_id {
@@ -540,7 +556,7 @@ impl OutlinePanel {
entry @ EntryOwned::FoldedDirs(..) => {
self.toggle_expanded(entry, cx);
}
- EntryOwned::Entry(FsEntry::File(_, file_entry)) => {
+ EntryOwned::Entry(FsEntry::File(_, file_entry, ..)) => {
let scroll_target = self
.project
.update(cx, |project, cx| {
@@ -571,45 +587,11 @@ impl OutlinePanel {
})
}
}
- EntryOwned::Outline(_, outline) => {
- let Some(full_buffer_snapshot) =
- outline
- .range
- .start
- .buffer_id
- .and_then(|buffer_id| active_multi_buffer.read(cx).buffer(buffer_id))
- .or_else(|| {
- outline.range.end.buffer_id.and_then(|buffer_id| {
- active_multi_buffer.read(cx).buffer(buffer_id)
- })
- })
- .map(|buffer| buffer.read(cx).snapshot())
- else {
- return;
- };
- let outline_offset_range = outline.range.to_offset(&full_buffer_snapshot);
+ EntryOwned::Outline(_, excerpt_id, outline) => {
let scroll_target = multi_buffer_snapshot
- .excerpts()
- .filter(|(_, buffer_snapshot, _)| {
- let buffer_id = buffer_snapshot.remote_id();
- Some(buffer_id) == outline.range.start.buffer_id
- || Some(buffer_id) == outline.range.end.buffer_id
- })
- .min_by_key(|(_, _, excerpt_range)| {
- let excerpt_offeset_range =
- excerpt_range.context.to_offset(&full_buffer_snapshot);
- ((outline_offset_range.start / 2 + outline_offset_range.end / 2) as isize
- - (excerpt_offeset_range.start / 2 + excerpt_offeset_range.end / 2)
- as isize)
- .abs()
- })
- .and_then(|(excerpt_id, excerpt_snapshot, excerpt_range)| {
- let location = if outline.range.start.is_valid(excerpt_snapshot) {
- outline.range.start
- } else {
- excerpt_range.context.start
- };
- multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, location)
+ .anchor_in_excerpt(*excerpt_id, outline.range.start)
+ .or_else(|| {
+ multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, outline.range.end)
});
if let Some(anchor) = scroll_target {
self.selected_entry = Some(entry.clone());
@@ -624,242 +606,163 @@ impl OutlinePanel {
})
}
}
+ excerpt_entry @ EntryOwned::Excerpt(_, excerpt_id, excerpt_range) => {
+ self.toggle_expanded(excerpt_entry, cx);
+ let scroll_target = multi_buffer_snapshot
+ .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start);
+ if let Some(anchor) = scroll_target {
+ self.selected_entry = Some(entry.clone());
+ active_editor.update(cx, |editor, cx| {
+ editor.set_scroll_anchor(
+ ScrollAnchor {
+ offset: Point::default(),
+ anchor,
+ },
+ cx,
+ );
+ })
+ }
+ }
}
}
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
- if let Some(selected_entry) = &self.selected_entry {
- let outline_to_select = match selected_entry {
- EntryOwned::Entry(entry) => entry.outlines_container().and_then(|container| {
- let next_outline = self.outlines.get(&container)?.first()?.clone();
- Some((container, next_outline))
- }),
- EntryOwned::FoldedDirs(..) => None,
- EntryOwned::Outline(container, outline) => self
- .outlines
- .get(container)
- .and_then(|outlines| {
- outlines.iter().skip_while(|o| o != &outline).skip(1).next()
- })
- .map(|outline| (*container, outline.clone())),
- }
- .map(|(container, outline)| EntryOwned::Outline(container, outline));
-
- let entry_to_select = outline_to_select.or_else(|| {
- match selected_entry {
- EntryOwned::Entry(entry) => self
- .fs_entries
- .iter()
- .skip_while(|e| e != &entry)
- .skip(1)
- .next(),
- EntryOwned::FoldedDirs(worktree_id, dirs) => self
- .fs_entries
- .iter()
- .skip_while(|e| {
- if let FsEntry::Directory(dir_worktree_id, dir_entry) = e {
- dir_worktree_id != worktree_id || dirs.last() != Some(dir_entry)
- } else {
- true
- }
- })
- .skip(1)
- .next(),
- EntryOwned::Outline(container, _) => self
- .fs_entries
- .iter()
- .skip_while(|entry| entry.outlines_container().as_ref() != Some(container))
- .skip(1)
- .next(),
- }
+ if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
+ self.entries_with_depths(cx)
+ .iter()
+ .map(|(_, entry)| entry)
+ .skip_while(|entry| entry != &&selected_entry)
+ .skip(1)
+ .next()
.cloned()
- .map(EntryOwned::Entry)
- });
-
- if let Some(entry_to_select) = entry_to_select {
- self.selected_entry = Some(entry_to_select);
- self.autoscroll(cx);
- cx.notify();
- }
+ }) {
+ self.selected_entry = Some(entry_to_select);
+ self.autoscroll(cx);
+ cx.notify();
} else {
self.select_first(&SelectFirst {}, cx)
}
}
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
- if let Some(selected_entry) = &self.selected_entry {
- let outline_to_select = match selected_entry {
- EntryOwned::Entry(entry) => {
- let previous_entry = self
- .fs_entries
- .iter()
- .rev()
- .skip_while(|e| e != &entry)
- .skip(1)
- .next();
- previous_entry
- .and_then(|entry| entry.outlines_container())
- .and_then(|container| {
- let previous_outline = self.outlines.get(&container)?.last()?.clone();
- Some((container, previous_outline))
- })
- }
- EntryOwned::FoldedDirs(worktree_id, dirs) => {
- let previous_entry = self
- .fs_entries
- .iter()
- .rev()
- .skip_while(|e| {
- if let FsEntry::Directory(dir_worktree_id, dir_entry) = e {
- dir_worktree_id != worktree_id || dirs.first() != Some(dir_entry)
- } else {
- true
- }
- })
- .skip(1)
- .next();
- previous_entry
- .and_then(|entry| entry.outlines_container())
- .and_then(|container| {
- let previous_outline = self.outlines.get(&container)?.last()?.clone();
- Some((container, previous_outline))
- })
- }
- EntryOwned::Outline(container, outline) => self
- .outlines
- .get(container)
- .and_then(|outlines| {
- outlines
- .iter()
- .rev()
- .skip_while(|o| o != &outline)
- .skip(1)
- .next()
- })
- .map(|outline| (*container, outline.clone())),
- }
- .map(|(container, outline)| EntryOwned::Outline(container, outline));
-
- let entry_to_select = outline_to_select.or_else(|| {
- match selected_entry {
- EntryOwned::Entry(entry) => self
- .fs_entries
- .iter()
- .rev()
- .skip_while(|e| e != &entry)
- .skip(1)
- .next(),
- EntryOwned::FoldedDirs(worktree_id, dirs) => self
- .fs_entries
- .iter()
- .rev()
- .skip_while(|e| {
- if let FsEntry::Directory(dir_worktree_id, dir_entry) = e {
- dir_worktree_id != worktree_id || dirs.first() != Some(dir_entry)
- } else {
- true
- }
- })
- .skip(1)
- .next(),
- EntryOwned::Outline(container, _) => self
- .fs_entries
- .iter()
- .rev()
- .find(|entry| entry.outlines_container().as_ref() == Some(container)),
- }
+ if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
+ self.entries_with_depths(cx)
+ .iter()
+ .rev()
+ .map(|(_, entry)| entry)
+ .skip_while(|entry| entry != &&selected_entry)
+ .skip(1)
+ .next()
.cloned()
- .map(EntryOwned::Entry)
- });
-
- if let Some(entry_to_select) = entry_to_select {
- self.selected_entry = Some(entry_to_select);
- self.autoscroll(cx);
- cx.notify();
- }
+ }) {
+ self.selected_entry = Some(entry_to_select);
+ self.autoscroll(cx);
+ cx.notify();
} else {
- self.select_first(&SelectFirst {}, cx);
+ self.select_first(&SelectFirst {}, cx)
}
}
fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext<Self>) {
- if let Some(selected_entry) = &self.selected_entry {
- let parent_entry = match selected_entry {
- EntryOwned::Entry(entry) => self
- .fs_entries
- .iter()
- .rev()
- .skip_while(|e| e != &entry)
- .skip(1)
- .find(|entry_before_current| match (entry, entry_before_current) {
- (
- FsEntry::File(worktree_id, entry)
- | FsEntry::Directory(worktree_id, entry),
- FsEntry::Directory(parent_worktree_id, parent_entry),
- ) => {
- parent_worktree_id == worktree_id
- && directory_contains(parent_entry, entry)
+ if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
+ let mut previous_entries = self
+ .entries_with_depths(cx)
+ .iter()
+ .rev()
+ .map(|(_, entry)| entry)
+ .skip_while(|entry| entry != &&selected_entry)
+ .skip(1);
+ match &selected_entry {
+ EntryOwned::Entry(fs_entry) => match fs_entry {
+ FsEntry::ExternalFile(..) => None,
+ FsEntry::File(worktree_id, entry, ..)
+ | FsEntry::Directory(worktree_id, entry) => {
+ entry.path.parent().and_then(|parent_path| {
+ previous_entries.find(|entry| match entry {
+ EntryOwned::Entry(FsEntry::Directory(
+ dir_worktree_id,
+ dir_entry,
+ )) => {
+ dir_worktree_id == worktree_id
+ && dir_entry.path.as_ref() == parent_path
+ }
+ EntryOwned::FoldedDirs(dirs_worktree_id, dirs) => {
+ dirs_worktree_id == worktree_id
+ && dirs
+ .first()
+ .map_or(false, |dir| dir.path.as_ref() == parent_path)
+ }
+ _ => false,
+ })
+ })
+ }
+ },
+ EntryOwned::FoldedDirs(worktree_id, entries) => entries
+ .first()
+ .and_then(|entry| entry.path.parent())
+ .and_then(|parent_path| {
+ previous_entries.find(|entry| {
+ if let EntryOwned::Entry(FsEntry::Directory(
+ dir_worktree_id,
+ dir_entry,
+ )) = entry
+ {
+ dir_worktree_id == worktree_id
+ && dir_entry.path.as_ref() == parent_path
+ } else {
+ false
+ }
+ })
+ }),
+ EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) => {
+ previous_entries.find(|entry| match entry {
+ EntryOwned::Entry(FsEntry::File(_, _, file_buffer_id, file_excerpts)) => {
+ file_buffer_id == excerpt_buffer_id
+ && file_excerpts.contains(&excerpt_id)
+ }
+ EntryOwned::Entry(FsEntry::ExternalFile(file_buffer_id, file_excerpts)) => {
+ file_buffer_id == excerpt_buffer_id
+ && file_excerpts.contains(&excerpt_id)
}
_ => false,
- }),
- EntryOwned::FoldedDirs(worktree_id, dirs) => self
- .fs_entries
- .iter()
- .rev()
- .skip_while(|e| {
- if let FsEntry::Directory(dir_worktree_id, dir_entry) = e {
- dir_worktree_id != worktree_id || dirs.first() != Some(dir_entry)
+ })
+ }
+ EntryOwned::Outline(outline_buffer_id, outline_excerpt_id, _) => previous_entries
+ .find(|entry| {
+ if let EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) = entry {
+ outline_buffer_id == excerpt_buffer_id
+ && outline_excerpt_id == excerpt_id
} else {
- true
+ false
}
- })
- .skip(1)
- .find(
- |entry_before_current| match (dirs.first(), entry_before_current) {
- (Some(entry), FsEntry::Directory(parent_worktree_id, parent_entry)) => {
- parent_worktree_id == worktree_id
- && directory_contains(parent_entry, entry)
- }
- _ => false,
- },
- ),
- EntryOwned::Outline(container, _) => self
- .fs_entries
- .iter()
- .find(|entry| entry.outlines_container().as_ref() == Some(container)),
- }
- .cloned()
- .map(EntryOwned::Entry);
- if let Some(parent_entry) = parent_entry {
- self.selected_entry = Some(parent_entry);
- self.autoscroll(cx);
- cx.notify();
+ }),
}
+ }) {
+ self.selected_entry = Some(entry_to_select.clone());
+ self.autoscroll(cx);
+ cx.notify();
} else {
self.select_first(&SelectFirst {}, cx);
}
}
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
- if let Some(first_entry) = self.fs_entries.first().cloned().map(EntryOwned::Entry) {
- self.selected_entry = Some(first_entry);
+ if let Some((_, first_entry)) = self.entries_with_depths(cx).iter().next() {
+ self.selected_entry = Some(first_entry.clone());
self.autoscroll(cx);
cx.notify();
}
}
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
- if let Some(new_selection) = self.fs_entries.last().map(|last_entry| {
- last_entry
- .outlines_container()
- .and_then(|container| {
- let outline = self.outlines.get(&container)?.last()?;
- Some((container, outline.clone()))
- })
- .map(|(container, outline)| EntryOwned::Outline(container, outline))
- .unwrap_or_else(|| EntryOwned::Entry(last_entry.clone()))
- }) {
- self.selected_entry = Some(new_selection);
+ if let Some(new_selection) = self
+ .entries_with_depths(cx)
+ .iter()
+ .rev()
+ .map(|(_, entry)| entry)
+ .next()
+ {
+ self.selected_entry = Some(new_selection.clone());
self.autoscroll(cx);
cx.notify();
}
@@ -892,7 +795,7 @@ impl OutlinePanel {
) {
self.selected_entry = Some(entry.to_owned_entry());
let is_root = match entry {
- EntryRef::Entry(FsEntry::File(worktree_id, entry))
+ EntryRef::Entry(FsEntry::File(worktree_id, entry, ..))
| EntryRef::Entry(FsEntry::Directory(worktree_id, entry)) => self
.project
.read(cx)
@@ -913,7 +816,11 @@ impl OutlinePanel {
})
.unwrap_or(false),
EntryRef::Entry(FsEntry::ExternalFile(..)) => false,
- EntryRef::Outline(_, _) => {
+ EntryRef::Excerpt(..) => {
+ cx.notify();
+ return;
+ }
+ EntryRef::Outline(..) => {
cx.notify();
return;
}
@@ -985,8 +892,8 @@ impl OutlinePanel {
})
.skip(1)
.filter(|next_entry| match next_entry {
- FsEntry::ExternalFile(_) => false,
- FsEntry::Directory(worktree_id, entry) | FsEntry::File(worktree_id, 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())
}
@@ -1004,23 +911,32 @@ impl OutlinePanel {
else {
return;
};
- if let Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, selected_dir_entry))) =
- &self.selected_entry
- {
- let expanded = self
- .collapsed_dirs
- .get_mut(worktree_id)
- .map_or(false, |hidden_dirs| {
- hidden_dirs.remove(&selected_dir_entry.id)
- });
- if expanded {
+
+ let entry_to_expand = match &self.selected_entry {
+ Some(EntryOwned::FoldedDirs(worktree_id, dir_entries)) => dir_entries
+ .last()
+ .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)),
+ Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry))) => {
+ Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
+ }
+ Some(EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
+ Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
+ }
+ _ => None,
+ };
+ let Some(collapsed_entry) = entry_to_expand else {
+ return;
+ };
+ let expanded = self.collapsed_entries.remove(&collapsed_entry);
+ if expanded {
+ if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
self.project.update(cx, |project, cx| {
- project.expand_entry(*worktree_id, selected_dir_entry.id, cx);
+ project.expand_entry(worktree_id, dir_entry_id, cx);
});
- self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
- } else {
- self.select_next(&SelectNext, cx)
}
+ self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
+ } else {
+ self.select_next(&SelectNext, cx)
}
}
@@ -1032,22 +948,54 @@ impl OutlinePanel {
else {
return;
};
- if let Some(
- dir_entry @ EntryOwned::Entry(FsEntry::Directory(worktree_id, selected_dir_entry)),
- ) = &self.selected_entry
- {
- self.collapsed_dirs
- .entry(*worktree_id)
- .or_default()
- .insert(selected_dir_entry.id);
- self.update_fs_entries(
- &editor,
- HashSet::default(),
- Some(dir_entry.clone()),
- None,
- false,
- cx,
- );
+ 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,
+ );
+ }
+ Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => {
+ if let Some(dir_entry) = dir_entries.last() {
+ if self
+ .collapsed_entries
+ .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
+ {
+ self.update_fs_entries(
+ &editor,
+ HashSet::default(),
+ Some(dirs_entry.clone()),
+ None,
+ false,
+ cx,
+ );
+ }
+ }
+ }
+ Some(excerpt_entry @ EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
+ if self
+ .collapsed_entries
+ .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
+ {
+ self.update_fs_entries(
+ &editor,
+ HashSet::default(),
+ Some(excerpt_entry.clone()),
+ None,
+ false,
+ cx,
+ );
+ }
+ }
+ _ => (),
}
}
@@ -1060,15 +1008,12 @@ impl OutlinePanel {
return;
};
- self.fs_entries_depth
- .iter()
- .filter(|(_, &(is_dir, depth))| is_dir && depth == 0)
- .for_each(|(&(worktree_id, entry_id), _)| {
- self.collapsed_dirs
- .entry(worktree_id)
- .or_default()
- .insert(entry_id);
- });
+ self.collapsed_entries.extend(
+ self.fs_entries_depth
+ .iter()
+ .filter(|(_, &(is_dir, depth))| is_dir && depth == 0)
+ .map(|(&(worktree_id, entry_id), _)| CollapsedEntry::Dir(worktree_id, entry_id)),
+ );
self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
}
@@ -1084,47 +1029,39 @@ impl OutlinePanel {
match entry {
EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry)) => {
let entry_id = dir_entry.id;
- match self.collapsed_dirs.entry(*worktree_id) {
- hash_map::Entry::Occupied(mut o) => {
- let collapsed_dir_ids = o.get_mut();
- if collapsed_dir_ids.remove(&entry_id) {
- self.project
- .update(cx, |project, cx| {
- project.expand_entry(*worktree_id, entry_id, cx)
- })
- .unwrap_or_else(|| Task::ready(Ok(())))
- .detach_and_log_err(cx);
- } else {
- collapsed_dir_ids.insert(entry_id);
- }
- }
- hash_map::Entry::Vacant(v) => {
- v.insert(BTreeSet::new()).insert(entry_id);
- }
+ let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
+ if self.collapsed_entries.remove(&collapsed_entry) {
+ self.project
+ .update(cx, |project, cx| {
+ project.expand_entry(*worktree_id, entry_id, cx)
+ })
+ .unwrap_or_else(|| Task::ready(Ok(())))
+ .detach_and_log_err(cx);
+ } else {
+ self.collapsed_entries.insert(collapsed_entry);
}
}
EntryOwned::FoldedDirs(worktree_id, dir_entries) => {
if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
- match self.collapsed_dirs.entry(*worktree_id) {
- hash_map::Entry::Occupied(mut o) => {
- let collapsed_dir_ids = o.get_mut();
- if collapsed_dir_ids.remove(&entry_id) {
- self.project
- .update(cx, |project, cx| {
- project.expand_entry(*worktree_id, entry_id, cx)
- })
- .unwrap_or_else(|| Task::ready(Ok(())))
- .detach_and_log_err(cx);
- } else {
- collapsed_dir_ids.insert(entry_id);
- }
- }
- hash_map::Entry::Vacant(v) => {
- v.insert(BTreeSet::new()).insert(entry_id);
- }
+ let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
+ if self.collapsed_entries.remove(&collapsed_entry) {
+ self.project
+ .update(cx, |project, cx| {
+ project.expand_entry(*worktree_id, entry_id, cx)
+ })
+ .unwrap_or_else(|| Task::ready(Ok(())))
+ .detach_and_log_err(cx);
+ } else {
+ self.collapsed_entries.insert(collapsed_entry);
}
}
}
+ EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
+ let collapsed_entry = CollapsedEntry::Excerpt(*buffer_id, *excerpt_id);
+ if !self.collapsed_entries.remove(&collapsed_entry) {
+ self.collapsed_entries.insert(collapsed_entry);
+ }
+ }
_ => return,
}
@@ -1156,7 +1093,7 @@ impl OutlinePanel {
.and_then(|entry| match entry {
EntryOwned::Entry(entry) => entry.relative_path(&self.project, cx),
EntryOwned::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.as_ref()),
- EntryOwned::Outline(..) => None,
+ EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
})
.map(|p| p.to_string_lossy().to_string())
{
@@ -1197,37 +1134,24 @@ impl OutlinePanel {
editor: &View<Editor>,
cx: &mut ViewContext<'_, Self>,
) {
- let Some((container, outline_item)) = self.location_for_editor_selection(editor, cx) else {
+ if !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
+ return;
+ }
+ let Some((buffer_id, excerpt_id, outline)) = self.location_for_editor_selection(editor, cx)
+ else {
return;
};
-
- let file_entry_to_expand = self
- .fs_entries
- .iter()
- .find(|entry| match (entry, &container) {
- (
- FsEntry::ExternalFile(buffer_id),
- OutlinesContainer::ExternalFile(container_buffer_id),
- ) => buffer_id == container_buffer_id,
- (
- FsEntry::File(file_worktree_id, file_entry),
- OutlinesContainer::File(worktree_id, id),
- ) => file_worktree_id == worktree_id && &file_entry.id == id,
- _ => false,
- });
- let Some(entry_to_select) = outline_item
- .map(|outline| EntryOwned::Outline(container, outline))
- .or_else(|| Some(EntryOwned::Entry(file_entry_to_expand.cloned()?)))
+ let Some((file_entry_with_selection, entry_with_selection)) =
+ self.entry_for_selection(buffer_id, excerpt_id, outline)
else {
return;
};
-
- if self.selected_entry.as_ref() == Some(&entry_to_select) {
+ if self.selected_entry.as_ref() == Some(&entry_with_selection) {
return;
}
- if let Some(FsEntry::File(file_worktree_id, file_entry)) = file_entry_to_expand {
- if let Some(worktree) = self.project.read(cx).worktree_for_id(*file_worktree_id, cx) {
+ if let FsEntry::File(file_worktree_id, file_entry, ..) = file_entry_with_selection {
+ if let Some(worktree) = self.project.read(cx).worktree_for_id(file_worktree_id, cx) {
let parent_entry = {
let mut traversal = worktree.read(cx).traverse_from_path(
true,