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:
Kirill Bulatov 2024-07-01 19:36:20 +03:00 committed by GitHub
parent 25ad3185e0
commit 0e60730742
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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")