mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 20:41:53 +03:00
Slightly improve project panel ergonomics (#13704)
* properly fetch outlines from channel notes and other project-less external files * show better messages when for no contents * make file entries collapsible (hiding all excerpts and outlines beneath), keep the initial panel state unfolded up to file level Release Notes: - Slightly improved project panel ergonomics
This commit is contained in:
parent
25ad3185e0
commit
0e60730742
@ -40,7 +40,7 @@ use workspace::{
|
|||||||
item::ItemHandle,
|
item::ItemHandle,
|
||||||
ui::{
|
ui::{
|
||||||
h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, Icon, IconName, IconSize,
|
h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, Icon, IconName, IconSize,
|
||||||
Label, LabelCommon, ListItem, Selectable, StyledTypography,
|
Label, LabelCommon, ListItem, Selectable, Spacing, StyledTypography,
|
||||||
},
|
},
|
||||||
OpenInTerminal, Workspace,
|
OpenInTerminal, Workspace,
|
||||||
};
|
};
|
||||||
@ -85,6 +85,7 @@ pub struct OutlinePanel {
|
|||||||
selected_entry: Option<EntryOwned>,
|
selected_entry: Option<EntryOwned>,
|
||||||
active_item: Option<ActiveItem>,
|
active_item: Option<ActiveItem>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
|
loading_outlines: bool,
|
||||||
update_task: Task<()>,
|
update_task: Task<()>,
|
||||||
outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
|
outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
|
||||||
excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
|
excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
|
||||||
@ -94,6 +95,8 @@ pub struct OutlinePanel {
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
enum CollapsedEntry {
|
enum CollapsedEntry {
|
||||||
Dir(WorktreeId, ProjectEntryId),
|
Dir(WorktreeId, ProjectEntryId),
|
||||||
|
File(WorktreeId, BufferId),
|
||||||
|
ExternalFile(BufferId),
|
||||||
Excerpt(BufferId, ExcerptId),
|
Excerpt(BufferId, ExcerptId),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,19 +158,6 @@ impl EntryOwned {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn abs_path(&self, project: &Model<Project>, cx: &AppContext) -> Option<PathBuf> {
|
|
||||||
match self {
|
|
||||||
Self::Entry(entry) => entry.abs_path(project, cx),
|
|
||||||
Self::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
|
|
||||||
project
|
|
||||||
.read(cx)
|
|
||||||
.worktree_for_id(*worktree_id, cx)
|
|
||||||
.and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
|
|
||||||
}),
|
|
||||||
Self::Excerpt(..) | Self::Outline(..) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@ -218,46 +208,6 @@ impl PartialEq for FsEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FsEntry {
|
|
||||||
fn abs_path(&self, project: &Model<Project>, cx: &AppContext) -> Option<PathBuf> {
|
|
||||||
match self {
|
|
||||||
Self::ExternalFile(buffer_id, _) => project
|
|
||||||
.read(cx)
|
|
||||||
.buffer_for_id(*buffer_id)
|
|
||||||
.and_then(|buffer| File::from_dyn(buffer.read(cx).file()))
|
|
||||||
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok()),
|
|
||||||
Self::Directory(worktree_id, entry) => project
|
|
||||||
.read(cx)
|
|
||||||
.worktree_for_id(*worktree_id, cx)?
|
|
||||||
.read(cx)
|
|
||||||
.absolutize(&entry.path)
|
|
||||||
.ok(),
|
|
||||||
Self::File(worktree_id, entry, _, _) => project
|
|
||||||
.read(cx)
|
|
||||||
.worktree_for_id(*worktree_id, cx)?
|
|
||||||
.read(cx)
|
|
||||||
.absolutize(&entry.path)
|
|
||||||
.ok(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn relative_path<'a>(
|
|
||||||
&'a self,
|
|
||||||
project: &Model<Project>,
|
|
||||||
cx: &'a AppContext,
|
|
||||||
) -> Option<&'a Path> {
|
|
||||||
match self {
|
|
||||||
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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ActiveItem {
|
struct ActiveItem {
|
||||||
item_id: EntityId,
|
item_id: EntityId,
|
||||||
active_editor: WeakView<Editor>,
|
active_editor: WeakView<Editor>,
|
||||||
@ -383,6 +333,7 @@ impl OutlinePanel {
|
|||||||
width: None,
|
width: None,
|
||||||
active_item: None,
|
active_item: None,
|
||||||
pending_serialization: Task::ready(None),
|
pending_serialization: Task::ready(None),
|
||||||
|
loading_outlines: false,
|
||||||
update_task: Task::ready(()),
|
update_task: Task::ready(()),
|
||||||
outline_fetch_tasks: HashMap::default(),
|
outline_fetch_tasks: HashMap::default(),
|
||||||
excerpts: HashMap::default(),
|
excerpts: HashMap::default(),
|
||||||
@ -529,7 +480,9 @@ impl OutlinePanel {
|
|||||||
Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
|
Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.toggle_expanded(entry, cx);
|
||||||
match entry {
|
match entry {
|
||||||
|
EntryOwned::FoldedDirs(..) | EntryOwned::Entry(FsEntry::Directory(..)) => {}
|
||||||
EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
|
EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
|
||||||
let scroll_target = multi_buffer_snapshot.excerpts().find_map(
|
let scroll_target = multi_buffer_snapshot.excerpts().find_map(
|
||||||
|(excerpt_id, buffer_snapshot, excerpt_range)| {
|
|(excerpt_id, buffer_snapshot, excerpt_range)| {
|
||||||
@ -554,12 +507,6 @@ impl OutlinePanel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry @ EntryOwned::Entry(FsEntry::Directory(..)) => {
|
|
||||||
self.toggle_expanded(entry, cx);
|
|
||||||
}
|
|
||||||
entry @ EntryOwned::FoldedDirs(..) => {
|
|
||||||
self.toggle_expanded(entry, cx);
|
|
||||||
}
|
|
||||||
EntryOwned::Entry(FsEntry::File(_, file_entry, ..)) => {
|
EntryOwned::Entry(FsEntry::File(_, file_entry, ..)) => {
|
||||||
let scroll_target = self
|
let scroll_target = self
|
||||||
.project
|
.project
|
||||||
@ -610,8 +557,7 @@ impl OutlinePanel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
excerpt_entry @ EntryOwned::Excerpt(_, excerpt_id, excerpt_range) => {
|
EntryOwned::Excerpt(_, excerpt_id, excerpt_range) => {
|
||||||
self.toggle_expanded(excerpt_entry, cx);
|
|
||||||
let scroll_target = multi_buffer_snapshot
|
let scroll_target = multi_buffer_snapshot
|
||||||
.anchor_in_excerpt(*excerpt_id, excerpt_range.context.start);
|
.anchor_in_excerpt(*excerpt_id, excerpt_range.context.start);
|
||||||
if let Some(anchor) = scroll_target {
|
if let Some(anchor) = scroll_target {
|
||||||
@ -923,10 +869,16 @@ impl OutlinePanel {
|
|||||||
Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry))) => {
|
Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry))) => {
|
||||||
Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
|
Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
|
||||||
}
|
}
|
||||||
|
Some(EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => {
|
||||||
|
Some(CollapsedEntry::File(*worktree_id, *buffer_id))
|
||||||
|
}
|
||||||
|
Some(EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => {
|
||||||
|
Some(CollapsedEntry::ExternalFile(*buffer_id))
|
||||||
|
}
|
||||||
Some(EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
|
Some(EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
|
||||||
Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
|
Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
|
||||||
}
|
}
|
||||||
_ => None,
|
None | Some(EntryOwned::Outline(..)) => None,
|
||||||
};
|
};
|
||||||
let Some(collapsed_entry) = entry_to_expand else {
|
let Some(collapsed_entry) = entry_to_expand else {
|
||||||
return;
|
return;
|
||||||
@ -967,6 +919,30 @@ impl OutlinePanel {
|
|||||||
cx,
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => {
|
Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => {
|
||||||
if let Some(dir_entry) = dir_entries.last() {
|
if let Some(dir_entry) = dir_entries.last() {
|
||||||
if self
|
if self
|
||||||
@ -999,7 +975,7 @@ impl OutlinePanel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
None | Some(EntryOwned::Outline(..)) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1019,13 +995,19 @@ impl OutlinePanel {
|
|||||||
EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => {
|
EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => {
|
||||||
Some(CollapsedEntry::Dir(*worktree_id, entry.id))
|
Some(CollapsedEntry::Dir(*worktree_id, entry.id))
|
||||||
}
|
}
|
||||||
|
EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
|
||||||
|
Some(CollapsedEntry::File(*worktree_id, *buffer_id))
|
||||||
|
}
|
||||||
|
EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
|
||||||
|
Some(CollapsedEntry::ExternalFile(*buffer_id))
|
||||||
|
}
|
||||||
EntryOwned::FoldedDirs(worktree_id, entries) => {
|
EntryOwned::FoldedDirs(worktree_id, entries) => {
|
||||||
Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id))
|
Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id))
|
||||||
}
|
}
|
||||||
EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
|
EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
|
||||||
Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
|
Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
|
||||||
}
|
}
|
||||||
_ => None,
|
EntryOwned::Outline(..) => None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
self.collapsed_entries.extend(new_entries);
|
self.collapsed_entries.extend(new_entries);
|
||||||
@ -1056,6 +1038,18 @@ impl OutlinePanel {
|
|||||||
self.collapsed_entries.insert(collapsed_entry);
|
self.collapsed_entries.insert(collapsed_entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
|
||||||
|
let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
|
||||||
|
if !self.collapsed_entries.remove(&collapsed_entry) {
|
||||||
|
self.collapsed_entries.insert(collapsed_entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
|
||||||
|
let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id);
|
||||||
|
if !self.collapsed_entries.remove(&collapsed_entry) {
|
||||||
|
self.collapsed_entries.insert(collapsed_entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
EntryOwned::FoldedDirs(worktree_id, dir_entries) => {
|
EntryOwned::FoldedDirs(worktree_id, dir_entries) => {
|
||||||
if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
|
if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
|
||||||
let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
|
let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
|
||||||
@ -1077,7 +1071,7 @@ impl OutlinePanel {
|
|||||||
self.collapsed_entries.insert(collapsed_entry);
|
self.collapsed_entries.insert(collapsed_entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return,
|
EntryOwned::Outline(..) => return,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_fs_entries(
|
self.update_fs_entries(
|
||||||
@ -1094,7 +1088,7 @@ impl OutlinePanel {
|
|||||||
if let Some(clipboard_text) = self
|
if let Some(clipboard_text) = self
|
||||||
.selected_entry
|
.selected_entry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|entry| entry.abs_path(&self.project, cx))
|
.and_then(|entry| self.abs_path(&entry, cx))
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
{
|
{
|
||||||
cx.write_to_clipboard(ClipboardItem::new(clipboard_text));
|
cx.write_to_clipboard(ClipboardItem::new(clipboard_text));
|
||||||
@ -1106,8 +1100,10 @@ impl OutlinePanel {
|
|||||||
.selected_entry
|
.selected_entry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|entry| match entry {
|
.and_then(|entry| match entry {
|
||||||
EntryOwned::Entry(entry) => entry.relative_path(&self.project, cx),
|
EntryOwned::Entry(entry) => self.relative_path(&entry, cx),
|
||||||
EntryOwned::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.as_ref()),
|
EntryOwned::FoldedDirs(_, dirs) => {
|
||||||
|
dirs.last().map(|entry| entry.path.to_path_buf())
|
||||||
|
}
|
||||||
EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
|
EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
|
||||||
})
|
})
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
@ -1120,7 +1116,7 @@ impl OutlinePanel {
|
|||||||
if let Some(abs_path) = self
|
if let Some(abs_path) = self
|
||||||
.selected_entry
|
.selected_entry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|entry| entry.abs_path(&self.project, cx))
|
.and_then(|entry| self.abs_path(&entry, cx))
|
||||||
{
|
{
|
||||||
cx.reveal_path(&abs_path);
|
cx.reveal_path(&abs_path);
|
||||||
}
|
}
|
||||||
@ -1128,7 +1124,7 @@ impl OutlinePanel {
|
|||||||
|
|
||||||
fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
|
fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
|
||||||
let selected_entry = self.selected_entry.as_ref();
|
let selected_entry = self.selected_entry.as_ref();
|
||||||
let abs_path = selected_entry.and_then(|entry| entry.abs_path(&self.project, cx));
|
let abs_path = selected_entry.and_then(|entry| self.abs_path(&entry, cx));
|
||||||
let working_directory = if let (
|
let working_directory = if let (
|
||||||
Some(abs_path),
|
Some(abs_path),
|
||||||
Some(EntryOwned::Entry(FsEntry::File(..) | FsEntry::ExternalFile(..))),
|
Some(EntryOwned::Entry(FsEntry::File(..) | FsEntry::ExternalFile(..))),
|
||||||
@ -1173,17 +1169,26 @@ impl OutlinePanel {
|
|||||||
}
|
}
|
||||||
EntryOwned::Outline(buffer_id, excerpt_id, _)
|
EntryOwned::Outline(buffer_id, excerpt_id, _)
|
||||||
| EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
|
| EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
|
||||||
|
self.collapsed_entries
|
||||||
|
.remove(&CollapsedEntry::ExternalFile(buffer_id));
|
||||||
self.collapsed_entries
|
self.collapsed_entries
|
||||||
.remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
|
.remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
let entry_id = project
|
let entry_id = project
|
||||||
.buffer_for_id(buffer_id)
|
.buffer_for_id(buffer_id)
|
||||||
.and_then(|buffer| buffer.read(cx).entry_id(cx));
|
.and_then(|buffer| buffer.read(cx).entry_id(cx));
|
||||||
|
|
||||||
entry_id.and_then(|entry_id| {
|
entry_id.and_then(|entry_id| {
|
||||||
let worktree = project.worktree_for_entry(entry_id, cx)?;
|
project
|
||||||
|
.worktree_for_entry(entry_id, cx)
|
||||||
|
.and_then(|worktree| {
|
||||||
|
let worktree_id = worktree.read(cx).id();
|
||||||
|
self.collapsed_entries
|
||||||
|
.remove(&CollapsedEntry::File(worktree_id, buffer_id));
|
||||||
let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
|
let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
|
||||||
Some((worktree, entry))
|
Some((worktree, entry))
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
EntryOwned::Entry(FsEntry::ExternalFile(..)) => None,
|
EntryOwned::Entry(FsEntry::ExternalFile(..)) => None,
|
||||||
_ => return,
|
_ => return,
|
||||||
@ -1274,12 +1279,7 @@ impl OutlinePanel {
|
|||||||
}
|
}
|
||||||
.unwrap_or_else(empty_icon);
|
.unwrap_or_else(empty_icon);
|
||||||
|
|
||||||
let buffer_snapshot = self
|
let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
|
||||||
.project
|
|
||||||
.read(cx)
|
|
||||||
.buffer_for_id(buffer_id)?
|
|
||||||
.read(cx)
|
|
||||||
.snapshot();
|
|
||||||
let excerpt_range = range.context.to_point(&buffer_snapshot);
|
let excerpt_range = range.context.to_point(&buffer_snapshot);
|
||||||
let label_element = Label::new(format!(
|
let label_element = Label::new(format!(
|
||||||
"Lines {}-{}",
|
"Lines {}-{}",
|
||||||
@ -1397,8 +1397,8 @@ impl OutlinePanel {
|
|||||||
}
|
}
|
||||||
FsEntry::ExternalFile(buffer_id, ..) => {
|
FsEntry::ExternalFile(buffer_id, ..) => {
|
||||||
let color = entry_label_color(is_active);
|
let color = entry_label_color(is_active);
|
||||||
let (icon, name) = match self.project.read(cx).buffer_for_id(*buffer_id) {
|
let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) {
|
||||||
Some(buffer) => match buffer.read(cx).file() {
|
Some(buffer_snapshot) => match buffer_snapshot.file() {
|
||||||
Some(file) => {
|
Some(file) => {
|
||||||
let path = file.path();
|
let path = file.path();
|
||||||
let icon = if settings.file_icons {
|
let icon = if settings.file_icons {
|
||||||
@ -1621,10 +1621,12 @@ impl OutlinePanel {
|
|||||||
let file = File::from_dyn(buffer_snapshot.file());
|
let file = File::from_dyn(buffer_snapshot.file());
|
||||||
let entry_id = file.and_then(|file| file.project_entry_id(cx));
|
let entry_id = file.and_then(|file| file.project_entry_id(cx));
|
||||||
let worktree = file.map(|file| file.worktree.read(cx).snapshot());
|
let worktree = file.map(|file| file.worktree.read(cx).snapshot());
|
||||||
|
let is_new =
|
||||||
|
new_entries.contains(&excerpt_id) || !self.excerpts.contains_key(&buffer_id);
|
||||||
buffer_excerpts
|
buffer_excerpts
|
||||||
.entry(buffer_id)
|
.entry(buffer_id)
|
||||||
.or_insert_with(|| (Vec::new(), entry_id, worktree))
|
.or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
|
||||||
.0
|
.1
|
||||||
.push(excerpt_id);
|
.push(excerpt_id);
|
||||||
|
|
||||||
let outlines = match self
|
let outlines = match self
|
||||||
@ -1656,6 +1658,7 @@ impl OutlinePanel {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.loading_outlines = true;
|
||||||
self.update_task = cx.spawn(|outline_panel, mut cx| async move {
|
self.update_task = cx.spawn(|outline_panel, mut cx| async move {
|
||||||
if let Some(debounce) = debounce {
|
if let Some(debounce) = debounce {
|
||||||
cx.background_executor().timer(debounce).await;
|
cx.background_executor().timer(debounce).await;
|
||||||
@ -1672,7 +1675,25 @@ impl OutlinePanel {
|
|||||||
>::default();
|
>::default();
|
||||||
let mut external_excerpts = HashMap::default();
|
let mut external_excerpts = HashMap::default();
|
||||||
|
|
||||||
for (buffer_id, (excerpts, entry_id, worktree)) in buffer_excerpts {
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for excerpt_id in &excerpts {
|
||||||
|
new_collapsed_entries
|
||||||
|
.insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(worktree) = worktree {
|
if let Some(worktree) = worktree {
|
||||||
let worktree_id = worktree.id();
|
let worktree_id = worktree.id();
|
||||||
let unfolded_dirs =
|
let unfolded_dirs =
|
||||||
@ -1688,9 +1709,6 @@ impl OutlinePanel {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut entries_to_add = HashSet::default();
|
let mut entries_to_add = HashSet::default();
|
||||||
let is_new = excerpts
|
|
||||||
.iter()
|
|
||||||
.any(|excerpt_id| new_entries.contains(excerpt_id));
|
|
||||||
worktree_excerpts
|
worktree_excerpts
|
||||||
.entry(worktree_id)
|
.entry(worktree_id)
|
||||||
.or_default()
|
.or_default()
|
||||||
@ -1945,6 +1963,7 @@ impl OutlinePanel {
|
|||||||
|
|
||||||
outline_panel
|
outline_panel
|
||||||
.update(&mut cx, |outline_panel, cx| {
|
.update(&mut cx, |outline_panel, cx| {
|
||||||
|
outline_panel.loading_outlines = false;
|
||||||
outline_panel.excerpts = new_excerpts;
|
outline_panel.excerpts = new_excerpts;
|
||||||
outline_panel.collapsed_entries = new_collapsed_entries;
|
outline_panel.collapsed_entries = new_collapsed_entries;
|
||||||
outline_panel.unfolded_dirs = new_unfolded_dirs;
|
outline_panel.unfolded_dirs = new_unfolded_dirs;
|
||||||
@ -2266,10 +2285,34 @@ impl OutlinePanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entries.push((depth, EntryOwned::Entry(entry.clone())));
|
entries.push((depth, EntryOwned::Entry(entry.clone())));
|
||||||
if let FsEntry::File(_, _, buffer_id, entry_excerpts)
|
|
||||||
| FsEntry::ExternalFile(buffer_id, entry_excerpts) = entry
|
let excerpts_to_consider = match entry {
|
||||||
|
FsEntry::File(worktree_id, _, buffer_id, entry_excerpts) => {
|
||||||
|
if is_singleton
|
||||||
|
|| !self
|
||||||
|
.collapsed_entries
|
||||||
|
.contains(&CollapsedEntry::File(*worktree_id, *buffer_id))
|
||||||
{
|
{
|
||||||
if let Some(excerpts) = self.excerpts.get(buffer_id) {
|
Some((*buffer_id, entry_excerpts))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
|
||||||
|
if is_singleton
|
||||||
|
|| !self
|
||||||
|
.collapsed_entries
|
||||||
|
.contains(&CollapsedEntry::ExternalFile(*buffer_id))
|
||||||
|
{
|
||||||
|
Some((*buffer_id, entry_excerpts))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
|
||||||
|
if let Some(excerpts) = self.excerpts.get(&buffer_id) {
|
||||||
for &entry_excerpt in entry_excerpts {
|
for &entry_excerpt in entry_excerpts {
|
||||||
let Some(excerpt) = excerpts.get(&entry_excerpt) else {
|
let Some(excerpt) = excerpts.get(&entry_excerpt) else {
|
||||||
continue;
|
continue;
|
||||||
@ -2278,7 +2321,7 @@ impl OutlinePanel {
|
|||||||
entries.push((
|
entries.push((
|
||||||
excerpt_depth,
|
excerpt_depth,
|
||||||
EntryOwned::Excerpt(
|
EntryOwned::Excerpt(
|
||||||
*buffer_id,
|
buffer_id,
|
||||||
entry_excerpt,
|
entry_excerpt,
|
||||||
excerpt.range.clone(),
|
excerpt.range.clone(),
|
||||||
),
|
),
|
||||||
@ -2290,7 +2333,7 @@ impl OutlinePanel {
|
|||||||
entries.clear();
|
entries.clear();
|
||||||
} else if self
|
} else if self
|
||||||
.collapsed_entries
|
.collapsed_entries
|
||||||
.contains(&CollapsedEntry::Excerpt(*buffer_id, entry_excerpt))
|
.contains(&CollapsedEntry::Excerpt(buffer_id, entry_excerpt))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -2298,7 +2341,7 @@ impl OutlinePanel {
|
|||||||
for outline in excerpt.iter_outlines() {
|
for outline in excerpt.iter_outlines() {
|
||||||
entries.push((
|
entries.push((
|
||||||
outline_base_depth + outline.depth,
|
outline_base_depth + outline.depth,
|
||||||
EntryOwned::Outline(*buffer_id, entry_excerpt, outline.clone()),
|
EntryOwned::Outline(buffer_id, entry_excerpt, outline.clone()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if is_singleton && entries.is_empty() {
|
if is_singleton && entries.is_empty() {
|
||||||
@ -2306,7 +2349,7 @@ impl OutlinePanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
|
if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
|
||||||
entries.push((
|
entries.push((
|
||||||
@ -2384,11 +2427,8 @@ impl OutlinePanel {
|
|||||||
.insert(file_excerpt, excerpt.range.clone());
|
.insert(file_excerpt, excerpt.range.clone());
|
||||||
}
|
}
|
||||||
hash_map::Entry::Vacant(v) => {
|
hash_map::Entry::Vacant(v) => {
|
||||||
if let Some(buffer_snapshot) = self
|
if let Some(buffer_snapshot) =
|
||||||
.project
|
self.buffer_snapshot_for_id(*buffer_id, cx)
|
||||||
.read(cx)
|
|
||||||
.buffer_for_id(*buffer_id)
|
|
||||||
.map(|buffer| buffer.read(cx).snapshot())
|
|
||||||
{
|
{
|
||||||
v.insert((buffer_snapshot, HashMap::default()))
|
v.insert((buffer_snapshot, HashMap::default()))
|
||||||
.1
|
.1
|
||||||
@ -2411,11 +2451,8 @@ impl OutlinePanel {
|
|||||||
o.get_mut().1.insert(*excerpt_id, excerpt.range.clone());
|
o.get_mut().1.insert(*excerpt_id, excerpt.range.clone());
|
||||||
}
|
}
|
||||||
hash_map::Entry::Vacant(v) => {
|
hash_map::Entry::Vacant(v) => {
|
||||||
if let Some(buffer_snapshot) = self
|
if let Some(buffer_snapshot) =
|
||||||
.project
|
self.buffer_snapshot_for_id(*buffer_id, cx)
|
||||||
.read(cx)
|
|
||||||
.buffer_for_id(*buffer_id)
|
|
||||||
.map(|buffer| buffer.read(cx).snapshot())
|
|
||||||
{
|
{
|
||||||
v.insert((buffer_snapshot, HashMap::default()))
|
v.insert((buffer_snapshot, HashMap::default()))
|
||||||
.1
|
.1
|
||||||
@ -2433,6 +2470,60 @@ impl OutlinePanel {
|
|||||||
None => HashMap::default(),
|
None => HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn buffer_snapshot_for_id(
|
||||||
|
&self,
|
||||||
|
buffer_id: BufferId,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<BufferSnapshot> {
|
||||||
|
let editor = self.active_item.as_ref()?.active_editor.upgrade()?;
|
||||||
|
Some(
|
||||||
|
editor
|
||||||
|
.read(cx)
|
||||||
|
.buffer()
|
||||||
|
.read(cx)
|
||||||
|
.buffer(buffer_id)?
|
||||||
|
.read(cx)
|
||||||
|
.snapshot(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn abs_path(&self, entry: &EntryOwned, cx: &AppContext) -> Option<PathBuf> {
|
||||||
|
match entry {
|
||||||
|
EntryOwned::Entry(
|
||||||
|
FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
|
||||||
|
) => self
|
||||||
|
.buffer_snapshot_for_id(*buffer_id, cx)
|
||||||
|
.and_then(|buffer_snapshot| {
|
||||||
|
let file = File::from_dyn(buffer_snapshot.file())?;
|
||||||
|
file.worktree.read(cx).absolutize(&file.path).ok()
|
||||||
|
}),
|
||||||
|
EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => self
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(*worktree_id, cx)?
|
||||||
|
.read(cx)
|
||||||
|
.absolutize(&entry.path)
|
||||||
|
.ok(),
|
||||||
|
EntryOwned::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
|
||||||
|
self.project
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(*worktree_id, cx)
|
||||||
|
.and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
|
||||||
|
}),
|
||||||
|
EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option<PathBuf> {
|
||||||
|
match entry {
|
||||||
|
FsEntry::ExternalFile(buffer_id, _) => self
|
||||||
|
.buffer_snapshot_for_id(*buffer_id, cx)
|
||||||
|
.and_then(|buffer_snapshot| Some(buffer_snapshot.file()?.path().to_path_buf())),
|
||||||
|
FsEntry::Directory(_, entry) => Some(entry.path.to_path_buf()),
|
||||||
|
FsEntry::File(_, entry, ..) => Some(entry.path.to_path_buf()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn back_to_common_visited_parent(
|
fn back_to_common_visited_parent(
|
||||||
@ -2572,12 +2663,37 @@ impl Render for OutlinePanel {
|
|||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
if self.fs_entries.is_empty() {
|
if self.fs_entries.is_empty() {
|
||||||
|
let header = if self.loading_outlines {
|
||||||
|
"Loading outlines"
|
||||||
|
} else {
|
||||||
|
"No outlines available"
|
||||||
|
};
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("empty-outline_panel")
|
.id("empty-outline_panel")
|
||||||
|
.justify_center()
|
||||||
.size_full()
|
.size_full()
|
||||||
.p_4()
|
.p_4()
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.child(Label::new("No editor outlines available"))
|
.child(h_flex().justify_center().child(Label::new(header)))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.pt(Spacing::Small.rems(cx))
|
||||||
|
.justify_center()
|
||||||
|
.child({
|
||||||
|
let keystroke = match self.position(cx) {
|
||||||
|
DockPosition::Left => {
|
||||||
|
cx.keystroke_text_for(&workspace::ToggleLeftDock)
|
||||||
|
}
|
||||||
|
DockPosition::Bottom => {
|
||||||
|
cx.keystroke_text_for(&workspace::ToggleBottomDock)
|
||||||
|
}
|
||||||
|
DockPosition::Right => {
|
||||||
|
cx.keystroke_text_for(&workspace::ToggleRightDock)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Label::new(format!("Toggle this panel with {keystroke}",))
|
||||||
|
}),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("outline-panel")
|
.id("outline-panel")
|
||||||
|
Loading…
Reference in New Issue
Block a user