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 RevealInFileManager,
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 .when(cfg!(target_os = "macos"), |menu| {
785 menu.action("Reveal in Finder", Box::new(RevealInFileManager))
786 })
787 .when(cfg!(not(target_os = "macos")), |menu| {
788 menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
789 })
790 .action("Open in Terminal", Box::new(OpenInTerminal))
791 .when(is_unfoldable, |menu| {
792 menu.action("Unfold Directory", Box::new(UnfoldDirectory))
793 })
794 .when(is_foldable, |menu| {
795 menu.action("Fold Directory", Box::new(FoldDirectory))
796 })
797 .separator()
798 .action("Copy Path", Box::new(CopyPath))
799 .action("Copy Relative Path", Box::new(CopyRelativePath))
800 });
801 cx.focus_view(&context_menu);
802 let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
803 outline_panel.context_menu.take();
804 cx.notify();
805 });
806 self.context_menu = Some((context_menu, position, subscription));
807 cx.notify();
808 }
809
810 fn is_unfoldable(&self, entry: EntryRef) -> bool {
811 matches!(entry, EntryRef::FoldedDirs(..))
812 }
813
814 fn is_foldable(&self, entry: EntryRef) -> bool {
815 let (directory_worktree, directory_entry) = match entry {
816 EntryRef::Entry(FsEntry::Directory(directory_worktree, directory_entry)) => {
817 (*directory_worktree, Some(directory_entry))
818 }
819 EntryRef::FoldedDirs(directory_worktree, entries) => {
820 (directory_worktree, entries.last())
821 }
822 _ => return false,
823 };
824 let Some(directory_entry) = directory_entry else {
825 return false;
826 };
827
828 if self
829 .unfolded_dirs
830 .get(&directory_worktree)
831 .map_or(false, |unfolded_dirs| {
832 unfolded_dirs.contains(&directory_entry.id)
833 })
834 {
835 return true;
836 }
837
838 let child_entries = self
839 .fs_entries
840 .iter()
841 .skip_while(|entry| {
842 if let FsEntry::Directory(worktree_id, entry) = entry {
843 worktree_id != &directory_worktree || entry.id != directory_entry.id
844 } else {
845 true
846 }
847 })
848 .skip(1)
849 .filter(|next_entry| match next_entry {
850 FsEntry::ExternalFile(..) => false,
851 FsEntry::Directory(worktree_id, entry) | FsEntry::File(worktree_id, entry, ..) => {
852 worktree_id == &directory_worktree
853 && entry.path.parent() == Some(directory_entry.path.as_ref())
854 }
855 })
856 .collect::<Vec<_>>();
857
858 child_entries.len() == 1 && matches!(child_entries.first(), Some(FsEntry::Directory(..)))
859 }
860
861 fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
862 let Some(editor) = self
863 .active_item
864 .as_ref()
865 .and_then(|item| item.active_editor.upgrade())
866 else {
867 return;
868 };
869
870 let entry_to_expand = match &self.selected_entry {
871 Some(EntryOwned::FoldedDirs(worktree_id, dir_entries)) => dir_entries
872 .last()
873 .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)),
874 Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry))) => {
875 Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
876 }
877 Some(EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => {
878 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
879 }
880 Some(EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => {
881 Some(CollapsedEntry::ExternalFile(*buffer_id))
882 }
883 Some(EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
884 Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
885 }
886 None | Some(EntryOwned::Outline(..)) => None,
887 };
888 let Some(collapsed_entry) = entry_to_expand else {
889 return;
890 };
891 let expanded = self.collapsed_entries.remove(&collapsed_entry);
892 if expanded {
893 if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
894 self.project.update(cx, |project, cx| {
895 project.expand_entry(worktree_id, dir_entry_id, cx);
896 });
897 }
898 self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
899 } else {
900 self.select_next(&SelectNext, cx)
901 }
902 }
903
904 fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
905 let Some(editor) = self
906 .active_item
907 .as_ref()
908 .and_then(|item| item.active_editor.upgrade())
909 else {
910 return;
911 };
912 match &self.selected_entry {
913 Some(
914 dir_entry @ EntryOwned::Entry(FsEntry::Directory(worktree_id, selected_dir_entry)),
915 ) => {
916 self.collapsed_entries
917 .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id));
918 self.update_fs_entries(
919 &editor,
920 HashSet::default(),
921 Some(dir_entry.clone()),
922 None,
923 false,
924 cx,
925 );
926 }
927 Some(file_entry @ EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => {
928 self.collapsed_entries
929 .insert(CollapsedEntry::File(*worktree_id, *buffer_id));
930 self.update_fs_entries(
931 &editor,
932 HashSet::default(),
933 Some(file_entry.clone()),
934 None,
935 false,
936 cx,
937 );
938 }
939 Some(file_entry @ EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => {
940 self.collapsed_entries
941 .insert(CollapsedEntry::ExternalFile(*buffer_id));
942 self.update_fs_entries(
943 &editor,
944 HashSet::default(),
945 Some(file_entry.clone()),
946 None,
947 false,
948 cx,
949 );
950 }
951 Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => {
952 if let Some(dir_entry) = dir_entries.last() {
953 if self
954 .collapsed_entries
955 .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
956 {
957 self.update_fs_entries(
958 &editor,
959 HashSet::default(),
960 Some(dirs_entry.clone()),
961 None,
962 false,
963 cx,
964 );
965 }
966 }
967 }
968 Some(excerpt_entry @ EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
969 if self
970 .collapsed_entries
971 .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
972 {
973 self.update_fs_entries(
974 &editor,
975 HashSet::default(),
976 Some(excerpt_entry.clone()),
977 None,
978 false,
979 cx,
980 );
981 }
982 }
983 None | Some(EntryOwned::Outline(..)) => {}
984 }
985 }
986
987 pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
988 let Some(editor) = self
989 .active_item
990 .as_ref()
991 .and_then(|item| item.active_editor.upgrade())
992 else {
993 return;
994 };
995
996 let new_entries = self
997 .entries_with_depths(cx)
998 .iter()
999 .flat_map(|(_, entry)| match entry {
1000 EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => {
1001 Some(CollapsedEntry::Dir(*worktree_id, entry.id))
1002 }
1003 EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1004 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1005 }
1006 EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
1007 Some(CollapsedEntry::ExternalFile(*buffer_id))
1008 }
1009 EntryOwned::FoldedDirs(worktree_id, entries) => {
1010 Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id))
1011 }
1012 EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
1013 Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1014 }
1015 EntryOwned::Outline(..) => None,
1016 })
1017 .collect::<Vec<_>>();
1018 self.collapsed_entries.extend(new_entries);
1019 self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
1020 }
1021
1022 fn toggle_expanded(&mut self, entry: &EntryOwned, cx: &mut ViewContext<Self>) {
1023 let Some(editor) = self
1024 .active_item
1025 .as_ref()
1026 .and_then(|item| item.active_editor.upgrade())
1027 else {
1028 return;
1029 };
1030
1031 match entry {
1032 EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry)) => {
1033 let entry_id = dir_entry.id;
1034 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1035 if self.collapsed_entries.remove(&collapsed_entry) {
1036 self.project
1037 .update(cx, |project, cx| {
1038 project.expand_entry(*worktree_id, entry_id, cx)
1039 })
1040 .unwrap_or_else(|| Task::ready(Ok(())))
1041 .detach_and_log_err(cx);
1042 } else {
1043 self.collapsed_entries.insert(collapsed_entry);
1044 }
1045 }
1046 EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1047 let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1048 if !self.collapsed_entries.remove(&collapsed_entry) {
1049 self.collapsed_entries.insert(collapsed_entry);
1050 }
1051 }
1052 EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
1053 let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id);
1054 if !self.collapsed_entries.remove(&collapsed_entry) {
1055 self.collapsed_entries.insert(collapsed_entry);
1056 }
1057 }
1058 EntryOwned::FoldedDirs(worktree_id, dir_entries) => {
1059 if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
1060 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1061 if self.collapsed_entries.remove(&collapsed_entry) {
1062 self.project
1063 .update(cx, |project, cx| {
1064 project.expand_entry(*worktree_id, entry_id, cx)
1065 })
1066 .unwrap_or_else(|| Task::ready(Ok(())))
1067 .detach_and_log_err(cx);
1068 } else {
1069 self.collapsed_entries.insert(collapsed_entry);
1070 }
1071 }
1072 }
1073 EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
1074 let collapsed_entry = CollapsedEntry::Excerpt(*buffer_id, *excerpt_id);
1075 if !self.collapsed_entries.remove(&collapsed_entry) {
1076 self.collapsed_entries.insert(collapsed_entry);
1077 }
1078 }
1079 EntryOwned::Outline(..) => return,
1080 }
1081
1082 self.update_fs_entries(
1083 &editor,
1084 HashSet::default(),
1085 Some(entry.clone()),
1086 None,
1087 false,
1088 cx,
1089 );
1090 }
1091
1092 fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
1093 if let Some(clipboard_text) = self
1094 .selected_entry
1095 .as_ref()
1096 .and_then(|entry| self.abs_path(&entry, cx))
1097 .map(|p| p.to_string_lossy().to_string())
1098 {
1099 cx.write_to_clipboard(ClipboardItem::new(clipboard_text));
1100 }
1101 }
1102
1103 fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1104 if let Some(clipboard_text) = self
1105 .selected_entry
1106 .as_ref()
1107 .and_then(|entry| match entry {
1108 EntryOwned::Entry(entry) => self.relative_path(&entry, cx),
1109 EntryOwned::FoldedDirs(_, dirs) => {
1110 dirs.last().map(|entry| entry.path.to_path_buf())
1111 }
1112 EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
1113 })
1114 .map(|p| p.to_string_lossy().to_string())
1115 {
1116 cx.write_to_clipboard(ClipboardItem::new(clipboard_text));
1117 }
1118 }
1119
1120 fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
1121 if let Some(abs_path) = self
1122 .selected_entry
1123 .as_ref()
1124 .and_then(|entry| self.abs_path(&entry, cx))
1125 {
1126 cx.reveal_path(&abs_path);
1127 }
1128 }
1129
1130 fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
1131 let selected_entry = self.selected_entry.as_ref();
1132 let abs_path = selected_entry.and_then(|entry| self.abs_path(&entry, cx));
1133 let working_directory = if let (
1134 Some(abs_path),
1135 Some(EntryOwned::Entry(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1136 ) = (&abs_path, selected_entry)
1137 {
1138 abs_path.parent().map(|p| p.to_owned())
1139 } else {
1140 abs_path
1141 };
1142
1143 if let Some(working_directory) = working_directory {
1144 cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
1145 }
1146 }
1147
1148 fn reveal_entry_for_selection(
1149 &mut self,
1150 editor: &View<Editor>,
1151 cx: &mut ViewContext<'_, Self>,
1152 ) {
1153 if !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
1154 return;
1155 }
1156 let Some(entry_with_selection) = self.location_for_editor_selection(editor, cx) else {
1157 self.selected_entry = None;
1158 cx.notify();
1159 return;
1160 };
1161 let related_buffer_entry = match entry_with_selection {
1162 EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1163 let project = self.project.read(cx);
1164 let entry_id = project
1165 .buffer_for_id(buffer_id)
1166 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1167 project
1168 .worktree_for_id(worktree_id, cx)
1169 .zip(entry_id)
1170 .and_then(|(worktree, entry_id)| {
1171 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1172 Some((worktree, entry))
1173 })
1174 }
1175 EntryOwned::Outline(buffer_id, excerpt_id, _)
1176 | EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
1177 self.collapsed_entries
1178 .remove(&CollapsedEntry::ExternalFile(buffer_id));
1179 self.collapsed_entries
1180 .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1181 let project = self.project.read(cx);
1182 let entry_id = project
1183 .buffer_for_id(buffer_id)
1184 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1185
1186 entry_id.and_then(|entry_id| {
1187 project
1188 .worktree_for_entry(entry_id, cx)
1189 .and_then(|worktree| {
1190 let worktree_id = worktree.read(cx).id();
1191 self.collapsed_entries
1192 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1193 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1194 Some((worktree, entry))
1195 })
1196 })
1197 }
1198 EntryOwned::Entry(FsEntry::ExternalFile(..)) => None,
1199 _ => return,
1200 };
1201 if let Some((worktree, buffer_entry)) = related_buffer_entry {
1202 let worktree_id = worktree.read(cx).id();
1203 let mut dirs_to_expand = Vec::new();
1204 {
1205 let mut traversal = worktree.read(cx).traverse_from_path(
1206 true,
1207 true,
1208 true,
1209 buffer_entry.path.as_ref(),
1210 );
1211 let mut current_entry = buffer_entry;
1212 loop {
1213 if current_entry.is_dir() {
1214 if self
1215 .collapsed_entries
1216 .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
1217 {
1218 dirs_to_expand.push(current_entry.id);
1219 }
1220 }
1221
1222 if traversal.back_to_parent() {
1223 if let Some(parent_entry) = traversal.entry() {
1224 current_entry = parent_entry.clone();
1225 continue;
1226 }
1227 }
1228 break;
1229 }
1230 }
1231 for dir_to_expand in dirs_to_expand {
1232 self.project
1233 .update(cx, |project, cx| {
1234 project.expand_entry(worktree_id, dir_to_expand, cx)
1235 })
1236 .unwrap_or_else(|| Task::ready(Ok(())))
1237 .detach_and_log_err(cx)
1238 }
1239 }
1240
1241 self.update_fs_entries(
1242 &editor,
1243 HashSet::default(),
1244 Some(entry_with_selection),
1245 None,
1246 false,
1247 cx,
1248 );
1249 }
1250
1251 fn render_excerpt(
1252 &self,
1253 buffer_id: BufferId,
1254 excerpt_id: ExcerptId,
1255 range: &ExcerptRange<language::Anchor>,
1256 depth: usize,
1257 cx: &mut ViewContext<OutlinePanel>,
1258 ) -> Option<Stateful<Div>> {
1259 let item_id = ElementId::from(excerpt_id.to_proto() as usize);
1260 let is_active = match &self.selected_entry {
1261 Some(EntryOwned::Excerpt(selected_buffer_id, selected_excerpt_id, _)) => {
1262 selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id
1263 }
1264 _ => false,
1265 };
1266 let has_outlines = self
1267 .excerpts
1268 .get(&buffer_id)
1269 .and_then(|excerpts| match &excerpts.get(&excerpt_id)?.outlines {
1270 ExcerptOutlines::Outlines(outlines) => Some(outlines),
1271 ExcerptOutlines::Invalidated(outlines) => Some(outlines),
1272 ExcerptOutlines::NotFetched => None,
1273 })
1274 .map_or(false, |outlines| !outlines.is_empty());
1275 let is_expanded = !self
1276 .collapsed_entries
1277 .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1278 let color = entry_git_aware_label_color(None, false, is_active);
1279 let icon = if has_outlines {
1280 FileIcons::get_chevron_icon(is_expanded, cx)
1281 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1282 } else {
1283 None
1284 }
1285 .unwrap_or_else(empty_icon);
1286
1287 let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
1288 let excerpt_range = range.context.to_point(&buffer_snapshot);
1289 let label_element = Label::new(format!(
1290 "Lines {}-{}",
1291 excerpt_range.start.row + 1,
1292 excerpt_range.end.row + 1,
1293 ))
1294 .single_line()
1295 .color(color)
1296 .into_any_element();
1297
1298 Some(self.entry_element(
1299 EntryRef::Excerpt(buffer_id, excerpt_id, range),
1300 item_id,
1301 depth,
1302 Some(icon),
1303 is_active,
1304 label_element,
1305 cx,
1306 ))
1307 }
1308
1309 fn render_outline(
1310 &self,
1311 buffer_id: BufferId,
1312 excerpt_id: ExcerptId,
1313 rendered_outline: &Outline,
1314 depth: usize,
1315 cx: &mut ViewContext<Self>,
1316 ) -> Stateful<Div> {
1317 let (item_id, label_element) = (
1318 ElementId::from(SharedString::from(format!(
1319 "{buffer_id:?}|{excerpt_id:?}{:?}|{:?}",
1320 rendered_outline.range, &rendered_outline.text,
1321 ))),
1322 language::render_item(&rendered_outline, None, cx).into_any_element(),
1323 );
1324 let is_active = match &self.selected_entry {
1325 Some(EntryOwned::Outline(selected_buffer_id, selected_excerpt_id, selected_entry)) => {
1326 selected_buffer_id == &buffer_id
1327 && selected_excerpt_id == &excerpt_id
1328 && selected_entry == rendered_outline
1329 }
1330 _ => false,
1331 };
1332 let icon = if self.is_singleton_active(cx) {
1333 None
1334 } else {
1335 Some(empty_icon())
1336 };
1337 self.entry_element(
1338 EntryRef::Outline(buffer_id, excerpt_id, rendered_outline),
1339 item_id,
1340 depth,
1341 icon,
1342 is_active,
1343 label_element,
1344 cx,
1345 )
1346 }
1347
1348 fn render_entry(
1349 &self,
1350 rendered_entry: &FsEntry,
1351 depth: usize,
1352 cx: &mut ViewContext<Self>,
1353 ) -> Stateful<Div> {
1354 let settings = OutlinePanelSettings::get_global(cx);
1355 let is_active = match &self.selected_entry {
1356 Some(EntryOwned::Entry(selected_entry)) => selected_entry == rendered_entry,
1357 _ => false,
1358 };
1359 let (item_id, label_element, icon) = match rendered_entry {
1360 FsEntry::File(worktree_id, entry, ..) => {
1361 let name = self.entry_name(worktree_id, entry, cx);
1362 let color =
1363 entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1364 let icon = if settings.file_icons {
1365 FileIcons::get_icon(&entry.path, cx)
1366 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1367 } else {
1368 None
1369 };
1370 (
1371 ElementId::from(entry.id.to_proto() as usize),
1372 Label::new(name)
1373 .single_line()
1374 .color(color)
1375 .into_any_element(),
1376 icon.unwrap_or_else(empty_icon),
1377 )
1378 }
1379 FsEntry::Directory(worktree_id, entry) => {
1380 let name = self.entry_name(worktree_id, entry, cx);
1381
1382 let is_expanded = !self
1383 .collapsed_entries
1384 .contains(&CollapsedEntry::Dir(*worktree_id, entry.id));
1385 let color =
1386 entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1387 let icon = if settings.folder_icons {
1388 FileIcons::get_folder_icon(is_expanded, cx)
1389 } else {
1390 FileIcons::get_chevron_icon(is_expanded, cx)
1391 }
1392 .map(Icon::from_path)
1393 .map(|icon| icon.color(color).into_any_element());
1394 (
1395 ElementId::from(entry.id.to_proto() as usize),
1396 Label::new(name)
1397 .single_line()
1398 .color(color)
1399 .into_any_element(),
1400 icon.unwrap_or_else(empty_icon),
1401 )
1402 }
1403 FsEntry::ExternalFile(buffer_id, ..) => {
1404 let color = entry_label_color(is_active);
1405 let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) {
1406 Some(buffer_snapshot) => match buffer_snapshot.file() {
1407 Some(file) => {
1408 let path = file.path();
1409 let icon = if settings.file_icons {
1410 FileIcons::get_icon(path.as_ref(), cx)
1411 } else {
1412 None
1413 }
1414 .map(Icon::from_path)
1415 .map(|icon| icon.color(color).into_any_element());
1416 (icon, file_name(path.as_ref()))
1417 }
1418 None => (None, "Untitled".to_string()),
1419 },
1420 None => (None, "Unknown buffer".to_string()),
1421 };
1422 (
1423 ElementId::from(buffer_id.to_proto() as usize),
1424 Label::new(name)
1425 .single_line()
1426 .color(color)
1427 .into_any_element(),
1428 icon.unwrap_or_else(empty_icon),
1429 )
1430 }
1431 };
1432
1433 self.entry_element(
1434 EntryRef::Entry(rendered_entry),
1435 item_id,
1436 depth,
1437 Some(icon),
1438 is_active,
1439 label_element,
1440 cx,
1441 )
1442 }
1443
1444 fn render_folded_dirs(
1445 &self,
1446 worktree_id: WorktreeId,
1447 dir_entries: &[Entry],
1448 depth: usize,
1449 cx: &mut ViewContext<OutlinePanel>,
1450 ) -> Stateful<Div> {
1451 let settings = OutlinePanelSettings::get_global(cx);
1452 let is_active = match &self.selected_entry {
1453 Some(EntryOwned::FoldedDirs(selected_worktree_id, selected_entries)) => {
1454 selected_worktree_id == &worktree_id && selected_entries == dir_entries
1455 }
1456 _ => false,
1457 };
1458 let (item_id, label_element, icon) = {
1459 let name = dir_entries.iter().fold(String::new(), |mut name, entry| {
1460 if !name.is_empty() {
1461 name.push(std::path::MAIN_SEPARATOR)
1462 }
1463 name.push_str(&self.entry_name(&worktree_id, entry, cx));
1464 name
1465 });
1466
1467 let is_expanded = dir_entries.iter().all(|dir| {
1468 !self
1469 .collapsed_entries
1470 .contains(&CollapsedEntry::Dir(worktree_id, dir.id))
1471 });
1472 let is_ignored = dir_entries.iter().any(|entry| entry.is_ignored);
1473 let git_status = dir_entries.first().and_then(|entry| entry.git_status);
1474 let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
1475 let icon = if settings.folder_icons {
1476 FileIcons::get_folder_icon(is_expanded, cx)
1477 } else {
1478 FileIcons::get_chevron_icon(is_expanded, cx)
1479 }
1480 .map(Icon::from_path)
1481 .map(|icon| icon.color(color).into_any_element());
1482 (
1483 ElementId::from(
1484 dir_entries
1485 .last()
1486 .map(|entry| entry.id.to_proto())
1487 .unwrap_or_else(|| worktree_id.to_proto()) as usize,
1488 ),
1489 Label::new(name)
1490 .single_line()
1491 .color(color)
1492 .into_any_element(),
1493 icon.unwrap_or_else(empty_icon),
1494 )
1495 };
1496
1497 self.entry_element(
1498 EntryRef::FoldedDirs(worktree_id, dir_entries),
1499 item_id,
1500 depth,
1501 Some(icon),
1502 is_active,
1503 label_element,
1504 cx,
1505 )
1506 }
1507
1508 #[allow(clippy::too_many_arguments)]
1509 fn entry_element(
1510 &self,
1511 rendered_entry: EntryRef<'_>,
1512 item_id: ElementId,
1513 depth: usize,
1514 icon_element: Option<AnyElement>,
1515 is_active: bool,
1516 label_element: gpui::AnyElement,
1517 cx: &mut ViewContext<OutlinePanel>,
1518 ) -> Stateful<Div> {
1519 let settings = OutlinePanelSettings::get_global(cx);
1520 let rendered_entry = rendered_entry.to_owned_entry();
1521 div()
1522 .text_ui(cx)
1523 .id(item_id.clone())
1524 .child(
1525 ListItem::new(item_id)
1526 .indent_level(depth)
1527 .indent_step_size(px(settings.indent_size))
1528 .selected(is_active)
1529 .when_some(icon_element, |list_item, icon_element| {
1530 list_item.child(h_flex().child(icon_element))
1531 })
1532 .child(h_flex().h_6().child(label_element).ml_1())
1533 .on_click({
1534 let clicked_entry = rendered_entry.clone();
1535 cx.listener(move |outline_panel, event: &gpui::ClickEvent, cx| {
1536 if event.down.button == MouseButton::Right || event.down.first_mouse {
1537 return;
1538 }
1539 outline_panel.open_entry(&clicked_entry, cx);
1540 })
1541 })
1542 .on_secondary_mouse_down(cx.listener(
1543 move |outline_panel, event: &MouseDownEvent, cx| {
1544 // Stop propagation to prevent the catch-all context menu for the project
1545 // panel from being deployed.
1546 cx.stop_propagation();
1547 outline_panel.deploy_context_menu(
1548 event.position,
1549 rendered_entry.to_ref_entry(),
1550 cx,
1551 )
1552 },
1553 )),
1554 )
1555 .border_1()
1556 .border_r_2()
1557 .rounded_none()
1558 .hover(|style| {
1559 if is_active {
1560 style
1561 } else {
1562 let hover_color = cx.theme().colors().ghost_element_hover;
1563 style.bg(hover_color).border_color(hover_color)
1564 }
1565 })
1566 .when(is_active && self.focus_handle.contains_focused(cx), |div| {
1567 div.border_color(Color::Selected.color(cx))
1568 })
1569 }
1570
1571 fn entry_name(
1572 &self,
1573 worktree_id: &WorktreeId,
1574 entry: &Entry,
1575 cx: &ViewContext<OutlinePanel>,
1576 ) -> String {
1577 let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
1578 Some(worktree) => {
1579 let worktree = worktree.read(cx);
1580 match worktree.snapshot().root_entry() {
1581 Some(root_entry) => {
1582 if root_entry.id == entry.id {
1583 file_name(worktree.abs_path().as_ref())
1584 } else {
1585 let path = worktree.absolutize(entry.path.as_ref()).ok();
1586 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
1587 file_name(path)
1588 }
1589 }
1590 None => {
1591 let path = worktree.absolutize(entry.path.as_ref()).ok();
1592 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
1593 file_name(path)
1594 }
1595 }
1596 }
1597 None => file_name(entry.path.as_ref()),
1598 };
1599 name
1600 }
1601
1602 fn update_fs_entries(
1603 &mut self,
1604 active_editor: &View<Editor>,
1605 new_entries: HashSet<ExcerptId>,
1606 new_selected_entry: Option<EntryOwned>,
1607 debounce: Option<Duration>,
1608 prefetch: bool,
1609 cx: &mut ViewContext<Self>,
1610 ) {
1611 if !self.active {
1612 return;
1613 }
1614
1615 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
1616 let active_multi_buffer = active_editor.read(cx).buffer().clone();
1617 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
1618 let mut new_collapsed_entries = self.collapsed_entries.clone();
1619 let mut new_unfolded_dirs = self.unfolded_dirs.clone();
1620 let mut root_entries = HashSet::default();
1621 let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
1622 let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
1623 HashMap::default(),
1624 |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
1625 let buffer_id = buffer_snapshot.remote_id();
1626 let file = File::from_dyn(buffer_snapshot.file());
1627 let entry_id = file.and_then(|file| file.project_entry_id(cx));
1628 let worktree = file.map(|file| file.worktree.read(cx).snapshot());
1629 let is_new =
1630 new_entries.contains(&excerpt_id) || !self.excerpts.contains_key(&buffer_id);
1631 buffer_excerpts
1632 .entry(buffer_id)
1633 .or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
1634 .1
1635 .push(excerpt_id);
1636
1637 let outlines = match self
1638 .excerpts
1639 .get(&buffer_id)
1640 .and_then(|excerpts| excerpts.get(&excerpt_id))
1641 {
1642 Some(old_excerpt) => match &old_excerpt.outlines {
1643 ExcerptOutlines::Outlines(outlines) => {
1644 ExcerptOutlines::Outlines(outlines.clone())
1645 }
1646 ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
1647 ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
1648 },
1649 None => {
1650 new_collapsed_entries
1651 .insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1652 ExcerptOutlines::NotFetched
1653 }
1654 };
1655 new_excerpts.entry(buffer_id).or_default().insert(
1656 excerpt_id,
1657 Excerpt {
1658 range: excerpt_range,
1659 outlines,
1660 },
1661 );
1662 buffer_excerpts
1663 },
1664 );
1665
1666 self.loading_outlines = true;
1667 self.update_task = cx.spawn(|outline_panel, mut cx| async move {
1668 if let Some(debounce) = debounce {
1669 cx.background_executor().timer(debounce).await;
1670 }
1671 let Some((new_collapsed_entries, new_unfolded_dirs, new_fs_entries, new_depth_map)) =
1672 cx.background_executor()
1673 .spawn(async move {
1674 let mut processed_external_buffers = HashSet::default();
1675 let mut new_worktree_entries =
1676 HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
1677 let mut worktree_excerpts = HashMap::<
1678 WorktreeId,
1679 HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
1680 >::default();
1681 let mut external_excerpts = HashMap::default();
1682
1683 for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
1684 if is_new {
1685 match &worktree {
1686 Some(worktree) => {
1687 new_collapsed_entries
1688 .insert(CollapsedEntry::File(worktree.id(), buffer_id));
1689 }
1690 None => {
1691 new_collapsed_entries
1692 .insert(CollapsedEntry::ExternalFile(buffer_id));
1693 }
1694 }
1695
1696 for excerpt_id in &excerpts {
1697 new_collapsed_entries
1698 .insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
1699 }
1700 }
1701
1702 if let Some(worktree) = worktree {
1703 let worktree_id = worktree.id();
1704 let unfolded_dirs =
1705 new_unfolded_dirs.entry(worktree_id).or_default();
1706
1707 match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
1708 Some(entry) => {
1709 let mut traversal = worktree.traverse_from_path(
1710 true,
1711 true,
1712 true,
1713 entry.path.as_ref(),
1714 );
1715
1716 let mut entries_to_add = HashSet::default();
1717 worktree_excerpts
1718 .entry(worktree_id)
1719 .or_default()
1720 .insert(entry.id, (buffer_id, excerpts));
1721 let mut current_entry = entry;
1722 loop {
1723 if current_entry.is_dir() {
1724 let is_root =
1725 worktree.root_entry().map(|entry| entry.id)
1726 == Some(current_entry.id);
1727 if is_root {
1728 root_entries.insert(current_entry.id);
1729 if auto_fold_dirs {
1730 unfolded_dirs.insert(current_entry.id);
1731 }
1732 }
1733
1734 if is_new {
1735 new_collapsed_entries.remove(
1736 &CollapsedEntry::Dir(
1737 worktree_id,
1738 current_entry.id,
1739 ),
1740 );
1741 } else if new_collapsed_entries.contains(
1742 &CollapsedEntry::Dir(
1743 worktree_id,
1744 current_entry.id,
1745 ),
1746 ) {
1747 entries_to_add.clear();
1748 }
1749 }
1750
1751 let new_entry_added =
1752 entries_to_add.insert(current_entry);
1753 if new_entry_added && traversal.back_to_parent() {
1754 if let Some(parent_entry) = traversal.entry() {
1755 current_entry = parent_entry.clone();
1756 continue;
1757 }
1758 }
1759 break;
1760 }
1761 new_worktree_entries
1762 .entry(worktree_id)
1763 .or_insert_with(|| {
1764 (worktree.clone(), HashSet::default())
1765 })
1766 .1
1767 .extend(entries_to_add);
1768 }
1769 None => {
1770 if processed_external_buffers.insert(buffer_id) {
1771 external_excerpts
1772 .entry(buffer_id)
1773 .or_insert_with(|| Vec::new())
1774 .extend(excerpts);
1775 }
1776 }
1777 }
1778 } else if processed_external_buffers.insert(buffer_id) {
1779 external_excerpts
1780 .entry(buffer_id)
1781 .or_insert_with(|| Vec::new())
1782 .extend(excerpts);
1783 }
1784 }
1785
1786 #[derive(Clone, Copy, Default)]
1787 struct Children {
1788 files: usize,
1789 dirs: usize,
1790 }
1791 let mut children_count =
1792 HashMap::<WorktreeId, HashMap<PathBuf, Children>>::default();
1793
1794 let worktree_entries = new_worktree_entries
1795 .into_iter()
1796 .map(|(worktree_id, (worktree_snapshot, entries))| {
1797 let mut entries = entries.into_iter().collect::<Vec<_>>();
1798 // For a proper git status propagation, we have to keep the entries sorted lexicographically.
1799 entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
1800 worktree_snapshot.propagate_git_statuses(&mut entries);
1801 project::sort_worktree_entries(&mut entries);
1802 (worktree_id, entries)
1803 })
1804 .flat_map(|(worktree_id, entries)| {
1805 {
1806 entries
1807 .into_iter()
1808 .filter_map(|entry| {
1809 if auto_fold_dirs {
1810 if let Some(parent) = entry.path.parent() {
1811 let children = children_count
1812 .entry(worktree_id)
1813 .or_default()
1814 .entry(parent.to_path_buf())
1815 .or_default();
1816 if entry.is_dir() {
1817 children.dirs += 1;
1818 } else {
1819 children.files += 1;
1820 }
1821 }
1822 }
1823
1824 if entry.is_dir() {
1825 Some(FsEntry::Directory(worktree_id, entry))
1826 } else {
1827 let (buffer_id, excerpts) = worktree_excerpts
1828 .get_mut(&worktree_id)
1829 .and_then(|worktree_excerpts| {
1830 worktree_excerpts.remove(&entry.id)
1831 })?;
1832 Some(FsEntry::File(
1833 worktree_id,
1834 entry,
1835 buffer_id,
1836 excerpts,
1837 ))
1838 }
1839 })
1840 .collect::<Vec<_>>()
1841 }
1842 })
1843 .collect::<Vec<_>>();
1844
1845 let mut visited_dirs = Vec::new();
1846 let mut new_depth_map = HashMap::default();
1847 let new_visible_entries = external_excerpts
1848 .into_iter()
1849 .sorted_by_key(|(id, _)| *id)
1850 .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
1851 .chain(worktree_entries)
1852 .filter(|visible_item| {
1853 match visible_item {
1854 FsEntry::Directory(worktree_id, dir_entry) => {
1855 let parent_id = back_to_common_visited_parent(
1856 &mut visited_dirs,
1857 worktree_id,
1858 dir_entry,
1859 );
1860
1861 visited_dirs.push((dir_entry.id, dir_entry.path.clone()));
1862 let depth = if root_entries.contains(&dir_entry.id) {
1863 0
1864 } else if auto_fold_dirs {
1865 let (parent_folded, parent_depth) = match parent_id {
1866 Some((worktree_id, id)) => (
1867 new_unfolded_dirs.get(&worktree_id).map_or(
1868 true,
1869 |unfolded_dirs| {
1870 !unfolded_dirs.contains(&id)
1871 },
1872 ),
1873 new_depth_map
1874 .get(&(worktree_id, id))
1875 .copied()
1876 .unwrap_or(0),
1877 ),
1878
1879 None => (false, 0),
1880 };
1881
1882 let children = children_count
1883 .get(&worktree_id)
1884 .and_then(|children_count| {
1885 children_count
1886 .get(&dir_entry.path.to_path_buf())
1887 })
1888 .copied()
1889 .unwrap_or_default();
1890 let folded = if children.dirs > 1
1891 || (children.dirs == 1 && children.files > 0)
1892 || (children.dirs == 0
1893 && visited_dirs
1894 .last()
1895 .map(|(parent_dir_id, _)| {
1896 root_entries.contains(parent_dir_id)
1897 })
1898 .unwrap_or(true))
1899 {
1900 new_unfolded_dirs
1901 .entry(*worktree_id)
1902 .or_default()
1903 .insert(dir_entry.id);
1904 false
1905 } else {
1906 new_unfolded_dirs.get(&worktree_id).map_or(
1907 true,
1908 |unfolded_dirs| {
1909 !unfolded_dirs.contains(&dir_entry.id)
1910 },
1911 )
1912 };
1913
1914 if parent_folded && folded {
1915 parent_depth
1916 } else {
1917 parent_depth + 1
1918 }
1919 } else {
1920 parent_id
1921 .and_then(|(worktree_id, id)| {
1922 new_depth_map.get(&(worktree_id, id)).copied()
1923 })
1924 .unwrap_or(0)
1925 + 1
1926 };
1927 new_depth_map.insert((*worktree_id, dir_entry.id), depth);
1928 }
1929 FsEntry::File(worktree_id, file_entry, ..) => {
1930 let parent_id = back_to_common_visited_parent(
1931 &mut visited_dirs,
1932 worktree_id,
1933 file_entry,
1934 );
1935 let depth = if root_entries.contains(&file_entry.id) {
1936 0
1937 } else {
1938 parent_id
1939 .and_then(|(worktree_id, id)| {
1940 new_depth_map.get(&(worktree_id, id)).copied()
1941 })
1942 .unwrap_or(0)
1943 + 1
1944 };
1945 new_depth_map.insert((*worktree_id, file_entry.id), depth);
1946 }
1947 FsEntry::ExternalFile(..) => {
1948 visited_dirs.clear();
1949 }
1950 }
1951
1952 true
1953 })
1954 .collect::<Vec<_>>();
1955
1956 anyhow::Ok((
1957 new_collapsed_entries,
1958 new_unfolded_dirs,
1959 new_visible_entries,
1960 new_depth_map,
1961 ))
1962 })
1963 .await
1964 .log_err()
1965 else {
1966 return;
1967 };
1968
1969 outline_panel
1970 .update(&mut cx, |outline_panel, cx| {
1971 outline_panel.loading_outlines = false;
1972 outline_panel.excerpts = new_excerpts;
1973 outline_panel.collapsed_entries = new_collapsed_entries;
1974 outline_panel.unfolded_dirs = new_unfolded_dirs;
1975 outline_panel.fs_entries = new_fs_entries;
1976 outline_panel.fs_entries_depth = new_depth_map;
1977 outline_panel.cached_entries_with_depth = None;
1978 if new_selected_entry.is_some() {
1979 outline_panel.selected_entry = new_selected_entry;
1980 }
1981 if prefetch {
1982 let range = if outline_panel.last_visible_range.is_empty() {
1983 0..(outline_panel.entries_with_depths(cx).len() / 4).min(50)
1984 } else {
1985 outline_panel.last_visible_range.clone()
1986 };
1987 outline_panel.fetch_outlines(&range, cx);
1988 }
1989
1990 outline_panel.autoscroll(cx);
1991 cx.notify();
1992 })
1993 .ok();
1994 });
1995 }
1996
1997 fn replace_visible_entries(
1998 &mut self,
1999 new_active_editor: View<Editor>,
2000 cx: &mut ViewContext<Self>,
2001 ) {
2002 let new_selected_entry = self.location_for_editor_selection(&new_active_editor, cx);
2003 self.clear_previous();
2004 self.active_item = Some(ActiveItem {
2005 item_id: new_active_editor.item_id(),
2006 _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, cx),
2007 active_editor: new_active_editor.downgrade(),
2008 });
2009 let new_entries =
2010 HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
2011 self.update_fs_entries(
2012 &new_active_editor,
2013 new_entries,
2014 new_selected_entry,
2015 None,
2016 true,
2017 cx,
2018 );
2019 }
2020
2021 fn clear_previous(&mut self) {
2022 self.collapsed_entries.clear();
2023 self.unfolded_dirs.clear();
2024 self.last_visible_range = 0..0;
2025 self.selected_entry = None;
2026 self.update_task = Task::ready(());
2027 self.active_item = None;
2028 self.fs_entries.clear();
2029 self.fs_entries_depth.clear();
2030 self.outline_fetch_tasks.clear();
2031 self.excerpts.clear();
2032 self.cached_entries_with_depth = None;
2033 }
2034
2035 fn location_for_editor_selection(
2036 &self,
2037 editor: &View<Editor>,
2038 cx: &mut ViewContext<Self>,
2039 ) -> Option<EntryOwned> {
2040 let selection = editor
2041 .read(cx)
2042 .selections
2043 .newest::<language::Point>(cx)
2044 .head();
2045 let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx));
2046 let multi_buffer = editor.read(cx).buffer();
2047 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
2048 let (excerpt_id, buffer, _) = editor
2049 .read(cx)
2050 .buffer()
2051 .read(cx)
2052 .excerpt_containing(selection, cx)?;
2053 let buffer_id = buffer.read(cx).remote_id();
2054 let selection_display_point = selection.to_display_point(&editor_snapshot);
2055
2056 let excerpt_outlines = self
2057 .excerpts
2058 .get(&buffer_id)
2059 .and_then(|excerpts| excerpts.get(&excerpt_id))
2060 .into_iter()
2061 .flat_map(|excerpt| excerpt.iter_outlines())
2062 .flat_map(|outline| {
2063 let start = multi_buffer_snapshot
2064 .anchor_in_excerpt(excerpt_id, outline.range.start)?
2065 .to_display_point(&editor_snapshot);
2066 let end = multi_buffer_snapshot
2067 .anchor_in_excerpt(excerpt_id, outline.range.end)?
2068 .to_display_point(&editor_snapshot);
2069 Some((start..end, outline))
2070 })
2071 .collect::<Vec<_>>();
2072
2073 let mut matching_outline_indices = Vec::new();
2074 let mut children = HashMap::default();
2075 let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
2076
2077 for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
2078 if outline_range
2079 .to_inclusive()
2080 .contains(&selection_display_point)
2081 {
2082 matching_outline_indices.push(i);
2083 } else if (outline_range.start.row()..outline_range.end.row())
2084 .to_inclusive()
2085 .contains(&selection_display_point.row())
2086 {
2087 matching_outline_indices.push(i);
2088 }
2089
2090 while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
2091 if parent_outline.depth >= outline.depth
2092 || !parent_range.contains(&outline_range.start)
2093 {
2094 parents_stack.pop();
2095 } else {
2096 break;
2097 }
2098 }
2099 if let Some((_, _, parent_index)) = parents_stack.last_mut() {
2100 children
2101 .entry(*parent_index)
2102 .or_insert_with(Vec::new)
2103 .push(i);
2104 }
2105 parents_stack.push((outline_range, outline, i));
2106 }
2107
2108 let outline_item = matching_outline_indices
2109 .into_iter()
2110 .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
2111 .filter(|(i, _)| {
2112 children
2113 .get(i)
2114 .map(|children| {
2115 children.iter().all(|child_index| {
2116 excerpt_outlines
2117 .get(*child_index)
2118 .map(|(child_range, _)| child_range.start > selection_display_point)
2119 .unwrap_or(false)
2120 })
2121 })
2122 .unwrap_or(true)
2123 })
2124 .min_by_key(|(_, (outline_range, outline))| {
2125 let distance_from_start = if outline_range.start > selection_display_point {
2126 outline_range.start - selection_display_point
2127 } else {
2128 selection_display_point - outline_range.start
2129 };
2130 let distance_from_end = if outline_range.end > selection_display_point {
2131 outline_range.end - selection_display_point
2132 } else {
2133 selection_display_point - outline_range.end
2134 };
2135
2136 (
2137 cmp::Reverse(outline.depth),
2138 distance_from_start + distance_from_end,
2139 )
2140 })
2141 .map(|(_, (_, outline))| *outline)
2142 .cloned();
2143
2144 let closest_container = match outline_item {
2145 Some(outline) => EntryOwned::Outline(buffer_id, excerpt_id, outline),
2146 None => self
2147 .cached_entries_with_depth
2148 .iter()
2149 .flatten()
2150 .rev()
2151 .find_map(|(_, entry)| match entry {
2152 EntryOwned::Excerpt(entry_buffer_id, entry_excerpt_id, _) => {
2153 if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
2154 Some(entry.clone())
2155 } else {
2156 None
2157 }
2158 }
2159 EntryOwned::Entry(
2160 FsEntry::ExternalFile(file_buffer_id, file_excerpts)
2161 | FsEntry::File(_, _, file_buffer_id, file_excerpts),
2162 ) => {
2163 if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
2164 Some(entry.clone())
2165 } else {
2166 None
2167 }
2168 }
2169 _ => None,
2170 })?,
2171 };
2172 Some(closest_container)
2173 }
2174
2175 fn fetch_outlines(&mut self, range: &Range<usize>, cx: &mut ViewContext<Self>) {
2176 let range_len = range.len();
2177 let half_range = range_len / 2;
2178 let entries = self.entries_with_depths(cx);
2179 let expanded_range =
2180 range.start.saturating_sub(half_range)..(range.end + half_range).min(entries.len());
2181
2182 let excerpt_fetch_ranges = self.excerpt_fetch_ranges(expanded_range, cx);
2183 if excerpt_fetch_ranges.is_empty() {
2184 return;
2185 }
2186
2187 let syntax_theme = cx.theme().syntax().clone();
2188 for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
2189 for (excerpt_id, excerpt_range) in excerpt_ranges {
2190 let syntax_theme = syntax_theme.clone();
2191 let buffer_snapshot = buffer_snapshot.clone();
2192 self.outline_fetch_tasks.insert(
2193 (buffer_id, excerpt_id),
2194 cx.spawn(|outline_panel, mut cx| async move {
2195 let fetched_outlines = cx
2196 .background_executor()
2197 .spawn(async move {
2198 buffer_snapshot
2199 .outline_items_containing(
2200 excerpt_range.context,
2201 false,
2202 Some(&syntax_theme),
2203 )
2204 .unwrap_or_default()
2205 })
2206 .await;
2207 outline_panel
2208 .update(&mut cx, |outline_panel, cx| {
2209 if let Some(excerpt) = outline_panel
2210 .excerpts
2211 .entry(buffer_id)
2212 .or_default()
2213 .get_mut(&excerpt_id)
2214 {
2215 excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
2216 }
2217 outline_panel.cached_entries_with_depth = None;
2218 cx.notify();
2219 })
2220 .ok();
2221 }),
2222 );
2223 }
2224 }
2225 }
2226
2227 fn entries_with_depths(&mut self, cx: &AppContext) -> &[(usize, EntryOwned)] {
2228 let is_singleton = self.is_singleton_active(cx);
2229 self.cached_entries_with_depth.get_or_insert_with(|| {
2230 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2231 let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
2232 let mut entries = Vec::new();
2233
2234 for entry in &self.fs_entries {
2235 let depth = match entry {
2236 FsEntry::Directory(worktree_id, dir_entry) => {
2237 let depth = self
2238 .fs_entries_depth
2239 .get(&(*worktree_id, dir_entry.id))
2240 .copied()
2241 .unwrap_or(0);
2242 if auto_fold_dirs {
2243 let folded = self
2244 .unfolded_dirs
2245 .get(worktree_id)
2246 .map_or(true, |unfolded_dirs| {
2247 !unfolded_dirs.contains(&dir_entry.id)
2248 });
2249 if folded {
2250 if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) =
2251 folded_dirs_entry.take()
2252 {
2253 if worktree_id == &folded_worktree_id
2254 && dir_entry.path.parent()
2255 == folded_dirs.last().map(|entry| entry.path.as_ref())
2256 {
2257 folded_dirs.push(dir_entry.clone());
2258 folded_dirs_entry =
2259 Some((folded_depth, folded_worktree_id, folded_dirs))
2260 } else {
2261 entries.push((
2262 folded_depth,
2263 EntryOwned::FoldedDirs(folded_worktree_id, folded_dirs),
2264 ));
2265 folded_dirs_entry =
2266 Some((depth, *worktree_id, vec![dir_entry.clone()]))
2267 }
2268 } else {
2269 folded_dirs_entry =
2270 Some((depth, *worktree_id, vec![dir_entry.clone()]))
2271 }
2272
2273 continue;
2274 }
2275 }
2276 depth
2277 }
2278 FsEntry::ExternalFile(..) => 0,
2279 FsEntry::File(worktree_id, file_entry, ..) => self
2280 .fs_entries_depth
2281 .get(&(*worktree_id, file_entry.id))
2282 .copied()
2283 .unwrap_or(0),
2284 };
2285 if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
2286 entries.push((
2287 folded_depth,
2288 EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2289 ));
2290 }
2291
2292 entries.push((depth, EntryOwned::Entry(entry.clone())));
2293
2294 let excerpts_to_consider = match entry {
2295 FsEntry::File(worktree_id, _, buffer_id, entry_excerpts) => {
2296 if is_singleton
2297 || !self
2298 .collapsed_entries
2299 .contains(&CollapsedEntry::File(*worktree_id, *buffer_id))
2300 {
2301 Some((*buffer_id, entry_excerpts))
2302 } else {
2303 None
2304 }
2305 }
2306 FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
2307 if is_singleton
2308 || !self
2309 .collapsed_entries
2310 .contains(&CollapsedEntry::ExternalFile(*buffer_id))
2311 {
2312 Some((*buffer_id, entry_excerpts))
2313 } else {
2314 None
2315 }
2316 }
2317 _ => None,
2318 };
2319 if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
2320 if let Some(excerpts) = self.excerpts.get(&buffer_id) {
2321 for &entry_excerpt in entry_excerpts {
2322 let Some(excerpt) = excerpts.get(&entry_excerpt) else {
2323 continue;
2324 };
2325 let excerpt_depth = depth + 1;
2326 entries.push((
2327 excerpt_depth,
2328 EntryOwned::Excerpt(
2329 buffer_id,
2330 entry_excerpt,
2331 excerpt.range.clone(),
2332 ),
2333 ));
2334
2335 let mut outline_base_depth = excerpt_depth + 1;
2336 if is_singleton {
2337 outline_base_depth = 0;
2338 entries.clear();
2339 } else if self
2340 .collapsed_entries
2341 .contains(&CollapsedEntry::Excerpt(buffer_id, entry_excerpt))
2342 {
2343 continue;
2344 }
2345
2346 for outline in excerpt.iter_outlines() {
2347 entries.push((
2348 outline_base_depth + outline.depth,
2349 EntryOwned::Outline(buffer_id, entry_excerpt, outline.clone()),
2350 ));
2351 }
2352 if is_singleton && entries.is_empty() {
2353 entries.push((0, EntryOwned::Entry(entry.clone())));
2354 }
2355 }
2356 }
2357 }
2358 }
2359 if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
2360 entries.push((
2361 folded_depth,
2362 EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2363 ));
2364 }
2365 entries
2366 })
2367 }
2368
2369 fn is_singleton_active(&self, cx: &AppContext) -> bool {
2370 self.active_item
2371 .as_ref()
2372 .and_then(|active_item| {
2373 Some(
2374 active_item
2375 .active_editor
2376 .upgrade()?
2377 .read(cx)
2378 .buffer()
2379 .read(cx)
2380 .is_singleton(),
2381 )
2382 })
2383 .unwrap_or(false)
2384 }
2385
2386 fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
2387 self.outline_fetch_tasks.clear();
2388 let mut ids = ids.into_iter().collect::<HashSet<_>>();
2389 for excerpts in self.excerpts.values_mut() {
2390 ids.retain(|id| {
2391 if let Some(excerpt) = excerpts.get_mut(id) {
2392 excerpt.invalidate_outlines();
2393 false
2394 } else {
2395 true
2396 }
2397 });
2398 if ids.is_empty() {
2399 break;
2400 }
2401 }
2402 }
2403
2404 fn excerpt_fetch_ranges(
2405 &self,
2406 entry_range: Range<usize>,
2407 cx: &AppContext,
2408 ) -> HashMap<
2409 BufferId,
2410 (
2411 BufferSnapshot,
2412 HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
2413 ),
2414 > {
2415 match self.cached_entries_with_depth.as_ref() {
2416 Some(entries) => entries.get(entry_range).into_iter().flatten().fold(
2417 HashMap::default(),
2418 |mut excerpts_to_fetch, (_, entry)| {
2419 match entry {
2420 EntryOwned::Entry(FsEntry::File(_, _, buffer_id, file_excerpts))
2421 | EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, file_excerpts)) => {
2422 let excerpts = self.excerpts.get(&buffer_id);
2423 for &file_excerpt in file_excerpts {
2424 if let Some(excerpt) = excerpts
2425 .and_then(|excerpts| excerpts.get(&file_excerpt))
2426 .filter(|excerpt| excerpt.should_fetch_outlines())
2427 {
2428 match excerpts_to_fetch.entry(*buffer_id) {
2429 hash_map::Entry::Occupied(mut o) => {
2430 o.get_mut()
2431 .1
2432 .insert(file_excerpt, excerpt.range.clone());
2433 }
2434 hash_map::Entry::Vacant(v) => {
2435 if let Some(buffer_snapshot) =
2436 self.buffer_snapshot_for_id(*buffer_id, cx)
2437 {
2438 v.insert((buffer_snapshot, HashMap::default()))
2439 .1
2440 .insert(file_excerpt, excerpt.range.clone());
2441 }
2442 }
2443 }
2444 }
2445 }
2446 }
2447 EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
2448 if let Some(excerpt) = self
2449 .excerpts
2450 .get(&buffer_id)
2451 .and_then(|excerpts| excerpts.get(&excerpt_id))
2452 .filter(|excerpt| excerpt.should_fetch_outlines())
2453 {
2454 match excerpts_to_fetch.entry(*buffer_id) {
2455 hash_map::Entry::Occupied(mut o) => {
2456 o.get_mut().1.insert(*excerpt_id, excerpt.range.clone());
2457 }
2458 hash_map::Entry::Vacant(v) => {
2459 if let Some(buffer_snapshot) =
2460 self.buffer_snapshot_for_id(*buffer_id, cx)
2461 {
2462 v.insert((buffer_snapshot, HashMap::default()))
2463 .1
2464 .insert(*excerpt_id, excerpt.range.clone());
2465 }
2466 }
2467 }
2468 }
2469 }
2470 _ => {}
2471 }
2472 excerpts_to_fetch
2473 },
2474 ),
2475 None => HashMap::default(),
2476 }
2477 }
2478
2479 fn buffer_snapshot_for_id(
2480 &self,
2481 buffer_id: BufferId,
2482 cx: &AppContext,
2483 ) -> Option<BufferSnapshot> {
2484 let editor = self.active_item.as_ref()?.active_editor.upgrade()?;
2485 Some(
2486 editor
2487 .read(cx)
2488 .buffer()
2489 .read(cx)
2490 .buffer(buffer_id)?
2491 .read(cx)
2492 .snapshot(),
2493 )
2494 }
2495
2496 fn abs_path(&self, entry: &EntryOwned, cx: &AppContext) -> Option<PathBuf> {
2497 match entry {
2498 EntryOwned::Entry(
2499 FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
2500 ) => self
2501 .buffer_snapshot_for_id(*buffer_id, cx)
2502 .and_then(|buffer_snapshot| {
2503 let file = File::from_dyn(buffer_snapshot.file())?;
2504 file.worktree.read(cx).absolutize(&file.path).ok()
2505 }),
2506 EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => self
2507 .project
2508 .read(cx)
2509 .worktree_for_id(*worktree_id, cx)?
2510 .read(cx)
2511 .absolutize(&entry.path)
2512 .ok(),
2513 EntryOwned::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
2514 self.project
2515 .read(cx)
2516 .worktree_for_id(*worktree_id, cx)
2517 .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
2518 }),
2519 EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
2520 }
2521 }
2522
2523 fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option<PathBuf> {
2524 match entry {
2525 FsEntry::ExternalFile(buffer_id, _) => self
2526 .buffer_snapshot_for_id(*buffer_id, cx)
2527 .and_then(|buffer_snapshot| Some(buffer_snapshot.file()?.path().to_path_buf())),
2528 FsEntry::Directory(_, entry) => Some(entry.path.to_path_buf()),
2529 FsEntry::File(_, entry, ..) => Some(entry.path.to_path_buf()),
2530 }
2531 }
2532}
2533
2534fn back_to_common_visited_parent(
2535 visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
2536 worktree_id: &WorktreeId,
2537 new_entry: &Entry,
2538) -> Option<(WorktreeId, ProjectEntryId)> {
2539 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
2540 match new_entry.path.parent() {
2541 Some(parent_path) => {
2542 if parent_path == visited_path.as_ref() {
2543 return Some((*worktree_id, *visited_dir_id));
2544 }
2545 }
2546 None => {
2547 break;
2548 }
2549 }
2550 visited_dirs.pop();
2551 }
2552 None
2553}
2554
2555fn file_name(path: &Path) -> String {
2556 let mut current_path = path;
2557 loop {
2558 if let Some(file_name) = current_path.file_name() {
2559 return file_name.to_string_lossy().into_owned();
2560 }
2561 match current_path.parent() {
2562 Some(parent) => current_path = parent,
2563 None => return path.to_string_lossy().into_owned(),
2564 }
2565 }
2566}
2567
2568impl Panel for OutlinePanel {
2569 fn persistent_name() -> &'static str {
2570 "Outline Panel"
2571 }
2572
2573 fn position(&self, cx: &WindowContext) -> DockPosition {
2574 match OutlinePanelSettings::get_global(cx).dock {
2575 OutlinePanelDockPosition::Left => DockPosition::Left,
2576 OutlinePanelDockPosition::Right => DockPosition::Right,
2577 }
2578 }
2579
2580 fn position_is_valid(&self, position: DockPosition) -> bool {
2581 matches!(position, DockPosition::Left | DockPosition::Right)
2582 }
2583
2584 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2585 settings::update_settings_file::<OutlinePanelSettings>(
2586 self.fs.clone(),
2587 cx,
2588 move |settings| {
2589 let dock = match position {
2590 DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
2591 DockPosition::Right => OutlinePanelDockPosition::Right,
2592 };
2593 settings.dock = Some(dock);
2594 },
2595 );
2596 }
2597
2598 fn size(&self, cx: &WindowContext) -> Pixels {
2599 self.width
2600 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
2601 }
2602
2603 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
2604 self.width = size;
2605 self.serialize(cx);
2606 cx.notify();
2607 }
2608
2609 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
2610 OutlinePanelSettings::get_global(cx)
2611 .button
2612 .then(|| IconName::ListTree)
2613 }
2614
2615 fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
2616 Some("Outline Panel")
2617 }
2618
2619 fn toggle_action(&self) -> Box<dyn Action> {
2620 Box::new(ToggleFocus)
2621 }
2622
2623 fn starts_open(&self, _: &WindowContext) -> bool {
2624 self.active
2625 }
2626
2627 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2628 let old_active = self.active;
2629 self.active = active;
2630 if active && old_active != active {
2631 if let Some(active_editor) = self
2632 .active_item
2633 .as_ref()
2634 .and_then(|item| item.active_editor.upgrade())
2635 {
2636 if self.active_item.as_ref().map(|item| item.item_id)
2637 == Some(active_editor.item_id())
2638 {
2639 let new_selected_entry = self.location_for_editor_selection(&active_editor, cx);
2640 self.update_fs_entries(
2641 &active_editor,
2642 HashSet::default(),
2643 new_selected_entry,
2644 None,
2645 true,
2646 cx,
2647 )
2648 } else {
2649 self.replace_visible_entries(active_editor, cx);
2650 }
2651 }
2652 }
2653 self.serialize(cx);
2654 }
2655}
2656
2657impl FocusableView for OutlinePanel {
2658 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
2659 self.focus_handle.clone()
2660 }
2661}
2662
2663impl EventEmitter<Event> for OutlinePanel {}
2664
2665impl EventEmitter<PanelEvent> for OutlinePanel {}
2666
2667impl Render for OutlinePanel {
2668 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2669 let project = self.project.read(cx);
2670 if self.fs_entries.is_empty() {
2671 let header = if self.loading_outlines {
2672 "Loading outlines"
2673 } else {
2674 "No outlines available"
2675 };
2676 v_flex()
2677 .id("empty-outline_panel")
2678 .justify_center()
2679 .size_full()
2680 .p_4()
2681 .track_focus(&self.focus_handle)
2682 .child(h_flex().justify_center().child(Label::new(header)))
2683 .child(
2684 h_flex()
2685 .pt(Spacing::Small.rems(cx))
2686 .justify_center()
2687 .child({
2688 let keystroke = match self.position(cx) {
2689 DockPosition::Left => {
2690 cx.keystroke_text_for(&workspace::ToggleLeftDock)
2691 }
2692 DockPosition::Bottom => {
2693 cx.keystroke_text_for(&workspace::ToggleBottomDock)
2694 }
2695 DockPosition::Right => {
2696 cx.keystroke_text_for(&workspace::ToggleRightDock)
2697 }
2698 };
2699 Label::new(format!("Toggle this panel with {keystroke}",))
2700 }),
2701 )
2702 } else {
2703 h_flex()
2704 .id("outline-panel")
2705 .size_full()
2706 .relative()
2707 .key_context(self.dispatch_context(cx))
2708 .on_action(cx.listener(Self::open))
2709 .on_action(cx.listener(Self::select_next))
2710 .on_action(cx.listener(Self::select_prev))
2711 .on_action(cx.listener(Self::select_first))
2712 .on_action(cx.listener(Self::select_last))
2713 .on_action(cx.listener(Self::select_parent))
2714 .on_action(cx.listener(Self::expand_selected_entry))
2715 .on_action(cx.listener(Self::collapse_selected_entry))
2716 .on_action(cx.listener(Self::collapse_all_entries))
2717 .on_action(cx.listener(Self::copy_path))
2718 .on_action(cx.listener(Self::copy_relative_path))
2719 .on_action(cx.listener(Self::unfold_directory))
2720 .on_action(cx.listener(Self::fold_directory))
2721 .when(project.is_local(), |el| {
2722 el.on_action(cx.listener(Self::reveal_in_finder))
2723 .on_action(cx.listener(Self::open_in_terminal))
2724 })
2725 .on_mouse_down(
2726 MouseButton::Right,
2727 cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
2728 if let Some(entry) = outline_panel.selected_entry.clone() {
2729 outline_panel.deploy_context_menu(
2730 event.position,
2731 entry.to_ref_entry(),
2732 cx,
2733 )
2734 } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
2735 outline_panel.deploy_context_menu(
2736 event.position,
2737 EntryRef::Entry(&entry),
2738 cx,
2739 )
2740 }
2741 }),
2742 )
2743 .track_focus(&self.focus_handle)
2744 .child({
2745 let items_len = self.entries_with_depths(cx).len();
2746 uniform_list(cx.view().clone(), "entries", items_len, {
2747 move |outline_panel, range, cx| {
2748 outline_panel.last_visible_range = range.clone();
2749 outline_panel.fetch_outlines(&range, cx);
2750 let entries = outline_panel.entries_with_depths(cx).get(range);
2751 entries
2752 .map(|entries| entries.to_vec())
2753 .unwrap_or_default()
2754 .into_iter()
2755 .filter_map(|(depth, entry)| match entry {
2756 EntryOwned::Entry(entry) => {
2757 Some(outline_panel.render_entry(&entry, depth, cx))
2758 }
2759 EntryOwned::FoldedDirs(worktree_id, entries) => {
2760 Some(outline_panel.render_folded_dirs(
2761 worktree_id,
2762 &entries,
2763 depth,
2764 cx,
2765 ))
2766 }
2767 EntryOwned::Excerpt(buffer_id, excerpt_id, excerpt) => {
2768 outline_panel.render_excerpt(
2769 buffer_id, excerpt_id, &excerpt, depth, cx,
2770 )
2771 }
2772 EntryOwned::Outline(buffer_id, excerpt_id, outline) => {
2773 Some(outline_panel.render_outline(
2774 buffer_id, excerpt_id, &outline, depth, cx,
2775 ))
2776 }
2777 })
2778 .collect()
2779 }
2780 })
2781 .size_full()
2782 .track_scroll(self.scroll_handle.clone())
2783 })
2784 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2785 deferred(
2786 anchored()
2787 .position(*position)
2788 .anchor(gpui::AnchorCorner::TopLeft)
2789 .child(menu.clone()),
2790 )
2791 .with_priority(1)
2792 }))
2793 }
2794 }
2795}
2796
2797fn subscribe_for_editor_events(
2798 editor: &View<Editor>,
2799 cx: &mut ViewContext<OutlinePanel>,
2800) -> Subscription {
2801 let debounce = Some(Duration::from_millis(UPDATE_DEBOUNCE_MILLIS));
2802 cx.subscribe(
2803 editor,
2804 move |outline_panel, editor, e: &EditorEvent, cx| match e {
2805 EditorEvent::SelectionsChanged { local: true } => {
2806 outline_panel.reveal_entry_for_selection(&editor, cx);
2807 cx.notify();
2808 }
2809 EditorEvent::ExcerptsAdded { excerpts, .. } => {
2810 outline_panel.update_fs_entries(
2811 &editor,
2812 excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
2813 None,
2814 debounce,
2815 false,
2816 cx,
2817 );
2818 }
2819 EditorEvent::ExcerptsRemoved { ids } => {
2820 let mut ids = ids.into_iter().collect::<HashSet<_>>();
2821 for excerpts in outline_panel.excerpts.values_mut() {
2822 excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
2823 if ids.is_empty() {
2824 break;
2825 }
2826 }
2827 outline_panel.update_fs_entries(
2828 &editor,
2829 HashSet::default(),
2830 None,
2831 debounce,
2832 false,
2833 cx,
2834 );
2835 }
2836 EditorEvent::ExcerptsExpanded { ids } => {
2837 outline_panel.invalidate_outlines(ids);
2838 outline_panel.update_fs_entries(
2839 &editor,
2840 HashSet::default(),
2841 None,
2842 debounce,
2843 true,
2844 cx,
2845 );
2846 }
2847 EditorEvent::ExcerptsEdited { ids } => {
2848 outline_panel.invalidate_outlines(ids);
2849 outline_panel.update_fs_entries(
2850 &editor,
2851 HashSet::default(),
2852 None,
2853 debounce,
2854 true,
2855 cx,
2856 );
2857 }
2858 EditorEvent::Reparsed(buffer_id) => {
2859 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
2860 for (_, excerpt) in excerpts {
2861 excerpt.invalidate_outlines();
2862 }
2863 }
2864 outline_panel.update_fs_entries(
2865 &editor,
2866 HashSet::default(),
2867 None,
2868 debounce,
2869 true,
2870 cx,
2871 );
2872 }
2873 _ => {}
2874 },
2875 )
2876}
2877
2878fn empty_icon() -> AnyElement {
2879 h_flex()
2880 .size(IconSize::default().rems())
2881 .invisible()
2882 .flex_none()
2883 .into_any_element()
2884}