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