1mod outline_panel_settings;
2
3use std::{
4 cmp,
5 ops::Range,
6 path::{Path, PathBuf},
7 sync::Arc,
8 time::Duration,
9};
10
11use anyhow::Context;
12use collections::{hash_map, BTreeSet, HashMap, HashSet};
13use db::kvp::KEY_VALUE_STORE;
14use editor::{
15 display_map::ToDisplayPoint,
16 items::{entry_git_aware_label_color, entry_label_color},
17 scroll::ScrollAnchor,
18 DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange,
19};
20use file_icons::FileIcons;
21use gpui::{
22 actions, anchored, deferred, div, px, uniform_list, Action, AnyElement, AppContext,
23 AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, EntityId,
24 EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyContext, Model,
25 MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, SharedString, Stateful,
26 Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext,
27 WeakView, WindowContext,
28};
29use itertools::Itertools;
30use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
31use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
32
33use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings};
34use project::{File, Fs, Item, Project};
35use serde::{Deserialize, Serialize};
36use settings::{Settings, SettingsStore};
37use util::{RangeExt, ResultExt, TryFutureExt};
38use workspace::{
39 dock::{DockPosition, Panel, PanelEvent},
40 item::ItemHandle,
41 ui::{
42 h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, Icon, IconName, IconSize,
43 Label, LabelCommon, ListItem, Selectable, Spacing, StyledTypography,
44 },
45 OpenInTerminal, Workspace,
46};
47use worktree::{Entry, ProjectEntryId, WorktreeId};
48
49actions!(
50 outline_panel,
51 [
52 ExpandSelectedEntry,
53 CollapseSelectedEntry,
54 CollapseAllEntries,
55 CopyPath,
56 CopyRelativePath,
57 RevealInFinder,
58 Open,
59 ToggleFocus,
60 UnfoldDirectory,
61 FoldDirectory,
62 SelectParent,
63 ]
64);
65
66const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
67const UPDATE_DEBOUNCE_MILLIS: u64 = 80;
68
69type Outline = OutlineItem<language::Anchor>;
70
71pub struct OutlinePanel {
72 fs: Arc<dyn Fs>,
73 width: Option<Pixels>,
74 project: Model<Project>,
75 active: bool,
76 scroll_handle: UniformListScrollHandle,
77 context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
78 focus_handle: FocusHandle,
79 pending_serialization: Task<Option<()>>,
80 fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
81 fs_entries: Vec<FsEntry>,
82 collapsed_entries: HashSet<CollapsedEntry>,
83 unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
84 last_visible_range: Range<usize>,
85 selected_entry: Option<EntryOwned>,
86 active_item: Option<ActiveItem>,
87 _subscriptions: Vec<Subscription>,
88 loading_outlines: bool,
89 update_task: Task<()>,
90 outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
91 excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
92 cached_entries_with_depth: Option<Vec<(usize, EntryOwned)>>,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
96enum CollapsedEntry {
97 Dir(WorktreeId, ProjectEntryId),
98 File(WorktreeId, BufferId),
99 ExternalFile(BufferId),
100 Excerpt(BufferId, ExcerptId),
101}
102
103#[derive(Debug)]
104struct Excerpt {
105 range: ExcerptRange<language::Anchor>,
106 outlines: ExcerptOutlines,
107}
108
109impl Excerpt {
110 fn invalidate_outlines(&mut self) {
111 if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines {
112 self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines));
113 }
114 }
115
116 fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {
117 match &self.outlines {
118 ExcerptOutlines::Outlines(outlines) => outlines.iter(),
119 ExcerptOutlines::Invalidated(outlines) => outlines.iter(),
120 ExcerptOutlines::NotFetched => [].iter(),
121 }
122 }
123
124 fn should_fetch_outlines(&self) -> bool {
125 match &self.outlines {
126 ExcerptOutlines::Outlines(_) => false,
127 ExcerptOutlines::Invalidated(_) => true,
128 ExcerptOutlines::NotFetched => true,
129 }
130 }
131}
132
133#[derive(Debug)]
134enum ExcerptOutlines {
135 Outlines(Vec<Outline>),
136 Invalidated(Vec<Outline>),
137 NotFetched,
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
141enum EntryOwned {
142 Entry(FsEntry),
143 FoldedDirs(WorktreeId, Vec<Entry>),
144 Excerpt(BufferId, ExcerptId, ExcerptRange<language::Anchor>),
145 Outline(BufferId, ExcerptId, Outline),
146}
147
148impl EntryOwned {
149 fn to_ref_entry(&self) -> EntryRef<'_> {
150 match self {
151 Self::Entry(entry) => EntryRef::Entry(entry),
152 Self::FoldedDirs(worktree_id, dirs) => EntryRef::FoldedDirs(*worktree_id, dirs),
153 Self::Excerpt(buffer_id, excerpt_id, range) => {
154 EntryRef::Excerpt(*buffer_id, *excerpt_id, range)
155 }
156 Self::Outline(buffer_id, excerpt_id, outline) => {
157 EntryRef::Outline(*buffer_id, *excerpt_id, outline)
158 }
159 }
160 }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164enum EntryRef<'a> {
165 Entry(&'a FsEntry),
166 FoldedDirs(WorktreeId, &'a [Entry]),
167 Excerpt(BufferId, ExcerptId, &'a ExcerptRange<language::Anchor>),
168 Outline(BufferId, ExcerptId, &'a Outline),
169}
170
171impl EntryRef<'_> {
172 fn to_owned_entry(&self) -> EntryOwned {
173 match self {
174 &Self::Entry(entry) => EntryOwned::Entry(entry.clone()),
175 &Self::FoldedDirs(worktree_id, dirs) => {
176 EntryOwned::FoldedDirs(worktree_id, dirs.to_vec())
177 }
178 &Self::Excerpt(buffer_id, excerpt_id, range) => {
179 EntryOwned::Excerpt(buffer_id, excerpt_id, range.clone())
180 }
181 &Self::Outline(buffer_id, excerpt_id, outline) => {
182 EntryOwned::Outline(buffer_id, excerpt_id, outline.clone())
183 }
184 }
185 }
186}
187
188#[derive(Clone, Debug, Eq)]
189enum FsEntry {
190 ExternalFile(BufferId, Vec<ExcerptId>),
191 Directory(WorktreeId, Entry),
192 File(WorktreeId, Entry, BufferId, Vec<ExcerptId>),
193}
194
195impl PartialEq for FsEntry {
196 fn eq(&self, other: &Self) -> bool {
197 match (self, other) {
198 (Self::ExternalFile(id_a, _), Self::ExternalFile(id_b, _)) => id_a == id_b,
199 (Self::Directory(id_a, entry_a), Self::Directory(id_b, entry_b)) => {
200 id_a == id_b && entry_a.id == entry_b.id
201 }
202 (
203 Self::File(worktree_a, entry_a, id_a, ..),
204 Self::File(worktree_b, entry_b, id_b, ..),
205 ) => worktree_a == worktree_b && entry_a.id == entry_b.id && id_a == id_b,
206 _ => false,
207 }
208 }
209}
210
211struct ActiveItem {
212 item_id: EntityId,
213 active_editor: WeakView<Editor>,
214 _editor_subscrpiption: Subscription,
215}
216
217#[derive(Debug)]
218pub enum Event {
219 Focus,
220}
221
222#[derive(Serialize, Deserialize)]
223struct SerializedOutlinePanel {
224 width: Option<Pixels>,
225 active: Option<bool>,
226}
227
228pub fn init_settings(cx: &mut AppContext) {
229 OutlinePanelSettings::register(cx);
230}
231
232pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
233 init_settings(cx);
234 file_icons::init(assets, cx);
235
236 cx.observe_new_views(|workspace: &mut Workspace, _| {
237 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
238 workspace.toggle_panel_focus::<OutlinePanel>(cx);
239 });
240 })
241 .detach();
242}
243
244impl OutlinePanel {
245 pub async fn load(
246 workspace: WeakView<Workspace>,
247 mut cx: AsyncWindowContext,
248 ) -> anyhow::Result<View<Self>> {
249 let serialized_panel = cx
250 .background_executor()
251 .spawn(async move { KEY_VALUE_STORE.read_kvp(OUTLINE_PANEL_KEY) })
252 .await
253 .context("loading outline panel")
254 .log_err()
255 .flatten()
256 .map(|panel| serde_json::from_str::<SerializedOutlinePanel>(&panel))
257 .transpose()
258 .log_err()
259 .flatten();
260
261 workspace.update(&mut cx, |workspace, cx| {
262 let panel = Self::new(workspace, cx);
263 if let Some(serialized_panel) = serialized_panel {
264 panel.update(cx, |panel, cx| {
265 panel.width = serialized_panel.width.map(|px| px.round());
266 panel.active = serialized_panel.active.unwrap_or(false);
267 cx.notify();
268 });
269 }
270 panel
271 })
272 }
273
274 fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
275 let project = workspace.project().clone();
276 let outline_panel = cx.new_view(|cx| {
277 let focus_handle = cx.focus_handle();
278 let focus_subscription = cx.on_focus(&focus_handle, Self::focus_in);
279 let workspace_subscription = cx.subscribe(
280 &workspace
281 .weak_handle()
282 .upgrade()
283 .expect("have a &mut Workspace"),
284 move |outline_panel, workspace, event, cx| {
285 if let workspace::Event::ActiveItemChanged = event {
286 if let Some(new_active_editor) = workspace
287 .read(cx)
288 .active_item(cx)
289 .and_then(|item| item.act_as::<Editor>(cx))
290 {
291 let active_editor_updated = outline_panel
292 .active_item
293 .as_ref()
294 .map_or(true, |active_item| {
295 active_item.item_id != new_active_editor.item_id()
296 });
297 if active_editor_updated {
298 outline_panel.replace_visible_entries(new_active_editor, cx);
299 }
300 } else {
301 outline_panel.clear_previous();
302 cx.notify();
303 }
304 }
305 },
306 );
307
308 let icons_subscription = cx.observe_global::<FileIcons>(|_, cx| {
309 cx.notify();
310 });
311
312 let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx);
313 let settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
314 let new_settings = *OutlinePanelSettings::get_global(cx);
315 if outline_panel_settings != new_settings {
316 outline_panel_settings = new_settings;
317 cx.notify();
318 }
319 });
320
321 let mut outline_panel = Self {
322 active: false,
323 project: project.clone(),
324 fs: workspace.app_state().fs.clone(),
325 scroll_handle: UniformListScrollHandle::new(),
326 focus_handle,
327 fs_entries: Vec::new(),
328 fs_entries_depth: HashMap::default(),
329 collapsed_entries: HashSet::default(),
330 unfolded_dirs: HashMap::default(),
331 selected_entry: None,
332 context_menu: None,
333 width: None,
334 active_item: None,
335 pending_serialization: Task::ready(None),
336 loading_outlines: false,
337 update_task: Task::ready(()),
338 outline_fetch_tasks: HashMap::default(),
339 excerpts: HashMap::default(),
340 last_visible_range: 0..0,
341 cached_entries_with_depth: None,
342 _subscriptions: vec![
343 settings_subscription,
344 icons_subscription,
345 focus_subscription,
346 workspace_subscription,
347 ],
348 };
349 if let Some(editor) = workspace
350 .active_item(cx)
351 .and_then(|item| item.act_as::<Editor>(cx))
352 {
353 outline_panel.replace_visible_entries(editor, cx);
354 }
355 outline_panel
356 });
357
358 outline_panel
359 }
360
361 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
362 let width = self.width;
363 let active = Some(self.active);
364 self.pending_serialization = cx.background_executor().spawn(
365 async move {
366 KEY_VALUE_STORE
367 .write_kvp(
368 OUTLINE_PANEL_KEY.into(),
369 serde_json::to_string(&SerializedOutlinePanel { width, active })?,
370 )
371 .await?;
372 anyhow::Ok(())
373 }
374 .log_err(),
375 );
376 }
377
378 fn dispatch_context(&self, _: &ViewContext<Self>) -> KeyContext {
379 let mut dispatch_context = KeyContext::new_with_defaults();
380 dispatch_context.add("OutlinePanel");
381 dispatch_context.add("menu");
382 dispatch_context
383 }
384
385 fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
386 let Some(editor) = self
387 .active_item
388 .as_ref()
389 .and_then(|item| item.active_editor.upgrade())
390 else {
391 return;
392 };
393 if let Some(EntryOwned::FoldedDirs(worktree_id, entries)) = &self.selected_entry {
394 self.unfolded_dirs
395 .entry(*worktree_id)
396 .or_default()
397 .extend(entries.iter().map(|entry| entry.id));
398 self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
399 }
400 }
401
402 fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext<Self>) {
403 let Some(editor) = self
404 .active_item
405 .as_ref()
406 .and_then(|item| item.active_editor.upgrade())
407 else {
408 return;
409 };
410
411 let (worktree_id, entry) = match &self.selected_entry {
412 Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, entry))) => {
413 (worktree_id, Some(entry))
414 }
415 Some(EntryOwned::FoldedDirs(worktree_id, entries)) => (worktree_id, entries.last()),
416 _ => return,
417 };
418 let Some(entry) = entry else {
419 return;
420 };
421 let unfolded_dirs = self.unfolded_dirs.get_mut(worktree_id);
422 let worktree = self
423 .project
424 .read(cx)
425 .worktree_for_id(*worktree_id, cx)
426 .map(|w| w.read(cx).snapshot());
427 let Some((worktree, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
428 return;
429 };
430
431 unfolded_dirs.remove(&entry.id);
432 let mut parent = entry.path.parent();
433 while let Some(parent_path) = parent {
434 let removed = worktree.entry_for_path(parent_path).map_or(false, |entry| {
435 if worktree.root_entry().map(|entry| entry.id) == Some(entry.id) {
436 false
437 } else {
438 unfolded_dirs.remove(&entry.id)
439 }
440 });
441
442 if removed {
443 parent = parent_path.parent();
444 } else {
445 break;
446 }
447 }
448 for child_dir in worktree
449 .child_entries(&entry.path)
450 .filter(|entry| entry.is_dir())
451 {
452 let removed = unfolded_dirs.remove(&child_dir.id);
453 if !removed {
454 break;
455 }
456 }
457
458 self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
459 }
460
461 fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
462 if let Some(selected_entry) = self.selected_entry.clone() {
463 self.open_entry(&selected_entry, cx);
464 }
465 }
466
467 fn open_entry(&mut self, entry: &EntryOwned, cx: &mut ViewContext<OutlinePanel>) {
468 let Some(active_editor) = self
469 .active_item
470 .as_ref()
471 .and_then(|item| item.active_editor.upgrade())
472 else {
473 return;
474 };
475 let active_multi_buffer = active_editor.read(cx).buffer().clone();
476 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
477 let offset_from_top = if active_multi_buffer.read(cx).is_singleton() {
478 Point::default()
479 } else {
480 Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
481 };
482
483 self.toggle_expanded(entry, cx);
484 match entry {
485 EntryOwned::FoldedDirs(..) | EntryOwned::Entry(FsEntry::Directory(..)) => {}
486 EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
487 let scroll_target = multi_buffer_snapshot.excerpts().find_map(
488 |(excerpt_id, buffer_snapshot, excerpt_range)| {
489 if &buffer_snapshot.remote_id() == buffer_id {
490 multi_buffer_snapshot
491 .anchor_in_excerpt(excerpt_id, excerpt_range.context.start)
492 } else {
493 None
494 }
495 },
496 );
497 if let Some(anchor) = scroll_target {
498 self.selected_entry = Some(entry.clone());
499 active_editor.update(cx, |editor, cx| {
500 editor.set_scroll_anchor(
501 ScrollAnchor {
502 offset: offset_from_top,
503 anchor,
504 },
505 cx,
506 );
507 })
508 }
509 }
510 EntryOwned::Entry(FsEntry::File(_, file_entry, ..)) => {
511 let scroll_target = self
512 .project
513 .update(cx, |project, cx| {
514 project
515 .path_for_entry(file_entry.id, cx)
516 .and_then(|path| project.get_open_buffer(&path, cx))
517 })
518 .map(|buffer| {
519 active_multi_buffer
520 .read(cx)
521 .excerpts_for_buffer(&buffer, cx)
522 })
523 .and_then(|excerpts| {
524 let (excerpt_id, excerpt_range) = excerpts.first()?;
525 multi_buffer_snapshot
526 .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
527 });
528 if let Some(anchor) = scroll_target {
529 self.selected_entry = Some(entry.clone());
530 active_editor.update(cx, |editor, cx| {
531 editor.set_scroll_anchor(
532 ScrollAnchor {
533 offset: offset_from_top,
534 anchor,
535 },
536 cx,
537 );
538 })
539 }
540 }
541 EntryOwned::Outline(_, excerpt_id, outline) => {
542 let scroll_target = multi_buffer_snapshot
543 .anchor_in_excerpt(*excerpt_id, outline.range.start)
544 .or_else(|| {
545 multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, outline.range.end)
546 });
547 if let Some(anchor) = scroll_target {
548 self.selected_entry = Some(entry.clone());
549 active_editor.update(cx, |editor, cx| {
550 editor.set_scroll_anchor(
551 ScrollAnchor {
552 offset: Point::default(),
553 anchor,
554 },
555 cx,
556 );
557 })
558 }
559 }
560 EntryOwned::Excerpt(_, excerpt_id, excerpt_range) => {
561 let scroll_target = multi_buffer_snapshot
562 .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start);
563 if let Some(anchor) = scroll_target {
564 self.selected_entry = Some(entry.clone());
565 active_editor.update(cx, |editor, cx| {
566 editor.set_scroll_anchor(
567 ScrollAnchor {
568 offset: Point::default(),
569 anchor,
570 },
571 cx,
572 );
573 })
574 }
575 }
576 }
577 }
578
579 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
580 if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
581 self.entries_with_depths(cx)
582 .iter()
583 .map(|(_, entry)| entry)
584 .skip_while(|entry| entry != &&selected_entry)
585 .skip(1)
586 .next()
587 .cloned()
588 }) {
589 self.selected_entry = Some(entry_to_select);
590 self.autoscroll(cx);
591 cx.notify();
592 } else {
593 self.select_first(&SelectFirst {}, cx)
594 }
595 }
596
597 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
598 if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
599 self.entries_with_depths(cx)
600 .iter()
601 .rev()
602 .map(|(_, entry)| entry)
603 .skip_while(|entry| entry != &&selected_entry)
604 .skip(1)
605 .next()
606 .cloned()
607 }) {
608 self.selected_entry = Some(entry_to_select);
609 self.autoscroll(cx);
610 cx.notify();
611 } else {
612 self.select_first(&SelectFirst {}, cx)
613 }
614 }
615
616 fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext<Self>) {
617 if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
618 let mut previous_entries = self
619 .entries_with_depths(cx)
620 .iter()
621 .rev()
622 .map(|(_, entry)| entry)
623 .skip_while(|entry| entry != &&selected_entry)
624 .skip(1);
625 match &selected_entry {
626 EntryOwned::Entry(fs_entry) => match fs_entry {
627 FsEntry::ExternalFile(..) => None,
628 FsEntry::File(worktree_id, entry, ..)
629 | FsEntry::Directory(worktree_id, entry) => {
630 entry.path.parent().and_then(|parent_path| {
631 previous_entries.find(|entry| match entry {
632 EntryOwned::Entry(FsEntry::Directory(
633 dir_worktree_id,
634 dir_entry,
635 )) => {
636 dir_worktree_id == worktree_id
637 && dir_entry.path.as_ref() == parent_path
638 }
639 EntryOwned::FoldedDirs(dirs_worktree_id, dirs) => {
640 dirs_worktree_id == worktree_id
641 && dirs
642 .first()
643 .map_or(false, |dir| dir.path.as_ref() == parent_path)
644 }
645 _ => false,
646 })
647 })
648 }
649 },
650 EntryOwned::FoldedDirs(worktree_id, entries) => entries
651 .first()
652 .and_then(|entry| entry.path.parent())
653 .and_then(|parent_path| {
654 previous_entries.find(|entry| {
655 if let EntryOwned::Entry(FsEntry::Directory(
656 dir_worktree_id,
657 dir_entry,
658 )) = entry
659 {
660 dir_worktree_id == worktree_id
661 && dir_entry.path.as_ref() == parent_path
662 } else {
663 false
664 }
665 })
666 }),
667 EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) => {
668 previous_entries.find(|entry| match entry {
669 EntryOwned::Entry(FsEntry::File(_, _, file_buffer_id, file_excerpts)) => {
670 file_buffer_id == excerpt_buffer_id
671 && file_excerpts.contains(&excerpt_id)
672 }
673 EntryOwned::Entry(FsEntry::ExternalFile(file_buffer_id, file_excerpts)) => {
674 file_buffer_id == excerpt_buffer_id
675 && file_excerpts.contains(&excerpt_id)
676 }
677 _ => false,
678 })
679 }
680 EntryOwned::Outline(outline_buffer_id, outline_excerpt_id, _) => previous_entries
681 .find(|entry| {
682 if let EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) = entry {
683 outline_buffer_id == excerpt_buffer_id
684 && outline_excerpt_id == excerpt_id
685 } else {
686 false
687 }
688 }),
689 }
690 }) {
691 self.selected_entry = Some(entry_to_select.clone());
692 self.autoscroll(cx);
693 cx.notify();
694 } else {
695 self.select_first(&SelectFirst {}, cx);
696 }
697 }
698
699 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
700 if let Some((_, first_entry)) = self.entries_with_depths(cx).iter().next() {
701 self.selected_entry = Some(first_entry.clone());
702 self.autoscroll(cx);
703 cx.notify();
704 }
705 }
706
707 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
708 if let Some(new_selection) = self
709 .entries_with_depths(cx)
710 .iter()
711 .rev()
712 .map(|(_, entry)| entry)
713 .next()
714 {
715 self.selected_entry = Some(new_selection.clone());
716 self.autoscroll(cx);
717 cx.notify();
718 }
719 }
720
721 fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
722 if let Some(selected_entry) = self.selected_entry.clone() {
723 let index = self
724 .entries_with_depths(cx)
725 .iter()
726 .position(|(_, entry)| entry == &selected_entry);
727 if let Some(index) = index {
728 self.scroll_handle.scroll_to_item(index);
729 cx.notify();
730 }
731 }
732 }
733
734 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
735 if !self.focus_handle.contains_focused(cx) {
736 cx.emit(Event::Focus);
737 }
738 }
739
740 fn deploy_context_menu(
741 &mut self,
742 position: Point<Pixels>,
743 entry: EntryRef<'_>,
744 cx: &mut ViewContext<Self>,
745 ) {
746 self.selected_entry = Some(entry.to_owned_entry());
747 let is_root = match entry {
748 EntryRef::Entry(FsEntry::File(worktree_id, entry, ..))
749 | EntryRef::Entry(FsEntry::Directory(worktree_id, entry)) => self
750 .project
751 .read(cx)
752 .worktree_for_id(*worktree_id, cx)
753 .map(|worktree| {
754 worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
755 })
756 .unwrap_or(false),
757 EntryRef::FoldedDirs(worktree_id, entries) => entries
758 .first()
759 .and_then(|entry| {
760 self.project
761 .read(cx)
762 .worktree_for_id(worktree_id, cx)
763 .map(|worktree| {
764 worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
765 })
766 })
767 .unwrap_or(false),
768 EntryRef::Entry(FsEntry::ExternalFile(..)) => false,
769 EntryRef::Excerpt(..) => {
770 cx.notify();
771 return;
772 }
773 EntryRef::Outline(..) => {
774 cx.notify();
775 return;
776 }
777 };
778 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
779 let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(entry);
780 let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(entry);
781
782 let context_menu = ContextMenu::build(cx, |menu, _| {
783 menu.context(self.focus_handle.clone())
784 .action("Reveal in Finder", Box::new(RevealInFinder))
785 .action("Open in Terminal", Box::new(OpenInTerminal))
786 .when(is_unfoldable, |menu| {
787 menu.action("Unfold Directory", Box::new(UnfoldDirectory))
788 })
789 .when(is_foldable, |menu| {
790 menu.action("Fold Directory", Box::new(FoldDirectory))
791 })
792 .separator()
793 .action("Copy Path", Box::new(CopyPath))
794 .action("Copy Relative Path", Box::new(CopyRelativePath))
795 });
796 cx.focus_view(&context_menu);
797 let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
798 outline_panel.context_menu.take();
799 cx.notify();
800 });
801 self.context_menu = Some((context_menu, position, subscription));
802 cx.notify();
803 }
804
805 fn is_unfoldable(&self, entry: EntryRef) -> bool {
806 matches!(entry, EntryRef::FoldedDirs(..))
807 }
808
809 fn is_foldable(&self, entry: EntryRef) -> bool {
810 let (directory_worktree, directory_entry) = match entry {
811 EntryRef::Entry(FsEntry::Directory(directory_worktree, directory_entry)) => {
812 (*directory_worktree, Some(directory_entry))
813 }
814 EntryRef::FoldedDirs(directory_worktree, entries) => {
815 (directory_worktree, entries.last())
816 }
817 _ => return false,
818 };
819 let Some(directory_entry) = directory_entry else {
820 return false;
821 };
822
823 if self
824 .unfolded_dirs
825 .get(&directory_worktree)
826 .map_or(false, |unfolded_dirs| {
827 unfolded_dirs.contains(&directory_entry.id)
828 })
829 {
830 return true;
831 }
832
833 let child_entries = self
834 .fs_entries
835 .iter()
836 .skip_while(|entry| {
837 if let FsEntry::Directory(worktree_id, entry) = entry {
838 worktree_id != &directory_worktree || entry.id != directory_entry.id
839 } else {
840 true
841 }
842 })
843 .skip(1)
844 .filter(|next_entry| match next_entry {
845 FsEntry::ExternalFile(..) => false,
846 FsEntry::Directory(worktree_id, entry) | FsEntry::File(worktree_id, entry, ..) => {
847 worktree_id == &directory_worktree
848 && entry.path.parent() == Some(directory_entry.path.as_ref())
849 }
850 })
851 .collect::<Vec<_>>();
852
853 child_entries.len() == 1 && matches!(child_entries.first(), Some(FsEntry::Directory(..)))
854 }
855
856 fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
857 let Some(editor) = self
858 .active_item
859 .as_ref()
860 .and_then(|item| item.active_editor.upgrade())
861 else {
862 return;
863 };
864
865 let entry_to_expand = match &self.selected_entry {
866 Some(EntryOwned::FoldedDirs(worktree_id, dir_entries)) => dir_entries
867 .last()
868 .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)),
869 Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry))) => {
870 Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
871 }
872 Some(EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => {
873 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
874 }
875 Some(EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => {
876 Some(CollapsedEntry::ExternalFile(*buffer_id))
877 }
878 Some(EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
879 Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
880 }
881 None | Some(EntryOwned::Outline(..)) => None,
882 };
883 let Some(collapsed_entry) = entry_to_expand else {
884 return;
885 };
886 let expanded = self.collapsed_entries.remove(&collapsed_entry);
887 if expanded {
888 if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
889 self.project.update(cx, |project, cx| {
890 project.expand_entry(worktree_id, dir_entry_id, cx);
891 });
892 }
893 self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
894 } else {
895 self.select_next(&SelectNext, cx)
896 }
897 }
898
899 fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
900 let Some(editor) = self
901 .active_item
902 .as_ref()
903 .and_then(|item| item.active_editor.upgrade())
904 else {
905 return;
906 };
907 match &self.selected_entry {
908 Some(
909 dir_entry @ EntryOwned::Entry(FsEntry::Directory(worktree_id, selected_dir_entry)),
910 ) => {
911 self.collapsed_entries
912 .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id));
913 self.update_fs_entries(
914 &editor,
915 HashSet::default(),
916 Some(dir_entry.clone()),
917 None,
918 false,
919 cx,
920 );
921 }
922 Some(file_entry @ EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => {
923 self.collapsed_entries
924 .insert(CollapsedEntry::File(*worktree_id, *buffer_id));
925 self.update_fs_entries(
926 &editor,
927 HashSet::default(),
928 Some(file_entry.clone()),
929 None,
930 false,
931 cx,
932 );
933 }
934 Some(file_entry @ EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => {
935 self.collapsed_entries
936 .insert(CollapsedEntry::ExternalFile(*buffer_id));
937 self.update_fs_entries(
938 &editor,
939 HashSet::default(),
940 Some(file_entry.clone()),
941 None,
942 false,
943 cx,
944 );
945 }
946 Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => {
947 if let Some(dir_entry) = dir_entries.last() {
948 if self
949 .collapsed_entries
950 .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
951 {
952 self.update_fs_entries(
953 &editor,
954 HashSet::default(),
955 Some(dirs_entry.clone()),
956 None,
957 false,
958 cx,
959 );
960 }
961 }
962 }
963 Some(excerpt_entry @ EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
964 if self
965 .collapsed_entries
966 .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
967 {
968 self.update_fs_entries(
969 &editor,
970 HashSet::default(),
971 Some(excerpt_entry.clone()),
972 None,
973 false,
974 cx,
975 );
976 }
977 }
978 None | Some(EntryOwned::Outline(..)) => {}
979 }
980 }
981
982 pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
983 let Some(editor) = self
984 .active_item
985 .as_ref()
986 .and_then(|item| item.active_editor.upgrade())
987 else {
988 return;
989 };
990
991 let new_entries = self
992 .entries_with_depths(cx)
993 .iter()
994 .flat_map(|(_, entry)| match entry {
995 EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => {
996 Some(CollapsedEntry::Dir(*worktree_id, entry.id))
997 }
998 EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
999 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1000 }
1001 EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
1002 Some(CollapsedEntry::ExternalFile(*buffer_id))
1003 }
1004 EntryOwned::FoldedDirs(worktree_id, entries) => {
1005 Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id))
1006 }
1007 EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
1008 Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1009 }
1010 EntryOwned::Outline(..) => None,
1011 })
1012 .collect::<Vec<_>>();
1013 self.collapsed_entries.extend(new_entries);
1014 self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
1015 }
1016
1017 fn toggle_expanded(&mut self, entry: &EntryOwned, cx: &mut ViewContext<Self>) {
1018 let Some(editor) = self
1019 .active_item
1020 .as_ref()
1021 .and_then(|item| item.active_editor.upgrade())
1022 else {
1023 return;
1024 };
1025
1026 match entry {
1027 EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry)) => {
1028 let entry_id = dir_entry.id;
1029 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1030 if self.collapsed_entries.remove(&collapsed_entry) {
1031 self.project
1032 .update(cx, |project, cx| {
1033 project.expand_entry(*worktree_id, entry_id, cx)
1034 })
1035 .unwrap_or_else(|| Task::ready(Ok(())))
1036 .detach_and_log_err(cx);
1037 } else {
1038 self.collapsed_entries.insert(collapsed_entry);
1039 }
1040 }
1041 EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1042 let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1043 if !self.collapsed_entries.remove(&collapsed_entry) {
1044 self.collapsed_entries.insert(collapsed_entry);
1045 }
1046 }
1047 EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
1048 let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id);
1049 if !self.collapsed_entries.remove(&collapsed_entry) {
1050 self.collapsed_entries.insert(collapsed_entry);
1051 }
1052 }
1053 EntryOwned::FoldedDirs(worktree_id, dir_entries) => {
1054 if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
1055 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1056 if self.collapsed_entries.remove(&collapsed_entry) {
1057 self.project
1058 .update(cx, |project, cx| {
1059 project.expand_entry(*worktree_id, entry_id, cx)
1060 })
1061 .unwrap_or_else(|| Task::ready(Ok(())))
1062 .detach_and_log_err(cx);
1063 } else {
1064 self.collapsed_entries.insert(collapsed_entry);
1065 }
1066 }
1067 }
1068 EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
1069 let collapsed_entry = CollapsedEntry::Excerpt(*buffer_id, *excerpt_id);
1070 if !self.collapsed_entries.remove(&collapsed_entry) {
1071 self.collapsed_entries.insert(collapsed_entry);
1072 }
1073 }
1074 EntryOwned::Outline(..) => return,
1075 }
1076
1077 self.update_fs_entries(
1078 &editor,
1079 HashSet::default(),
1080 Some(entry.clone()),
1081 None,
1082 false,
1083 cx,
1084 );
1085 }
1086
1087 fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
1088 if let Some(clipboard_text) = self
1089 .selected_entry
1090 .as_ref()
1091 .and_then(|entry| self.abs_path(&entry, cx))
1092 .map(|p| p.to_string_lossy().to_string())
1093 {
1094 cx.write_to_clipboard(ClipboardItem::new(clipboard_text));
1095 }
1096 }
1097
1098 fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1099 if let Some(clipboard_text) = self
1100 .selected_entry
1101 .as_ref()
1102 .and_then(|entry| match entry {
1103 EntryOwned::Entry(entry) => self.relative_path(&entry, cx),
1104 EntryOwned::FoldedDirs(_, dirs) => {
1105 dirs.last().map(|entry| entry.path.to_path_buf())
1106 }
1107 EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
1108 })
1109 .map(|p| p.to_string_lossy().to_string())
1110 {
1111 cx.write_to_clipboard(ClipboardItem::new(clipboard_text));
1112 }
1113 }
1114
1115 fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
1116 if let Some(abs_path) = self
1117 .selected_entry
1118 .as_ref()
1119 .and_then(|entry| self.abs_path(&entry, cx))
1120 {
1121 cx.reveal_path(&abs_path);
1122 }
1123 }
1124
1125 fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
1126 let selected_entry = self.selected_entry.as_ref();
1127 let abs_path = selected_entry.and_then(|entry| self.abs_path(&entry, cx));
1128 let working_directory = if let (
1129 Some(abs_path),
1130 Some(EntryOwned::Entry(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1131 ) = (&abs_path, selected_entry)
1132 {
1133 abs_path.parent().map(|p| p.to_owned())
1134 } else {
1135 abs_path
1136 };
1137
1138 if let Some(working_directory) = working_directory {
1139 cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
1140 }
1141 }
1142
1143 fn reveal_entry_for_selection(
1144 &mut self,
1145 editor: &View<Editor>,
1146 cx: &mut ViewContext<'_, Self>,
1147 ) {
1148 if !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
1149 return;
1150 }
1151 let Some(entry_with_selection) = self.location_for_editor_selection(editor, cx) else {
1152 self.selected_entry = None;
1153 cx.notify();
1154 return;
1155 };
1156 let related_buffer_entry = match entry_with_selection {
1157 EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1158 let project = self.project.read(cx);
1159 let entry_id = project
1160 .buffer_for_id(buffer_id)
1161 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1162 project
1163 .worktree_for_id(worktree_id, cx)
1164 .zip(entry_id)
1165 .and_then(|(worktree, entry_id)| {
1166 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1167 Some((worktree, entry))
1168 })
1169 }
1170 EntryOwned::Outline(buffer_id, excerpt_id, _)
1171 | EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
1172 self.collapsed_entries
1173 .remove(&CollapsedEntry::ExternalFile(buffer_id));
1174 self.collapsed_entries
1175 .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1176 let project = self.project.read(cx);
1177 let entry_id = project
1178 .buffer_for_id(buffer_id)
1179 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1180
1181 entry_id.and_then(|entry_id| {
1182 project
1183 .worktree_for_entry(entry_id, cx)
1184 .and_then(|worktree| {
1185 let worktree_id = worktree.read(cx).id();
1186 self.collapsed_entries
1187 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1188 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1189 Some((worktree, entry))
1190 })
1191 })
1192 }
1193 EntryOwned::Entry(FsEntry::ExternalFile(..)) => None,
1194 _ => return,
1195 };
1196 if let Some((worktree, buffer_entry)) = related_buffer_entry {
1197 let worktree_id = worktree.read(cx).id();
1198 let mut dirs_to_expand = Vec::new();
1199 {
1200 let mut traversal = worktree.read(cx).traverse_from_path(
1201 true,
1202 true,
1203 true,
1204 buffer_entry.path.as_ref(),
1205 );
1206 let mut current_entry = buffer_entry;
1207 loop {
1208 if current_entry.is_dir() {
1209 if self
1210 .collapsed_entries
1211 .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
1212 {
1213 dirs_to_expand.push(current_entry.id);
1214 }
1215 }
1216
1217 if traversal.back_to_parent() {
1218 if let Some(parent_entry) = traversal.entry() {
1219 current_entry = parent_entry.clone();
1220 continue;
1221 }
1222 }
1223 break;
1224 }
1225 }
1226 for dir_to_expand in dirs_to_expand {
1227 self.project
1228 .update(cx, |project, cx| {
1229 project.expand_entry(worktree_id, dir_to_expand, cx)
1230 })
1231 .unwrap_or_else(|| Task::ready(Ok(())))
1232 .detach_and_log_err(cx)
1233 }
1234 }
1235
1236 self.update_fs_entries(
1237 &editor,
1238 HashSet::default(),
1239 Some(entry_with_selection),
1240 None,
1241 false,
1242 cx,
1243 );
1244 }
1245
1246 fn render_excerpt(
1247 &self,
1248 buffer_id: BufferId,
1249 excerpt_id: ExcerptId,
1250 range: &ExcerptRange<language::Anchor>,
1251 depth: usize,
1252 cx: &mut ViewContext<OutlinePanel>,
1253 ) -> Option<Stateful<Div>> {
1254 let item_id = ElementId::from(excerpt_id.to_proto() as usize);
1255 let is_active = match &self.selected_entry {
1256 Some(EntryOwned::Excerpt(selected_buffer_id, selected_excerpt_id, _)) => {
1257 selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id
1258 }
1259 _ => false,
1260 };
1261 let has_outlines = self
1262 .excerpts
1263 .get(&buffer_id)
1264 .and_then(|excerpts| match &excerpts.get(&excerpt_id)?.outlines {
1265 ExcerptOutlines::Outlines(outlines) => Some(outlines),
1266 ExcerptOutlines::Invalidated(outlines) => Some(outlines),
1267 ExcerptOutlines::NotFetched => None,
1268 })
1269 .map_or(false, |outlines| !outlines.is_empty());
1270 let is_expanded = !self
1271 .collapsed_entries
1272 .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1273 let color = entry_git_aware_label_color(None, false, is_active);
1274 let icon = if has_outlines {
1275 FileIcons::get_chevron_icon(is_expanded, cx)
1276 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1277 } else {
1278 None
1279 }
1280 .unwrap_or_else(empty_icon);
1281
1282 let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
1283 let excerpt_range = range.context.to_point(&buffer_snapshot);
1284 let label_element = Label::new(format!(
1285 "Lines {}-{}",
1286 excerpt_range.start.row + 1,
1287 excerpt_range.end.row + 1,
1288 ))
1289 .single_line()
1290 .color(color)
1291 .into_any_element();
1292
1293 Some(self.entry_element(
1294 EntryRef::Excerpt(buffer_id, excerpt_id, range),
1295 item_id,
1296 depth,
1297 Some(icon),
1298 is_active,
1299 label_element,
1300 cx,
1301 ))
1302 }
1303
1304 fn render_outline(
1305 &self,
1306 buffer_id: BufferId,
1307 excerpt_id: ExcerptId,
1308 rendered_outline: &Outline,
1309 depth: usize,
1310 cx: &mut ViewContext<Self>,
1311 ) -> Stateful<Div> {
1312 let (item_id, label_element) = (
1313 ElementId::from(SharedString::from(format!(
1314 "{buffer_id:?}|{excerpt_id:?}{:?}|{:?}",
1315 rendered_outline.range, &rendered_outline.text,
1316 ))),
1317 language::render_item(&rendered_outline, None, cx).into_any_element(),
1318 );
1319 let is_active = match &self.selected_entry {
1320 Some(EntryOwned::Outline(selected_buffer_id, selected_excerpt_id, selected_entry)) => {
1321 selected_buffer_id == &buffer_id
1322 && selected_excerpt_id == &excerpt_id
1323 && selected_entry == rendered_outline
1324 }
1325 _ => false,
1326 };
1327 let icon = if self.is_singleton_active(cx) {
1328 None
1329 } else {
1330 Some(empty_icon())
1331 };
1332 self.entry_element(
1333 EntryRef::Outline(buffer_id, excerpt_id, rendered_outline),
1334 item_id,
1335 depth,
1336 icon,
1337 is_active,
1338 label_element,
1339 cx,
1340 )
1341 }
1342
1343 fn render_entry(
1344 &self,
1345 rendered_entry: &FsEntry,
1346 depth: usize,
1347 cx: &mut ViewContext<Self>,
1348 ) -> Stateful<Div> {
1349 let settings = OutlinePanelSettings::get_global(cx);
1350 let is_active = match &self.selected_entry {
1351 Some(EntryOwned::Entry(selected_entry)) => selected_entry == rendered_entry,
1352 _ => false,
1353 };
1354 let (item_id, label_element, icon) = match rendered_entry {
1355 FsEntry::File(worktree_id, entry, ..) => {
1356 let name = self.entry_name(worktree_id, entry, cx);
1357 let color =
1358 entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1359 let icon = if settings.file_icons {
1360 FileIcons::get_icon(&entry.path, cx)
1361 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1362 } else {
1363 None
1364 };
1365 (
1366 ElementId::from(entry.id.to_proto() as usize),
1367 Label::new(name)
1368 .single_line()
1369 .color(color)
1370 .into_any_element(),
1371 icon.unwrap_or_else(empty_icon),
1372 )
1373 }
1374 FsEntry::Directory(worktree_id, entry) => {
1375 let name = self.entry_name(worktree_id, entry, cx);
1376
1377 let is_expanded = !self
1378 .collapsed_entries
1379 .contains(&CollapsedEntry::Dir(*worktree_id, entry.id));
1380 let color =
1381 entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1382 let icon = if settings.folder_icons {
1383 FileIcons::get_folder_icon(is_expanded, cx)
1384 } else {
1385 FileIcons::get_chevron_icon(is_expanded, cx)
1386 }
1387 .map(Icon::from_path)
1388 .map(|icon| icon.color(color).into_any_element());
1389 (
1390 ElementId::from(entry.id.to_proto() as usize),
1391 Label::new(name)
1392 .single_line()
1393 .color(color)
1394 .into_any_element(),
1395 icon.unwrap_or_else(empty_icon),
1396 )
1397 }
1398 FsEntry::ExternalFile(buffer_id, ..) => {
1399 let color = entry_label_color(is_active);
1400 let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) {
1401 Some(buffer_snapshot) => match buffer_snapshot.file() {
1402 Some(file) => {
1403 let path = file.path();
1404 let icon = if settings.file_icons {
1405 FileIcons::get_icon(path.as_ref(), cx)
1406 } else {
1407 None
1408 }
1409 .map(Icon::from_path)
1410 .map(|icon| icon.color(color).into_any_element());
1411 (icon, file_name(path.as_ref()))
1412 }
1413 None => (None, "Untitled".to_string()),
1414 },
1415 None => (None, "Unknown buffer".to_string()),
1416 };
1417 (
1418 ElementId::from(buffer_id.to_proto() as usize),
1419 Label::new(name)
1420 .single_line()
1421 .color(color)
1422 .into_any_element(),
1423 icon.unwrap_or_else(empty_icon),
1424 )
1425 }
1426 };
1427
1428 self.entry_element(
1429 EntryRef::Entry(rendered_entry),
1430 item_id,
1431 depth,
1432 Some(icon),
1433 is_active,
1434 label_element,
1435 cx,
1436 )
1437 }
1438
1439 fn render_folded_dirs(
1440 &self,
1441 worktree_id: WorktreeId,
1442 dir_entries: &[Entry],
1443 depth: usize,
1444 cx: &mut ViewContext<OutlinePanel>,
1445 ) -> Stateful<Div> {
1446 let settings = OutlinePanelSettings::get_global(cx);
1447 let is_active = match &self.selected_entry {
1448 Some(EntryOwned::FoldedDirs(selected_worktree_id, selected_entries)) => {
1449 selected_worktree_id == &worktree_id && selected_entries == dir_entries
1450 }
1451 _ => false,
1452 };
1453 let (item_id, label_element, icon) = {
1454 let name = dir_entries.iter().fold(String::new(), |mut name, entry| {
1455 if !name.is_empty() {
1456 name.push(std::path::MAIN_SEPARATOR)
1457 }
1458 name.push_str(&self.entry_name(&worktree_id, entry, cx));
1459 name
1460 });
1461
1462 let is_expanded = dir_entries.iter().all(|dir| {
1463 !self
1464 .collapsed_entries
1465 .contains(&CollapsedEntry::Dir(worktree_id, dir.id))
1466 });
1467 let is_ignored = dir_entries.iter().any(|entry| entry.is_ignored);
1468 let git_status = dir_entries.first().and_then(|entry| entry.git_status);
1469 let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
1470 let icon = if settings.folder_icons {
1471 FileIcons::get_folder_icon(is_expanded, cx)
1472 } else {
1473 FileIcons::get_chevron_icon(is_expanded, cx)
1474 }
1475 .map(Icon::from_path)
1476 .map(|icon| icon.color(color).into_any_element());
1477 (
1478 ElementId::from(
1479 dir_entries
1480 .last()
1481 .map(|entry| entry.id.to_proto())
1482 .unwrap_or_else(|| worktree_id.to_proto()) as usize,
1483 ),
1484 Label::new(name)
1485 .single_line()
1486 .color(color)
1487 .into_any_element(),
1488 icon.unwrap_or_else(empty_icon),
1489 )
1490 };
1491
1492 self.entry_element(
1493 EntryRef::FoldedDirs(worktree_id, dir_entries),
1494 item_id,
1495 depth,
1496 Some(icon),
1497 is_active,
1498 label_element,
1499 cx,
1500 )
1501 }
1502
1503 #[allow(clippy::too_many_arguments)]
1504 fn entry_element(
1505 &self,
1506 rendered_entry: EntryRef<'_>,
1507 item_id: ElementId,
1508 depth: usize,
1509 icon_element: Option<AnyElement>,
1510 is_active: bool,
1511 label_element: gpui::AnyElement,
1512 cx: &mut ViewContext<OutlinePanel>,
1513 ) -> Stateful<Div> {
1514 let settings = OutlinePanelSettings::get_global(cx);
1515 let rendered_entry = rendered_entry.to_owned_entry();
1516 div()
1517 .text_ui(cx)
1518 .id(item_id.clone())
1519 .child(
1520 ListItem::new(item_id)
1521 .indent_level(depth)
1522 .indent_step_size(px(settings.indent_size))
1523 .selected(is_active)
1524 .when_some(icon_element, |list_item, icon_element| {
1525 list_item.child(h_flex().child(icon_element))
1526 })
1527 .child(h_flex().h_6().child(label_element).ml_1())
1528 .on_click({
1529 let clicked_entry = rendered_entry.clone();
1530 cx.listener(move |outline_panel, event: &gpui::ClickEvent, cx| {
1531 if event.down.button == MouseButton::Right || event.down.first_mouse {
1532 return;
1533 }
1534 outline_panel.open_entry(&clicked_entry, cx);
1535 })
1536 })
1537 .on_secondary_mouse_down(cx.listener(
1538 move |outline_panel, event: &MouseDownEvent, cx| {
1539 // Stop propagation to prevent the catch-all context menu for the project
1540 // panel from being deployed.
1541 cx.stop_propagation();
1542 outline_panel.deploy_context_menu(
1543 event.position,
1544 rendered_entry.to_ref_entry(),
1545 cx,
1546 )
1547 },
1548 )),
1549 )
1550 .border_1()
1551 .border_r_2()
1552 .rounded_none()
1553 .hover(|style| {
1554 if is_active {
1555 style
1556 } else {
1557 let hover_color = cx.theme().colors().ghost_element_hover;
1558 style.bg(hover_color).border_color(hover_color)
1559 }
1560 })
1561 .when(is_active && self.focus_handle.contains_focused(cx), |div| {
1562 div.border_color(Color::Selected.color(cx))
1563 })
1564 }
1565
1566 fn entry_name(
1567 &self,
1568 worktree_id: &WorktreeId,
1569 entry: &Entry,
1570 cx: &ViewContext<OutlinePanel>,
1571 ) -> String {
1572 let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
1573 Some(worktree) => {
1574 let worktree = worktree.read(cx);
1575 match worktree.snapshot().root_entry() {
1576 Some(root_entry) => {
1577 if root_entry.id == entry.id {
1578 file_name(worktree.abs_path().as_ref())
1579 } else {
1580 let path = worktree.absolutize(entry.path.as_ref()).ok();
1581 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
1582 file_name(path)
1583 }
1584 }
1585 None => {
1586 let path = worktree.absolutize(entry.path.as_ref()).ok();
1587 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
1588 file_name(path)
1589 }
1590 }
1591 }
1592 None => file_name(entry.path.as_ref()),
1593 };
1594 name
1595 }
1596
1597 fn update_fs_entries(
1598 &mut self,
1599 active_editor: &View<Editor>,
1600 new_entries: HashSet<ExcerptId>,
1601 new_selected_entry: Option<EntryOwned>,
1602 debounce: Option<Duration>,
1603 prefetch: bool,
1604 cx: &mut ViewContext<Self>,
1605 ) {
1606 if !self.active {
1607 return;
1608 }
1609
1610 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
1611 let active_multi_buffer = active_editor.read(cx).buffer().clone();
1612 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
1613 let mut new_collapsed_entries = self.collapsed_entries.clone();
1614 let mut new_unfolded_dirs = self.unfolded_dirs.clone();
1615 let mut root_entries = HashSet::default();
1616 let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
1617 let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
1618 HashMap::default(),
1619 |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
1620 let buffer_id = buffer_snapshot.remote_id();
1621 let file = File::from_dyn(buffer_snapshot.file());
1622 let entry_id = file.and_then(|file| file.project_entry_id(cx));
1623 let worktree = file.map(|file| file.worktree.read(cx).snapshot());
1624 let is_new =
1625 new_entries.contains(&excerpt_id) || !self.excerpts.contains_key(&buffer_id);
1626 buffer_excerpts
1627 .entry(buffer_id)
1628 .or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
1629 .1
1630 .push(excerpt_id);
1631
1632 let outlines = match self
1633 .excerpts
1634 .get(&buffer_id)
1635 .and_then(|excerpts| excerpts.get(&excerpt_id))
1636 {
1637 Some(old_excerpt) => match &old_excerpt.outlines {
1638 ExcerptOutlines::Outlines(outlines) => {
1639 ExcerptOutlines::Outlines(outlines.clone())
1640 }
1641 ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
1642 ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
1643 },
1644 None => {
1645 new_collapsed_entries
1646 .insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1647 ExcerptOutlines::NotFetched
1648 }
1649 };
1650 new_excerpts.entry(buffer_id).or_default().insert(
1651 excerpt_id,
1652 Excerpt {
1653 range: excerpt_range,
1654 outlines,
1655 },
1656 );
1657 buffer_excerpts
1658 },
1659 );
1660
1661 self.loading_outlines = true;
1662 self.update_task = cx.spawn(|outline_panel, mut cx| async move {
1663 if let Some(debounce) = debounce {
1664 cx.background_executor().timer(debounce).await;
1665 }
1666 let Some((new_collapsed_entries, new_unfolded_dirs, new_fs_entries, new_depth_map)) =
1667 cx.background_executor()
1668 .spawn(async move {
1669 let mut processed_external_buffers = HashSet::default();
1670 let mut new_worktree_entries =
1671 HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
1672 let mut worktree_excerpts = HashMap::<
1673 WorktreeId,
1674 HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
1675 >::default();
1676 let mut external_excerpts = HashMap::default();
1677
1678 for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
1679 if is_new {
1680 match &worktree {
1681 Some(worktree) => {
1682 new_collapsed_entries
1683 .insert(CollapsedEntry::File(worktree.id(), buffer_id));
1684 }
1685 None => {
1686 new_collapsed_entries
1687 .insert(CollapsedEntry::ExternalFile(buffer_id));
1688 }
1689 }
1690
1691 for excerpt_id in &excerpts {
1692 new_collapsed_entries
1693 .insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
1694 }
1695 }
1696
1697 if let Some(worktree) = worktree {
1698 let worktree_id = worktree.id();
1699 let unfolded_dirs =
1700 new_unfolded_dirs.entry(worktree_id).or_default();
1701
1702 match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
1703 Some(entry) => {
1704 let mut traversal = worktree.traverse_from_path(
1705 true,
1706 true,
1707 true,
1708 entry.path.as_ref(),
1709 );
1710
1711 let mut entries_to_add = HashSet::default();
1712 worktree_excerpts
1713 .entry(worktree_id)
1714 .or_default()
1715 .insert(entry.id, (buffer_id, excerpts));
1716 let mut current_entry = entry;
1717 loop {
1718 if current_entry.is_dir() {
1719 let is_root =
1720 worktree.root_entry().map(|entry| entry.id)
1721 == Some(current_entry.id);
1722 if is_root {
1723 root_entries.insert(current_entry.id);
1724 if auto_fold_dirs {
1725 unfolded_dirs.insert(current_entry.id);
1726 }
1727 }
1728
1729 if is_new {
1730 new_collapsed_entries.remove(
1731 &CollapsedEntry::Dir(
1732 worktree_id,
1733 current_entry.id,
1734 ),
1735 );
1736 } else if new_collapsed_entries.contains(
1737 &CollapsedEntry::Dir(
1738 worktree_id,
1739 current_entry.id,
1740 ),
1741 ) {
1742 entries_to_add.clear();
1743 }
1744 }
1745
1746 let new_entry_added =
1747 entries_to_add.insert(current_entry);
1748 if new_entry_added && traversal.back_to_parent() {
1749 if let Some(parent_entry) = traversal.entry() {
1750 current_entry = parent_entry.clone();
1751 continue;
1752 }
1753 }
1754 break;
1755 }
1756 new_worktree_entries
1757 .entry(worktree_id)
1758 .or_insert_with(|| {
1759 (worktree.clone(), HashSet::default())
1760 })
1761 .1
1762 .extend(entries_to_add);
1763 }
1764 None => {
1765 if processed_external_buffers.insert(buffer_id) {
1766 external_excerpts
1767 .entry(buffer_id)
1768 .or_insert_with(|| Vec::new())
1769 .extend(excerpts);
1770 }
1771 }
1772 }
1773 } else if processed_external_buffers.insert(buffer_id) {
1774 external_excerpts
1775 .entry(buffer_id)
1776 .or_insert_with(|| Vec::new())
1777 .extend(excerpts);
1778 }
1779 }
1780
1781 #[derive(Clone, Copy, Default)]
1782 struct Children {
1783 files: usize,
1784 dirs: usize,
1785 }
1786 let mut children_count =
1787 HashMap::<WorktreeId, HashMap<PathBuf, Children>>::default();
1788
1789 let worktree_entries = new_worktree_entries
1790 .into_iter()
1791 .map(|(worktree_id, (worktree_snapshot, entries))| {
1792 let mut entries = entries.into_iter().collect::<Vec<_>>();
1793 // For a proper git status propagation, we have to keep the entries sorted lexicographically.
1794 entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
1795 worktree_snapshot.propagate_git_statuses(&mut entries);
1796 project::sort_worktree_entries(&mut entries);
1797 (worktree_id, entries)
1798 })
1799 .flat_map(|(worktree_id, entries)| {
1800 {
1801 entries
1802 .into_iter()
1803 .filter_map(|entry| {
1804 if auto_fold_dirs {
1805 if let Some(parent) = entry.path.parent() {
1806 let children = children_count
1807 .entry(worktree_id)
1808 .or_default()
1809 .entry(parent.to_path_buf())
1810 .or_default();
1811 if entry.is_dir() {
1812 children.dirs += 1;
1813 } else {
1814 children.files += 1;
1815 }
1816 }
1817 }
1818
1819 if entry.is_dir() {
1820 Some(FsEntry::Directory(worktree_id, entry))
1821 } else {
1822 let (buffer_id, excerpts) = worktree_excerpts
1823 .get_mut(&worktree_id)
1824 .and_then(|worktree_excerpts| {
1825 worktree_excerpts.remove(&entry.id)
1826 })?;
1827 Some(FsEntry::File(
1828 worktree_id,
1829 entry,
1830 buffer_id,
1831 excerpts,
1832 ))
1833 }
1834 })
1835 .collect::<Vec<_>>()
1836 }
1837 })
1838 .collect::<Vec<_>>();
1839
1840 let mut visited_dirs = Vec::new();
1841 let mut new_depth_map = HashMap::default();
1842 let new_visible_entries = external_excerpts
1843 .into_iter()
1844 .sorted_by_key(|(id, _)| *id)
1845 .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
1846 .chain(worktree_entries)
1847 .filter(|visible_item| {
1848 match visible_item {
1849 FsEntry::Directory(worktree_id, dir_entry) => {
1850 let parent_id = back_to_common_visited_parent(
1851 &mut visited_dirs,
1852 worktree_id,
1853 dir_entry,
1854 );
1855
1856 visited_dirs.push((dir_entry.id, dir_entry.path.clone()));
1857 let depth = if root_entries.contains(&dir_entry.id) {
1858 0
1859 } else if auto_fold_dirs {
1860 let (parent_folded, parent_depth) = match parent_id {
1861 Some((worktree_id, id)) => (
1862 new_unfolded_dirs.get(&worktree_id).map_or(
1863 true,
1864 |unfolded_dirs| {
1865 !unfolded_dirs.contains(&id)
1866 },
1867 ),
1868 new_depth_map
1869 .get(&(worktree_id, id))
1870 .copied()
1871 .unwrap_or(0),
1872 ),
1873
1874 None => (false, 0),
1875 };
1876
1877 let children = children_count
1878 .get(&worktree_id)
1879 .and_then(|children_count| {
1880 children_count
1881 .get(&dir_entry.path.to_path_buf())
1882 })
1883 .copied()
1884 .unwrap_or_default();
1885 let folded = if children.dirs > 1
1886 || (children.dirs == 1 && children.files > 0)
1887 || (children.dirs == 0
1888 && visited_dirs
1889 .last()
1890 .map(|(parent_dir_id, _)| {
1891 root_entries.contains(parent_dir_id)
1892 })
1893 .unwrap_or(true))
1894 {
1895 new_unfolded_dirs
1896 .entry(*worktree_id)
1897 .or_default()
1898 .insert(dir_entry.id);
1899 false
1900 } else {
1901 new_unfolded_dirs.get(&worktree_id).map_or(
1902 true,
1903 |unfolded_dirs| {
1904 !unfolded_dirs.contains(&dir_entry.id)
1905 },
1906 )
1907 };
1908
1909 if parent_folded && folded {
1910 parent_depth
1911 } else {
1912 parent_depth + 1
1913 }
1914 } else {
1915 parent_id
1916 .and_then(|(worktree_id, id)| {
1917 new_depth_map.get(&(worktree_id, id)).copied()
1918 })
1919 .unwrap_or(0)
1920 + 1
1921 };
1922 new_depth_map.insert((*worktree_id, dir_entry.id), depth);
1923 }
1924 FsEntry::File(worktree_id, file_entry, ..) => {
1925 let parent_id = back_to_common_visited_parent(
1926 &mut visited_dirs,
1927 worktree_id,
1928 file_entry,
1929 );
1930 let depth = if root_entries.contains(&file_entry.id) {
1931 0
1932 } else {
1933 parent_id
1934 .and_then(|(worktree_id, id)| {
1935 new_depth_map.get(&(worktree_id, id)).copied()
1936 })
1937 .unwrap_or(0)
1938 + 1
1939 };
1940 new_depth_map.insert((*worktree_id, file_entry.id), depth);
1941 }
1942 FsEntry::ExternalFile(..) => {
1943 visited_dirs.clear();
1944 }
1945 }
1946
1947 true
1948 })
1949 .collect::<Vec<_>>();
1950
1951 anyhow::Ok((
1952 new_collapsed_entries,
1953 new_unfolded_dirs,
1954 new_visible_entries,
1955 new_depth_map,
1956 ))
1957 })
1958 .await
1959 .log_err()
1960 else {
1961 return;
1962 };
1963
1964 outline_panel
1965 .update(&mut cx, |outline_panel, cx| {
1966 outline_panel.loading_outlines = false;
1967 outline_panel.excerpts = new_excerpts;
1968 outline_panel.collapsed_entries = new_collapsed_entries;
1969 outline_panel.unfolded_dirs = new_unfolded_dirs;
1970 outline_panel.fs_entries = new_fs_entries;
1971 outline_panel.fs_entries_depth = new_depth_map;
1972 outline_panel.cached_entries_with_depth = None;
1973 if new_selected_entry.is_some() {
1974 outline_panel.selected_entry = new_selected_entry;
1975 }
1976 if prefetch {
1977 let range = if outline_panel.last_visible_range.is_empty() {
1978 0..(outline_panel.entries_with_depths(cx).len() / 4).min(50)
1979 } else {
1980 outline_panel.last_visible_range.clone()
1981 };
1982 outline_panel.fetch_outlines(&range, cx);
1983 }
1984
1985 outline_panel.autoscroll(cx);
1986 cx.notify();
1987 })
1988 .ok();
1989 });
1990 }
1991
1992 fn replace_visible_entries(
1993 &mut self,
1994 new_active_editor: View<Editor>,
1995 cx: &mut ViewContext<Self>,
1996 ) {
1997 let new_selected_entry = self.location_for_editor_selection(&new_active_editor, cx);
1998 self.clear_previous();
1999 self.active_item = Some(ActiveItem {
2000 item_id: new_active_editor.item_id(),
2001 _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, cx),
2002 active_editor: new_active_editor.downgrade(),
2003 });
2004 let new_entries =
2005 HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
2006 self.update_fs_entries(
2007 &new_active_editor,
2008 new_entries,
2009 new_selected_entry,
2010 None,
2011 true,
2012 cx,
2013 );
2014 }
2015
2016 fn clear_previous(&mut self) {
2017 self.collapsed_entries.clear();
2018 self.unfolded_dirs.clear();
2019 self.last_visible_range = 0..0;
2020 self.selected_entry = None;
2021 self.update_task = Task::ready(());
2022 self.active_item = None;
2023 self.fs_entries.clear();
2024 self.fs_entries_depth.clear();
2025 self.outline_fetch_tasks.clear();
2026 self.excerpts.clear();
2027 self.cached_entries_with_depth = None;
2028 }
2029
2030 fn location_for_editor_selection(
2031 &self,
2032 editor: &View<Editor>,
2033 cx: &mut ViewContext<Self>,
2034 ) -> Option<EntryOwned> {
2035 let selection = editor
2036 .read(cx)
2037 .selections
2038 .newest::<language::Point>(cx)
2039 .head();
2040 let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx));
2041 let multi_buffer = editor.read(cx).buffer();
2042 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
2043 let (excerpt_id, buffer, _) = editor
2044 .read(cx)
2045 .buffer()
2046 .read(cx)
2047 .excerpt_containing(selection, cx)?;
2048 let buffer_id = buffer.read(cx).remote_id();
2049 let selection_display_point = selection.to_display_point(&editor_snapshot);
2050
2051 let excerpt_outlines = self
2052 .excerpts
2053 .get(&buffer_id)
2054 .and_then(|excerpts| excerpts.get(&excerpt_id))
2055 .into_iter()
2056 .flat_map(|excerpt| excerpt.iter_outlines())
2057 .flat_map(|outline| {
2058 let start = multi_buffer_snapshot
2059 .anchor_in_excerpt(excerpt_id, outline.range.start)?
2060 .to_display_point(&editor_snapshot);
2061 let end = multi_buffer_snapshot
2062 .anchor_in_excerpt(excerpt_id, outline.range.end)?
2063 .to_display_point(&editor_snapshot);
2064 Some((start..end, outline))
2065 })
2066 .collect::<Vec<_>>();
2067
2068 let mut matching_outline_indices = Vec::new();
2069 let mut children = HashMap::default();
2070 let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
2071
2072 for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
2073 if outline_range
2074 .to_inclusive()
2075 .contains(&selection_display_point)
2076 {
2077 matching_outline_indices.push(i);
2078 } else if (outline_range.start.row()..outline_range.end.row())
2079 .to_inclusive()
2080 .contains(&selection_display_point.row())
2081 {
2082 matching_outline_indices.push(i);
2083 }
2084
2085 while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
2086 if parent_outline.depth >= outline.depth
2087 || !parent_range.contains(&outline_range.start)
2088 {
2089 parents_stack.pop();
2090 } else {
2091 break;
2092 }
2093 }
2094 if let Some((_, _, parent_index)) = parents_stack.last_mut() {
2095 children
2096 .entry(*parent_index)
2097 .or_insert_with(Vec::new)
2098 .push(i);
2099 }
2100 parents_stack.push((outline_range, outline, i));
2101 }
2102
2103 let outline_item = matching_outline_indices
2104 .into_iter()
2105 .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
2106 .filter(|(i, _)| {
2107 children
2108 .get(i)
2109 .map(|children| {
2110 children.iter().all(|child_index| {
2111 excerpt_outlines
2112 .get(*child_index)
2113 .map(|(child_range, _)| child_range.start > selection_display_point)
2114 .unwrap_or(false)
2115 })
2116 })
2117 .unwrap_or(true)
2118 })
2119 .min_by_key(|(_, (outline_range, outline))| {
2120 let distance_from_start = if outline_range.start > selection_display_point {
2121 outline_range.start - selection_display_point
2122 } else {
2123 selection_display_point - outline_range.start
2124 };
2125 let distance_from_end = if outline_range.end > selection_display_point {
2126 outline_range.end - selection_display_point
2127 } else {
2128 selection_display_point - outline_range.end
2129 };
2130
2131 (
2132 cmp::Reverse(outline.depth),
2133 distance_from_start + distance_from_end,
2134 )
2135 })
2136 .map(|(_, (_, outline))| *outline)
2137 .cloned();
2138
2139 let closest_container = match outline_item {
2140 Some(outline) => EntryOwned::Outline(buffer_id, excerpt_id, outline),
2141 None => self
2142 .cached_entries_with_depth
2143 .iter()
2144 .flatten()
2145 .rev()
2146 .find_map(|(_, entry)| match entry {
2147 EntryOwned::Excerpt(entry_buffer_id, entry_excerpt_id, _) => {
2148 if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
2149 Some(entry.clone())
2150 } else {
2151 None
2152 }
2153 }
2154 EntryOwned::Entry(
2155 FsEntry::ExternalFile(file_buffer_id, file_excerpts)
2156 | FsEntry::File(_, _, file_buffer_id, file_excerpts),
2157 ) => {
2158 if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
2159 Some(entry.clone())
2160 } else {
2161 None
2162 }
2163 }
2164 _ => None,
2165 })?,
2166 };
2167 Some(closest_container)
2168 }
2169
2170 fn fetch_outlines(&mut self, range: &Range<usize>, cx: &mut ViewContext<Self>) {
2171 let range_len = range.len();
2172 let half_range = range_len / 2;
2173 let entries = self.entries_with_depths(cx);
2174 let expanded_range =
2175 range.start.saturating_sub(half_range)..(range.end + half_range).min(entries.len());
2176
2177 let excerpt_fetch_ranges = self.excerpt_fetch_ranges(expanded_range, cx);
2178 if excerpt_fetch_ranges.is_empty() {
2179 return;
2180 }
2181
2182 let syntax_theme = cx.theme().syntax().clone();
2183 for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
2184 for (excerpt_id, excerpt_range) in excerpt_ranges {
2185 let syntax_theme = syntax_theme.clone();
2186 let buffer_snapshot = buffer_snapshot.clone();
2187 self.outline_fetch_tasks.insert(
2188 (buffer_id, excerpt_id),
2189 cx.spawn(|outline_panel, mut cx| async move {
2190 let fetched_outlines = cx
2191 .background_executor()
2192 .spawn(async move {
2193 buffer_snapshot
2194 .outline_items_containing(
2195 excerpt_range.context,
2196 false,
2197 Some(&syntax_theme),
2198 )
2199 .unwrap_or_default()
2200 })
2201 .await;
2202 outline_panel
2203 .update(&mut cx, |outline_panel, cx| {
2204 if let Some(excerpt) = outline_panel
2205 .excerpts
2206 .entry(buffer_id)
2207 .or_default()
2208 .get_mut(&excerpt_id)
2209 {
2210 excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
2211 }
2212 outline_panel.cached_entries_with_depth = None;
2213 cx.notify();
2214 })
2215 .ok();
2216 }),
2217 );
2218 }
2219 }
2220 }
2221
2222 fn entries_with_depths(&mut self, cx: &AppContext) -> &[(usize, EntryOwned)] {
2223 let is_singleton = self.is_singleton_active(cx);
2224 self.cached_entries_with_depth.get_or_insert_with(|| {
2225 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2226 let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
2227 let mut entries = Vec::new();
2228
2229 for entry in &self.fs_entries {
2230 let depth = match entry {
2231 FsEntry::Directory(worktree_id, dir_entry) => {
2232 let depth = self
2233 .fs_entries_depth
2234 .get(&(*worktree_id, dir_entry.id))
2235 .copied()
2236 .unwrap_or(0);
2237 if auto_fold_dirs {
2238 let folded = self
2239 .unfolded_dirs
2240 .get(worktree_id)
2241 .map_or(true, |unfolded_dirs| {
2242 !unfolded_dirs.contains(&dir_entry.id)
2243 });
2244 if folded {
2245 if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) =
2246 folded_dirs_entry.take()
2247 {
2248 if worktree_id == &folded_worktree_id
2249 && dir_entry.path.parent()
2250 == folded_dirs.last().map(|entry| entry.path.as_ref())
2251 {
2252 folded_dirs.push(dir_entry.clone());
2253 folded_dirs_entry =
2254 Some((folded_depth, folded_worktree_id, folded_dirs))
2255 } else {
2256 entries.push((
2257 folded_depth,
2258 EntryOwned::FoldedDirs(folded_worktree_id, folded_dirs),
2259 ));
2260 folded_dirs_entry =
2261 Some((depth, *worktree_id, vec![dir_entry.clone()]))
2262 }
2263 } else {
2264 folded_dirs_entry =
2265 Some((depth, *worktree_id, vec![dir_entry.clone()]))
2266 }
2267
2268 continue;
2269 }
2270 }
2271 depth
2272 }
2273 FsEntry::ExternalFile(..) => 0,
2274 FsEntry::File(worktree_id, file_entry, ..) => self
2275 .fs_entries_depth
2276 .get(&(*worktree_id, file_entry.id))
2277 .copied()
2278 .unwrap_or(0),
2279 };
2280 if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
2281 entries.push((
2282 folded_depth,
2283 EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2284 ));
2285 }
2286
2287 entries.push((depth, EntryOwned::Entry(entry.clone())));
2288
2289 let excerpts_to_consider = match entry {
2290 FsEntry::File(worktree_id, _, buffer_id, entry_excerpts) => {
2291 if is_singleton
2292 || !self
2293 .collapsed_entries
2294 .contains(&CollapsedEntry::File(*worktree_id, *buffer_id))
2295 {
2296 Some((*buffer_id, entry_excerpts))
2297 } else {
2298 None
2299 }
2300 }
2301 FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
2302 if is_singleton
2303 || !self
2304 .collapsed_entries
2305 .contains(&CollapsedEntry::ExternalFile(*buffer_id))
2306 {
2307 Some((*buffer_id, entry_excerpts))
2308 } else {
2309 None
2310 }
2311 }
2312 _ => None,
2313 };
2314 if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
2315 if let Some(excerpts) = self.excerpts.get(&buffer_id) {
2316 for &entry_excerpt in entry_excerpts {
2317 let Some(excerpt) = excerpts.get(&entry_excerpt) else {
2318 continue;
2319 };
2320 let excerpt_depth = depth + 1;
2321 entries.push((
2322 excerpt_depth,
2323 EntryOwned::Excerpt(
2324 buffer_id,
2325 entry_excerpt,
2326 excerpt.range.clone(),
2327 ),
2328 ));
2329
2330 let mut outline_base_depth = excerpt_depth + 1;
2331 if is_singleton {
2332 outline_base_depth = 0;
2333 entries.clear();
2334 } else if self
2335 .collapsed_entries
2336 .contains(&CollapsedEntry::Excerpt(buffer_id, entry_excerpt))
2337 {
2338 continue;
2339 }
2340
2341 for outline in excerpt.iter_outlines() {
2342 entries.push((
2343 outline_base_depth + outline.depth,
2344 EntryOwned::Outline(buffer_id, entry_excerpt, outline.clone()),
2345 ));
2346 }
2347 if is_singleton && entries.is_empty() {
2348 entries.push((0, EntryOwned::Entry(entry.clone())));
2349 }
2350 }
2351 }
2352 }
2353 }
2354 if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
2355 entries.push((
2356 folded_depth,
2357 EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2358 ));
2359 }
2360 entries
2361 })
2362 }
2363
2364 fn is_singleton_active(&self, cx: &AppContext) -> bool {
2365 self.active_item
2366 .as_ref()
2367 .and_then(|active_item| {
2368 Some(
2369 active_item
2370 .active_editor
2371 .upgrade()?
2372 .read(cx)
2373 .buffer()
2374 .read(cx)
2375 .is_singleton(),
2376 )
2377 })
2378 .unwrap_or(false)
2379 }
2380
2381 fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
2382 self.outline_fetch_tasks.clear();
2383 let mut ids = ids.into_iter().collect::<HashSet<_>>();
2384 for excerpts in self.excerpts.values_mut() {
2385 ids.retain(|id| {
2386 if let Some(excerpt) = excerpts.get_mut(id) {
2387 excerpt.invalidate_outlines();
2388 false
2389 } else {
2390 true
2391 }
2392 });
2393 if ids.is_empty() {
2394 break;
2395 }
2396 }
2397 }
2398
2399 fn excerpt_fetch_ranges(
2400 &self,
2401 entry_range: Range<usize>,
2402 cx: &AppContext,
2403 ) -> HashMap<
2404 BufferId,
2405 (
2406 BufferSnapshot,
2407 HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
2408 ),
2409 > {
2410 match self.cached_entries_with_depth.as_ref() {
2411 Some(entries) => entries.get(entry_range).into_iter().flatten().fold(
2412 HashMap::default(),
2413 |mut excerpts_to_fetch, (_, entry)| {
2414 match entry {
2415 EntryOwned::Entry(FsEntry::File(_, _, buffer_id, file_excerpts))
2416 | EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, file_excerpts)) => {
2417 let excerpts = self.excerpts.get(&buffer_id);
2418 for &file_excerpt in file_excerpts {
2419 if let Some(excerpt) = excerpts
2420 .and_then(|excerpts| excerpts.get(&file_excerpt))
2421 .filter(|excerpt| excerpt.should_fetch_outlines())
2422 {
2423 match excerpts_to_fetch.entry(*buffer_id) {
2424 hash_map::Entry::Occupied(mut o) => {
2425 o.get_mut()
2426 .1
2427 .insert(file_excerpt, excerpt.range.clone());
2428 }
2429 hash_map::Entry::Vacant(v) => {
2430 if let Some(buffer_snapshot) =
2431 self.buffer_snapshot_for_id(*buffer_id, cx)
2432 {
2433 v.insert((buffer_snapshot, HashMap::default()))
2434 .1
2435 .insert(file_excerpt, excerpt.range.clone());
2436 }
2437 }
2438 }
2439 }
2440 }
2441 }
2442 EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
2443 if let Some(excerpt) = self
2444 .excerpts
2445 .get(&buffer_id)
2446 .and_then(|excerpts| excerpts.get(&excerpt_id))
2447 .filter(|excerpt| excerpt.should_fetch_outlines())
2448 {
2449 match excerpts_to_fetch.entry(*buffer_id) {
2450 hash_map::Entry::Occupied(mut o) => {
2451 o.get_mut().1.insert(*excerpt_id, excerpt.range.clone());
2452 }
2453 hash_map::Entry::Vacant(v) => {
2454 if let Some(buffer_snapshot) =
2455 self.buffer_snapshot_for_id(*buffer_id, cx)
2456 {
2457 v.insert((buffer_snapshot, HashMap::default()))
2458 .1
2459 .insert(*excerpt_id, excerpt.range.clone());
2460 }
2461 }
2462 }
2463 }
2464 }
2465 _ => {}
2466 }
2467 excerpts_to_fetch
2468 },
2469 ),
2470 None => HashMap::default(),
2471 }
2472 }
2473
2474 fn buffer_snapshot_for_id(
2475 &self,
2476 buffer_id: BufferId,
2477 cx: &AppContext,
2478 ) -> Option<BufferSnapshot> {
2479 let editor = self.active_item.as_ref()?.active_editor.upgrade()?;
2480 Some(
2481 editor
2482 .read(cx)
2483 .buffer()
2484 .read(cx)
2485 .buffer(buffer_id)?
2486 .read(cx)
2487 .snapshot(),
2488 )
2489 }
2490
2491 fn abs_path(&self, entry: &EntryOwned, cx: &AppContext) -> Option<PathBuf> {
2492 match entry {
2493 EntryOwned::Entry(
2494 FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
2495 ) => self
2496 .buffer_snapshot_for_id(*buffer_id, cx)
2497 .and_then(|buffer_snapshot| {
2498 let file = File::from_dyn(buffer_snapshot.file())?;
2499 file.worktree.read(cx).absolutize(&file.path).ok()
2500 }),
2501 EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => self
2502 .project
2503 .read(cx)
2504 .worktree_for_id(*worktree_id, cx)?
2505 .read(cx)
2506 .absolutize(&entry.path)
2507 .ok(),
2508 EntryOwned::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
2509 self.project
2510 .read(cx)
2511 .worktree_for_id(*worktree_id, cx)
2512 .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
2513 }),
2514 EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
2515 }
2516 }
2517
2518 fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option<PathBuf> {
2519 match entry {
2520 FsEntry::ExternalFile(buffer_id, _) => self
2521 .buffer_snapshot_for_id(*buffer_id, cx)
2522 .and_then(|buffer_snapshot| Some(buffer_snapshot.file()?.path().to_path_buf())),
2523 FsEntry::Directory(_, entry) => Some(entry.path.to_path_buf()),
2524 FsEntry::File(_, entry, ..) => Some(entry.path.to_path_buf()),
2525 }
2526 }
2527}
2528
2529fn back_to_common_visited_parent(
2530 visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
2531 worktree_id: &WorktreeId,
2532 new_entry: &Entry,
2533) -> Option<(WorktreeId, ProjectEntryId)> {
2534 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
2535 match new_entry.path.parent() {
2536 Some(parent_path) => {
2537 if parent_path == visited_path.as_ref() {
2538 return Some((*worktree_id, *visited_dir_id));
2539 }
2540 }
2541 None => {
2542 break;
2543 }
2544 }
2545 visited_dirs.pop();
2546 }
2547 None
2548}
2549
2550fn file_name(path: &Path) -> String {
2551 let mut current_path = path;
2552 loop {
2553 if let Some(file_name) = current_path.file_name() {
2554 return file_name.to_string_lossy().into_owned();
2555 }
2556 match current_path.parent() {
2557 Some(parent) => current_path = parent,
2558 None => return path.to_string_lossy().into_owned(),
2559 }
2560 }
2561}
2562
2563impl Panel for OutlinePanel {
2564 fn persistent_name() -> &'static str {
2565 "Outline Panel"
2566 }
2567
2568 fn position(&self, cx: &WindowContext) -> DockPosition {
2569 match OutlinePanelSettings::get_global(cx).dock {
2570 OutlinePanelDockPosition::Left => DockPosition::Left,
2571 OutlinePanelDockPosition::Right => DockPosition::Right,
2572 }
2573 }
2574
2575 fn position_is_valid(&self, position: DockPosition) -> bool {
2576 matches!(position, DockPosition::Left | DockPosition::Right)
2577 }
2578
2579 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2580 settings::update_settings_file::<OutlinePanelSettings>(
2581 self.fs.clone(),
2582 cx,
2583 move |settings| {
2584 let dock = match position {
2585 DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
2586 DockPosition::Right => OutlinePanelDockPosition::Right,
2587 };
2588 settings.dock = Some(dock);
2589 },
2590 );
2591 }
2592
2593 fn size(&self, cx: &WindowContext) -> Pixels {
2594 self.width
2595 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
2596 }
2597
2598 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
2599 self.width = size;
2600 self.serialize(cx);
2601 cx.notify();
2602 }
2603
2604 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
2605 OutlinePanelSettings::get_global(cx)
2606 .button
2607 .then(|| IconName::ListTree)
2608 }
2609
2610 fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
2611 Some("Outline Panel")
2612 }
2613
2614 fn toggle_action(&self) -> Box<dyn Action> {
2615 Box::new(ToggleFocus)
2616 }
2617
2618 fn starts_open(&self, _: &WindowContext) -> bool {
2619 self.active
2620 }
2621
2622 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2623 let old_active = self.active;
2624 self.active = active;
2625 if active && old_active != active {
2626 if let Some(active_editor) = self
2627 .active_item
2628 .as_ref()
2629 .and_then(|item| item.active_editor.upgrade())
2630 {
2631 if self.active_item.as_ref().map(|item| item.item_id)
2632 == Some(active_editor.item_id())
2633 {
2634 let new_selected_entry = self.location_for_editor_selection(&active_editor, cx);
2635 self.update_fs_entries(
2636 &active_editor,
2637 HashSet::default(),
2638 new_selected_entry,
2639 None,
2640 true,
2641 cx,
2642 )
2643 } else {
2644 self.replace_visible_entries(active_editor, cx);
2645 }
2646 }
2647 }
2648 self.serialize(cx);
2649 }
2650}
2651
2652impl FocusableView for OutlinePanel {
2653 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2654 self.focus_handle.clone()
2655 }
2656}
2657
2658impl EventEmitter<Event> for OutlinePanel {}
2659
2660impl EventEmitter<PanelEvent> for OutlinePanel {}
2661
2662impl Render for OutlinePanel {
2663 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2664 let project = self.project.read(cx);
2665 if self.fs_entries.is_empty() {
2666 let header = if self.loading_outlines {
2667 "Loading outlines"
2668 } else {
2669 "No outlines available"
2670 };
2671 v_flex()
2672 .id("empty-outline_panel")
2673 .justify_center()
2674 .size_full()
2675 .p_4()
2676 .track_focus(&self.focus_handle)
2677 .child(h_flex().justify_center().child(Label::new(header)))
2678 .child(
2679 h_flex()
2680 .pt(Spacing::Small.rems(cx))
2681 .justify_center()
2682 .child({
2683 let keystroke = match self.position(cx) {
2684 DockPosition::Left => {
2685 cx.keystroke_text_for(&workspace::ToggleLeftDock)
2686 }
2687 DockPosition::Bottom => {
2688 cx.keystroke_text_for(&workspace::ToggleBottomDock)
2689 }
2690 DockPosition::Right => {
2691 cx.keystroke_text_for(&workspace::ToggleRightDock)
2692 }
2693 };
2694 Label::new(format!("Toggle this panel with {keystroke}",))
2695 }),
2696 )
2697 } else {
2698 h_flex()
2699 .id("outline-panel")
2700 .size_full()
2701 .relative()
2702 .key_context(self.dispatch_context(cx))
2703 .on_action(cx.listener(Self::open))
2704 .on_action(cx.listener(Self::select_next))
2705 .on_action(cx.listener(Self::select_prev))
2706 .on_action(cx.listener(Self::select_first))
2707 .on_action(cx.listener(Self::select_last))
2708 .on_action(cx.listener(Self::select_parent))
2709 .on_action(cx.listener(Self::expand_selected_entry))
2710 .on_action(cx.listener(Self::collapse_selected_entry))
2711 .on_action(cx.listener(Self::collapse_all_entries))
2712 .on_action(cx.listener(Self::copy_path))
2713 .on_action(cx.listener(Self::copy_relative_path))
2714 .on_action(cx.listener(Self::unfold_directory))
2715 .on_action(cx.listener(Self::fold_directory))
2716 .when(project.is_local(), |el| {
2717 el.on_action(cx.listener(Self::reveal_in_finder))
2718 .on_action(cx.listener(Self::open_in_terminal))
2719 })
2720 .on_mouse_down(
2721 MouseButton::Right,
2722 cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
2723 if let Some(entry) = outline_panel.selected_entry.clone() {
2724 outline_panel.deploy_context_menu(
2725 event.position,
2726 entry.to_ref_entry(),
2727 cx,
2728 )
2729 } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
2730 outline_panel.deploy_context_menu(
2731 event.position,
2732 EntryRef::Entry(&entry),
2733 cx,
2734 )
2735 }
2736 }),
2737 )
2738 .track_focus(&self.focus_handle)
2739 .child({
2740 let items_len = self.entries_with_depths(cx).len();
2741 uniform_list(cx.view().clone(), "entries", items_len, {
2742 move |outline_panel, range, cx| {
2743 outline_panel.last_visible_range = range.clone();
2744 outline_panel.fetch_outlines(&range, cx);
2745 let entries = outline_panel.entries_with_depths(cx).get(range);
2746 entries
2747 .map(|entries| entries.to_vec())
2748 .unwrap_or_default()
2749 .into_iter()
2750 .filter_map(|(depth, entry)| match entry {
2751 EntryOwned::Entry(entry) => {
2752 Some(outline_panel.render_entry(&entry, depth, cx))
2753 }
2754 EntryOwned::FoldedDirs(worktree_id, entries) => {
2755 Some(outline_panel.render_folded_dirs(
2756 worktree_id,
2757 &entries,
2758 depth,
2759 cx,
2760 ))
2761 }
2762 EntryOwned::Excerpt(buffer_id, excerpt_id, excerpt) => {
2763 outline_panel.render_excerpt(
2764 buffer_id, excerpt_id, &excerpt, depth, cx,
2765 )
2766 }
2767 EntryOwned::Outline(buffer_id, excerpt_id, outline) => {
2768 Some(outline_panel.render_outline(
2769 buffer_id, excerpt_id, &outline, depth, cx,
2770 ))
2771 }
2772 })
2773 .collect()
2774 }
2775 })
2776 .size_full()
2777 .track_scroll(self.scroll_handle.clone())
2778 })
2779 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2780 deferred(
2781 anchored()
2782 .position(*position)
2783 .anchor(gpui::AnchorCorner::TopLeft)
2784 .child(menu.clone()),
2785 )
2786 .with_priority(1)
2787 }))
2788 }
2789 }
2790}
2791
2792fn subscribe_for_editor_events(
2793 editor: &View<Editor>,
2794 cx: &mut ViewContext<OutlinePanel>,
2795) -> Subscription {
2796 let debounce = Some(Duration::from_millis(UPDATE_DEBOUNCE_MILLIS));
2797 cx.subscribe(
2798 editor,
2799 move |outline_panel, editor, e: &EditorEvent, cx| match e {
2800 EditorEvent::SelectionsChanged { local: true } => {
2801 outline_panel.reveal_entry_for_selection(&editor, cx);
2802 cx.notify();
2803 }
2804 EditorEvent::ExcerptsAdded { excerpts, .. } => {
2805 outline_panel.update_fs_entries(
2806 &editor,
2807 excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
2808 None,
2809 debounce,
2810 false,
2811 cx,
2812 );
2813 }
2814 EditorEvent::ExcerptsRemoved { ids } => {
2815 let mut ids = ids.into_iter().collect::<HashSet<_>>();
2816 for excerpts in outline_panel.excerpts.values_mut() {
2817 excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
2818 if ids.is_empty() {
2819 break;
2820 }
2821 }
2822 outline_panel.update_fs_entries(
2823 &editor,
2824 HashSet::default(),
2825 None,
2826 debounce,
2827 false,
2828 cx,
2829 );
2830 }
2831 EditorEvent::ExcerptsExpanded { ids } => {
2832 outline_panel.invalidate_outlines(ids);
2833 outline_panel.update_fs_entries(
2834 &editor,
2835 HashSet::default(),
2836 None,
2837 debounce,
2838 true,
2839 cx,
2840 );
2841 }
2842 EditorEvent::ExcerptsEdited { ids } => {
2843 outline_panel.invalidate_outlines(ids);
2844 outline_panel.update_fs_entries(
2845 &editor,
2846 HashSet::default(),
2847 None,
2848 debounce,
2849 true,
2850 cx,
2851 );
2852 }
2853 EditorEvent::Reparsed(buffer_id) => {
2854 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
2855 for (_, excerpt) in excerpts {
2856 excerpt.invalidate_outlines();
2857 }
2858 }
2859 outline_panel.update_fs_entries(
2860 &editor,
2861 HashSet::default(),
2862 None,
2863 debounce,
2864 true,
2865 cx,
2866 );
2867 }
2868 _ => {}
2869 },
2870 )
2871}
2872
2873fn empty_icon() -> AnyElement {
2874 h_flex()
2875 .size(IconSize::default().rems())
2876 .invisible()
2877 .flex_none()
2878 .into_any_element()
2879}