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)
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)
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 => {
1626 new_collapsed_entries
1627 .insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1628 ExcerptOutlines::NotFetched
1629 }
1630 };
1631 new_excerpts.entry(buffer_id).or_default().insert(
1632 excerpt_id,
1633 Excerpt {
1634 range: excerpt_range,
1635 outlines,
1636 },
1637 );
1638 buffer_excerpts
1639 },
1640 );
1641
1642 self.updating_fs_entries = true;
1643 self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
1644 if let Some(debounce) = debounce {
1645 cx.background_executor().timer(debounce).await;
1646 }
1647 let Some((
1648 new_collapsed_entries,
1649 new_unfolded_dirs,
1650 new_fs_entries,
1651 new_depth_map,
1652 new_children_count,
1653 )) = cx
1654 .background_executor()
1655 .spawn(async move {
1656 let mut processed_external_buffers = HashSet::default();
1657 let mut new_worktree_entries =
1658 HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
1659 let mut worktree_excerpts = HashMap::<
1660 WorktreeId,
1661 HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
1662 >::default();
1663 let mut external_excerpts = HashMap::default();
1664
1665 for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
1666 if is_new {
1667 match &worktree {
1668 Some(worktree) => {
1669 new_collapsed_entries
1670 .insert(CollapsedEntry::File(worktree.id(), buffer_id));
1671 }
1672 None => {
1673 new_collapsed_entries
1674 .insert(CollapsedEntry::ExternalFile(buffer_id));
1675 }
1676 }
1677
1678 for excerpt_id in &excerpts {
1679 new_collapsed_entries
1680 .insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
1681 }
1682 }
1683
1684 if let Some(worktree) = worktree {
1685 let worktree_id = worktree.id();
1686 let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
1687
1688 match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
1689 Some(entry) => {
1690 let mut traversal = worktree.traverse_from_path(
1691 true,
1692 true,
1693 true,
1694 entry.path.as_ref(),
1695 );
1696
1697 let mut entries_to_add = HashSet::default();
1698 worktree_excerpts
1699 .entry(worktree_id)
1700 .or_default()
1701 .insert(entry.id, (buffer_id, excerpts));
1702 let mut current_entry = entry;
1703 loop {
1704 if current_entry.is_dir() {
1705 let is_root =
1706 worktree.root_entry().map(|entry| entry.id)
1707 == Some(current_entry.id);
1708 if is_root {
1709 root_entries.insert(current_entry.id);
1710 if auto_fold_dirs {
1711 unfolded_dirs.insert(current_entry.id);
1712 }
1713 }
1714 if is_new {
1715 new_collapsed_entries.remove(&CollapsedEntry::Dir(
1716 worktree_id,
1717 current_entry.id,
1718 ));
1719 }
1720 }
1721
1722 let new_entry_added = entries_to_add.insert(current_entry);
1723 if new_entry_added && traversal.back_to_parent() {
1724 if let Some(parent_entry) = traversal.entry() {
1725 current_entry = parent_entry.clone();
1726 continue;
1727 }
1728 }
1729 break;
1730 }
1731 new_worktree_entries
1732 .entry(worktree_id)
1733 .or_insert_with(|| (worktree.clone(), HashSet::default()))
1734 .1
1735 .extend(entries_to_add);
1736 }
1737 None => {
1738 if processed_external_buffers.insert(buffer_id) {
1739 external_excerpts
1740 .entry(buffer_id)
1741 .or_insert_with(|| Vec::new())
1742 .extend(excerpts);
1743 }
1744 }
1745 }
1746 } else if processed_external_buffers.insert(buffer_id) {
1747 external_excerpts
1748 .entry(buffer_id)
1749 .or_insert_with(|| Vec::new())
1750 .extend(excerpts);
1751 }
1752 }
1753
1754 let mut new_children_count =
1755 HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
1756
1757 let worktree_entries = new_worktree_entries
1758 .into_iter()
1759 .map(|(worktree_id, (worktree_snapshot, entries))| {
1760 let mut entries = entries.into_iter().collect::<Vec<_>>();
1761 // For a proper git status propagation, we have to keep the entries sorted lexicographically.
1762 entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
1763 worktree_snapshot.propagate_git_statuses(&mut entries);
1764 project::sort_worktree_entries(&mut entries);
1765 (worktree_id, entries)
1766 })
1767 .flat_map(|(worktree_id, entries)| {
1768 {
1769 entries
1770 .into_iter()
1771 .filter_map(|entry| {
1772 if auto_fold_dirs {
1773 if let Some(parent) = entry.path.parent() {
1774 let children = new_children_count
1775 .entry(worktree_id)
1776 .or_default()
1777 .entry(Arc::from(parent))
1778 .or_default();
1779 if entry.is_dir() {
1780 children.dirs += 1;
1781 } else {
1782 children.files += 1;
1783 }
1784 }
1785 }
1786
1787 if entry.is_dir() {
1788 Some(FsEntry::Directory(worktree_id, entry))
1789 } else {
1790 let (buffer_id, excerpts) = worktree_excerpts
1791 .get_mut(&worktree_id)
1792 .and_then(|worktree_excerpts| {
1793 worktree_excerpts.remove(&entry.id)
1794 })?;
1795 Some(FsEntry::File(
1796 worktree_id,
1797 entry,
1798 buffer_id,
1799 excerpts,
1800 ))
1801 }
1802 })
1803 .collect::<Vec<_>>()
1804 }
1805 })
1806 .collect::<Vec<_>>();
1807
1808 let mut visited_dirs = Vec::new();
1809 let mut new_depth_map = HashMap::default();
1810 let new_visible_entries = external_excerpts
1811 .into_iter()
1812 .sorted_by_key(|(id, _)| *id)
1813 .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
1814 .chain(worktree_entries)
1815 .filter(|visible_item| {
1816 match visible_item {
1817 FsEntry::Directory(worktree_id, dir_entry) => {
1818 let parent_id = back_to_common_visited_parent(
1819 &mut visited_dirs,
1820 worktree_id,
1821 dir_entry,
1822 );
1823
1824 let depth = if root_entries.contains(&dir_entry.id) {
1825 0
1826 } else {
1827 if auto_fold_dirs {
1828 let children = new_children_count
1829 .get(&worktree_id)
1830 .and_then(|children_count| {
1831 children_count.get(&dir_entry.path)
1832 })
1833 .copied()
1834 .unwrap_or_default();
1835
1836 if !children.may_be_fold_part()
1837 || (children.dirs == 0
1838 && visited_dirs
1839 .last()
1840 .map(|(parent_dir_id, _)| {
1841 new_unfolded_dirs
1842 .get(&worktree_id)
1843 .map_or(true, |unfolded_dirs| {
1844 unfolded_dirs
1845 .contains(&parent_dir_id)
1846 })
1847 })
1848 .unwrap_or(true))
1849 {
1850 new_unfolded_dirs
1851 .entry(*worktree_id)
1852 .or_default()
1853 .insert(dir_entry.id);
1854 }
1855 }
1856
1857 parent_id
1858 .and_then(|(worktree_id, id)| {
1859 new_depth_map.get(&(worktree_id, id)).copied()
1860 })
1861 .unwrap_or(0)
1862 + 1
1863 };
1864 visited_dirs.push((dir_entry.id, dir_entry.path.clone()));
1865 new_depth_map.insert((*worktree_id, dir_entry.id), depth);
1866 }
1867 FsEntry::File(worktree_id, file_entry, ..) => {
1868 let parent_id = back_to_common_visited_parent(
1869 &mut visited_dirs,
1870 worktree_id,
1871 file_entry,
1872 );
1873 let depth = if root_entries.contains(&file_entry.id) {
1874 0
1875 } else {
1876 parent_id
1877 .and_then(|(worktree_id, id)| {
1878 new_depth_map.get(&(worktree_id, id)).copied()
1879 })
1880 .unwrap_or(0)
1881 + 1
1882 };
1883 new_depth_map.insert((*worktree_id, file_entry.id), depth);
1884 }
1885 FsEntry::ExternalFile(..) => {
1886 visited_dirs.clear();
1887 }
1888 }
1889
1890 true
1891 })
1892 .collect::<Vec<_>>();
1893
1894 anyhow::Ok((
1895 new_collapsed_entries,
1896 new_unfolded_dirs,
1897 new_visible_entries,
1898 new_depth_map,
1899 new_children_count,
1900 ))
1901 })
1902 .await
1903 .log_err()
1904 else {
1905 return;
1906 };
1907
1908 outline_panel
1909 .update(&mut cx, |outline_panel, cx| {
1910 outline_panel.updating_fs_entries = false;
1911 outline_panel.excerpts = new_excerpts;
1912 outline_panel.collapsed_entries = new_collapsed_entries;
1913 outline_panel.unfolded_dirs = new_unfolded_dirs;
1914 outline_panel.fs_entries = new_fs_entries;
1915 outline_panel.fs_entries_depth = new_depth_map;
1916 outline_panel.fs_children_count = new_children_count;
1917 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
1918 if new_selected_entry.is_some() {
1919 outline_panel.selected_entry = new_selected_entry;
1920 }
1921 outline_panel.fetch_outdated_outlines(cx);
1922 outline_panel.autoscroll(cx);
1923 cx.notify();
1924 })
1925 .ok();
1926 });
1927 }
1928
1929 fn replace_visible_entries(
1930 &mut self,
1931 new_active_editor: View<Editor>,
1932 cx: &mut ViewContext<Self>,
1933 ) {
1934 let new_selected_entry = self.location_for_editor_selection(&new_active_editor, cx);
1935 self.clear_previous(cx);
1936 self.active_item = Some(ActiveItem {
1937 item_id: new_active_editor.item_id(),
1938 _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, cx),
1939 active_editor: new_active_editor.downgrade(),
1940 });
1941 let new_entries =
1942 HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
1943 self.update_fs_entries(
1944 &new_active_editor,
1945 new_entries,
1946 new_selected_entry,
1947 None,
1948 cx,
1949 );
1950 }
1951
1952 fn clear_previous(&mut self, cx: &mut WindowContext<'_>) {
1953 self.filter_editor.update(cx, |editor, cx| editor.clear(cx));
1954 self.collapsed_entries.clear();
1955 self.unfolded_dirs.clear();
1956 self.selected_entry = None;
1957 self.fs_entries_update_task = Task::ready(());
1958 self.cached_entries_update_task = Task::ready(());
1959 self.active_item = None;
1960 self.fs_entries.clear();
1961 self.fs_entries_depth.clear();
1962 self.fs_children_count.clear();
1963 self.outline_fetch_tasks.clear();
1964 self.excerpts.clear();
1965 self.cached_entries_with_depth = Vec::new();
1966 }
1967
1968 fn location_for_editor_selection(
1969 &mut self,
1970 editor: &View<Editor>,
1971 cx: &mut ViewContext<Self>,
1972 ) -> Option<EntryOwned> {
1973 let selection = editor
1974 .read(cx)
1975 .selections
1976 .newest::<language::Point>(cx)
1977 .head();
1978 let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx));
1979 let multi_buffer = editor.read(cx).buffer();
1980 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
1981 let (excerpt_id, buffer, _) = editor
1982 .read(cx)
1983 .buffer()
1984 .read(cx)
1985 .excerpt_containing(selection, cx)?;
1986 let buffer_id = buffer.read(cx).remote_id();
1987 let selection_display_point = selection.to_display_point(&editor_snapshot);
1988
1989 let excerpt_outlines = self
1990 .excerpts
1991 .get(&buffer_id)
1992 .and_then(|excerpts| excerpts.get(&excerpt_id))
1993 .into_iter()
1994 .flat_map(|excerpt| excerpt.iter_outlines())
1995 .flat_map(|outline| {
1996 let start = multi_buffer_snapshot
1997 .anchor_in_excerpt(excerpt_id, outline.range.start)?
1998 .to_display_point(&editor_snapshot);
1999 let end = multi_buffer_snapshot
2000 .anchor_in_excerpt(excerpt_id, outline.range.end)?
2001 .to_display_point(&editor_snapshot);
2002 Some((start..end, outline))
2003 })
2004 .collect::<Vec<_>>();
2005
2006 let mut matching_outline_indices = Vec::new();
2007 let mut children = HashMap::default();
2008 let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
2009
2010 for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
2011 if outline_range
2012 .to_inclusive()
2013 .contains(&selection_display_point)
2014 {
2015 matching_outline_indices.push(i);
2016 } else if (outline_range.start.row()..outline_range.end.row())
2017 .to_inclusive()
2018 .contains(&selection_display_point.row())
2019 {
2020 matching_outline_indices.push(i);
2021 }
2022
2023 while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
2024 if parent_outline.depth >= outline.depth
2025 || !parent_range.contains(&outline_range.start)
2026 {
2027 parents_stack.pop();
2028 } else {
2029 break;
2030 }
2031 }
2032 if let Some((_, _, parent_index)) = parents_stack.last_mut() {
2033 children
2034 .entry(*parent_index)
2035 .or_insert_with(Vec::new)
2036 .push(i);
2037 }
2038 parents_stack.push((outline_range, outline, i));
2039 }
2040
2041 let outline_item = matching_outline_indices
2042 .into_iter()
2043 .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
2044 .filter(|(i, _)| {
2045 children
2046 .get(i)
2047 .map(|children| {
2048 children.iter().all(|child_index| {
2049 excerpt_outlines
2050 .get(*child_index)
2051 .map(|(child_range, _)| child_range.start > selection_display_point)
2052 .unwrap_or(false)
2053 })
2054 })
2055 .unwrap_or(true)
2056 })
2057 .min_by_key(|(_, (outline_range, outline))| {
2058 let distance_from_start = if outline_range.start > selection_display_point {
2059 outline_range.start - selection_display_point
2060 } else {
2061 selection_display_point - outline_range.start
2062 };
2063 let distance_from_end = if outline_range.end > selection_display_point {
2064 outline_range.end - selection_display_point
2065 } else {
2066 selection_display_point - outline_range.end
2067 };
2068
2069 (
2070 cmp::Reverse(outline.depth),
2071 distance_from_start + distance_from_end,
2072 )
2073 })
2074 .map(|(_, (_, outline))| *outline)
2075 .cloned();
2076
2077 let closest_container = match outline_item {
2078 Some(outline) => EntryOwned::Outline(buffer_id, excerpt_id, outline),
2079 None => self
2080 .cached_entries_with_depth
2081 .iter()
2082 .rev()
2083 .find_map(|cached_entry| match &cached_entry.entry {
2084 EntryOwned::Excerpt(entry_buffer_id, entry_excerpt_id, _) => {
2085 if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
2086 Some(cached_entry.entry.clone())
2087 } else {
2088 None
2089 }
2090 }
2091 EntryOwned::Entry(
2092 FsEntry::ExternalFile(file_buffer_id, file_excerpts)
2093 | FsEntry::File(_, _, file_buffer_id, file_excerpts),
2094 ) => {
2095 if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
2096 Some(cached_entry.entry.clone())
2097 } else {
2098 None
2099 }
2100 }
2101 _ => None,
2102 })?,
2103 };
2104 Some(closest_container)
2105 }
2106
2107 fn fetch_outdated_outlines(&mut self, cx: &mut ViewContext<Self>) {
2108 let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
2109 if excerpt_fetch_ranges.is_empty() {
2110 return;
2111 }
2112
2113 let syntax_theme = cx.theme().syntax().clone();
2114 for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
2115 for (excerpt_id, excerpt_range) in excerpt_ranges {
2116 let syntax_theme = syntax_theme.clone();
2117 let buffer_snapshot = buffer_snapshot.clone();
2118 self.outline_fetch_tasks.insert(
2119 (buffer_id, excerpt_id),
2120 cx.spawn(|outline_panel, mut cx| async move {
2121 let fetched_outlines = cx
2122 .background_executor()
2123 .spawn(async move {
2124 buffer_snapshot
2125 .outline_items_containing(
2126 excerpt_range.context,
2127 false,
2128 Some(&syntax_theme),
2129 )
2130 .unwrap_or_default()
2131 })
2132 .await;
2133 outline_panel
2134 .update(&mut cx, |outline_panel, cx| {
2135 if let Some(excerpt) = outline_panel
2136 .excerpts
2137 .entry(buffer_id)
2138 .or_default()
2139 .get_mut(&excerpt_id)
2140 {
2141 excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
2142 }
2143 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
2144 })
2145 .ok();
2146 }),
2147 );
2148 }
2149 }
2150 }
2151
2152 fn is_singleton_active(&self, cx: &AppContext) -> bool {
2153 self.active_item
2154 .as_ref()
2155 .and_then(|active_item| {
2156 Some(
2157 active_item
2158 .active_editor
2159 .upgrade()?
2160 .read(cx)
2161 .buffer()
2162 .read(cx)
2163 .is_singleton(),
2164 )
2165 })
2166 .unwrap_or(false)
2167 }
2168
2169 fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
2170 self.outline_fetch_tasks.clear();
2171 let mut ids = ids.into_iter().collect::<HashSet<_>>();
2172 for excerpts in self.excerpts.values_mut() {
2173 ids.retain(|id| {
2174 if let Some(excerpt) = excerpts.get_mut(id) {
2175 excerpt.invalidate_outlines();
2176 false
2177 } else {
2178 true
2179 }
2180 });
2181 if ids.is_empty() {
2182 break;
2183 }
2184 }
2185 }
2186
2187 fn excerpt_fetch_ranges(
2188 &self,
2189 cx: &AppContext,
2190 ) -> HashMap<
2191 BufferId,
2192 (
2193 BufferSnapshot,
2194 HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
2195 ),
2196 > {
2197 self.fs_entries
2198 .iter()
2199 .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
2200 match fs_entry {
2201 FsEntry::File(_, _, buffer_id, file_excerpts)
2202 | FsEntry::ExternalFile(buffer_id, file_excerpts) => {
2203 let excerpts = self.excerpts.get(&buffer_id);
2204 for &file_excerpt in file_excerpts {
2205 if let Some(excerpt) = excerpts
2206 .and_then(|excerpts| excerpts.get(&file_excerpt))
2207 .filter(|excerpt| excerpt.should_fetch_outlines())
2208 {
2209 match excerpts_to_fetch.entry(*buffer_id) {
2210 hash_map::Entry::Occupied(mut o) => {
2211 o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
2212 }
2213 hash_map::Entry::Vacant(v) => {
2214 if let Some(buffer_snapshot) =
2215 self.buffer_snapshot_for_id(*buffer_id, cx)
2216 {
2217 v.insert((buffer_snapshot, HashMap::default()))
2218 .1
2219 .insert(file_excerpt, excerpt.range.clone());
2220 }
2221 }
2222 }
2223 }
2224 }
2225 }
2226 FsEntry::Directory(..) => {}
2227 }
2228 excerpts_to_fetch
2229 })
2230 }
2231
2232 fn buffer_snapshot_for_id(
2233 &self,
2234 buffer_id: BufferId,
2235 cx: &AppContext,
2236 ) -> Option<BufferSnapshot> {
2237 let editor = self.active_item.as_ref()?.active_editor.upgrade()?;
2238 Some(
2239 editor
2240 .read(cx)
2241 .buffer()
2242 .read(cx)
2243 .buffer(buffer_id)?
2244 .read(cx)
2245 .snapshot(),
2246 )
2247 }
2248
2249 fn abs_path(&self, entry: &EntryOwned, cx: &AppContext) -> Option<PathBuf> {
2250 match entry {
2251 EntryOwned::Entry(
2252 FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
2253 ) => self
2254 .buffer_snapshot_for_id(*buffer_id, cx)
2255 .and_then(|buffer_snapshot| {
2256 let file = File::from_dyn(buffer_snapshot.file())?;
2257 file.worktree.read(cx).absolutize(&file.path).ok()
2258 }),
2259 EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => self
2260 .project
2261 .read(cx)
2262 .worktree_for_id(*worktree_id, cx)?
2263 .read(cx)
2264 .absolutize(&entry.path)
2265 .ok(),
2266 EntryOwned::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
2267 self.project
2268 .read(cx)
2269 .worktree_for_id(*worktree_id, cx)
2270 .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
2271 }),
2272 EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
2273 }
2274 }
2275
2276 fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option<Arc<Path>> {
2277 match entry {
2278 FsEntry::ExternalFile(buffer_id, _) => {
2279 let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
2280 Some(buffer_snapshot.file()?.path().clone())
2281 }
2282 FsEntry::Directory(_, entry) => Some(entry.path.clone()),
2283 FsEntry::File(_, entry, ..) => Some(entry.path.clone()),
2284 }
2285 }
2286
2287 fn update_cached_entries(
2288 &mut self,
2289 debounce: Option<Duration>,
2290 cx: &mut ViewContext<OutlinePanel>,
2291 ) {
2292 let is_singleton = self.is_singleton_active(cx);
2293 let query = self.query(cx);
2294 self.cached_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
2295 if let Some(debounce) = debounce {
2296 cx.background_executor().timer(debounce).await;
2297 }
2298 let Some(new_cached_entries) = outline_panel
2299 .update(&mut cx, |outline_panel, cx| {
2300 outline_panel.generate_cached_entries(is_singleton, query, cx)
2301 })
2302 .ok()
2303 else {
2304 return;
2305 };
2306 let new_cached_entries = new_cached_entries.await;
2307 outline_panel
2308 .update(&mut cx, |outline_panel, cx| {
2309 outline_panel.cached_entries_with_depth = new_cached_entries;
2310 cx.notify();
2311 })
2312 .ok();
2313 });
2314 }
2315
2316 fn generate_cached_entries(
2317 &self,
2318 is_singleton: bool,
2319 query: Option<String>,
2320 cx: &mut ViewContext<'_, Self>,
2321 ) -> Task<Vec<CachedEntry>> {
2322 let project = self.project.clone();
2323 cx.spawn(|outline_panel, mut cx| async move {
2324 let mut entries = Vec::new();
2325 let mut match_candidates = Vec::new();
2326
2327 let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
2328 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2329 let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
2330 let track_matches = query.is_some();
2331 let mut parent_dirs = Vec::<(&Path, bool, bool, usize)>::new();
2332
2333 for entry in &outline_panel.fs_entries {
2334 let is_expanded = outline_panel.is_expanded(entry);
2335 let (depth, should_add) = match entry {
2336 FsEntry::Directory(worktree_id, dir_entry) => {
2337 let is_root = project
2338 .read(cx)
2339 .worktree_for_id(*worktree_id, cx)
2340 .map_or(false, |worktree| {
2341 worktree.read(cx).root_entry() == Some(dir_entry)
2342 });
2343 let folded = auto_fold_dirs
2344 && !is_root
2345 && outline_panel
2346 .unfolded_dirs
2347 .get(worktree_id)
2348 .map_or(true, |unfolded_dirs| {
2349 !unfolded_dirs.contains(&dir_entry.id)
2350 });
2351 let fs_depth = outline_panel
2352 .fs_entries_depth
2353 .get(&(*worktree_id, dir_entry.id))
2354 .copied()
2355 .unwrap_or(0);
2356 while let Some(&(previous_path, ..)) = parent_dirs.last() {
2357 if dir_entry.path.starts_with(previous_path) {
2358 break;
2359 }
2360 parent_dirs.pop();
2361 }
2362 let auto_fold = match parent_dirs.last() {
2363 Some((parent_path, parent_folded, _, _)) => {
2364 *parent_folded
2365 && Some(*parent_path) == dir_entry.path.parent()
2366 && outline_panel
2367 .fs_children_count
2368 .get(worktree_id)
2369 .and_then(|entries| entries.get(&dir_entry.path))
2370 .copied()
2371 .unwrap_or_default()
2372 .may_be_fold_part()
2373 }
2374 None => false,
2375 };
2376 let folded = folded || auto_fold;
2377 let (depth, parent_expanded) = match parent_dirs.last() {
2378 Some(&(_, previous_folded, previous_expanded, previous_depth)) => {
2379 let new_depth = if folded && previous_folded {
2380 previous_depth
2381 } else {
2382 previous_depth + 1
2383 };
2384 parent_dirs.push((
2385 &dir_entry.path,
2386 folded,
2387 previous_expanded && is_expanded,
2388 new_depth,
2389 ));
2390 (new_depth, previous_expanded)
2391 }
2392 None => {
2393 parent_dirs.push((
2394 &dir_entry.path,
2395 folded,
2396 is_expanded,
2397 fs_depth,
2398 ));
2399 (fs_depth, true)
2400 }
2401 };
2402
2403 if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) =
2404 folded_dirs_entry.take()
2405 {
2406 if folded
2407 && worktree_id == &folded_worktree_id
2408 && dir_entry.path.parent()
2409 == folded_dirs.last().map(|entry| entry.path.as_ref())
2410 {
2411 folded_dirs.push(dir_entry.clone());
2412 folded_dirs_entry =
2413 Some((folded_depth, folded_worktree_id, folded_dirs))
2414 } else {
2415 if parent_expanded || query.is_some() {
2416 let new_folded_dirs =
2417 EntryOwned::FoldedDirs(folded_worktree_id, folded_dirs);
2418 outline_panel.push_entry(
2419 &mut entries,
2420 &mut match_candidates,
2421 track_matches,
2422 new_folded_dirs,
2423 folded_depth,
2424 cx,
2425 );
2426 }
2427 folded_dirs_entry =
2428 Some((depth, *worktree_id, vec![dir_entry.clone()]))
2429 }
2430 } else if folded {
2431 folded_dirs_entry =
2432 Some((depth, *worktree_id, vec![dir_entry.clone()]));
2433 }
2434
2435 let should_add = parent_expanded && folded_dirs_entry.is_none();
2436 (depth, should_add)
2437 }
2438 FsEntry::ExternalFile(..) => {
2439 if let Some((folded_depth, worktree_id, folded_dirs)) =
2440 folded_dirs_entry.take()
2441 {
2442 let parent_expanded = parent_dirs
2443 .iter()
2444 .rev()
2445 .find(|(parent_path, ..)| {
2446 folded_dirs
2447 .iter()
2448 .all(|entry| entry.path.as_ref() != *parent_path)
2449 })
2450 .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2451 if parent_expanded || query.is_some() {
2452 outline_panel.push_entry(
2453 &mut entries,
2454 &mut match_candidates,
2455 track_matches,
2456 EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2457 folded_depth,
2458 cx,
2459 );
2460 }
2461 }
2462 parent_dirs.clear();
2463 (0, true)
2464 }
2465 FsEntry::File(worktree_id, file_entry, ..) => {
2466 if let Some((folded_depth, worktree_id, folded_dirs)) =
2467 folded_dirs_entry.take()
2468 {
2469 let parent_expanded = parent_dirs
2470 .iter()
2471 .rev()
2472 .find(|(parent_path, ..)| {
2473 folded_dirs
2474 .iter()
2475 .all(|entry| entry.path.as_ref() != *parent_path)
2476 })
2477 .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2478 if parent_expanded || query.is_some() {
2479 outline_panel.push_entry(
2480 &mut entries,
2481 &mut match_candidates,
2482 track_matches,
2483 EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2484 folded_depth,
2485 cx,
2486 );
2487 }
2488 }
2489
2490 let fs_depth = outline_panel
2491 .fs_entries_depth
2492 .get(&(*worktree_id, file_entry.id))
2493 .copied()
2494 .unwrap_or(0);
2495 while let Some(&(previous_path, ..)) = parent_dirs.last() {
2496 if file_entry.path.starts_with(previous_path) {
2497 break;
2498 }
2499 parent_dirs.pop();
2500 }
2501 let (depth, should_add) = match parent_dirs.last() {
2502 Some(&(_, _, previous_expanded, previous_depth)) => {
2503 let new_depth = previous_depth + 1;
2504 (new_depth, previous_expanded)
2505 }
2506 None => (fs_depth, true),
2507 };
2508 (depth, should_add)
2509 }
2510 };
2511
2512 if !is_singleton
2513 && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
2514 {
2515 outline_panel.push_entry(
2516 &mut entries,
2517 &mut match_candidates,
2518 track_matches,
2519 EntryOwned::Entry(entry.clone()),
2520 depth,
2521 cx,
2522 );
2523 }
2524
2525 let excerpts_to_consider =
2526 if is_singleton || query.is_some() || (should_add && is_expanded) {
2527 match entry {
2528 FsEntry::File(_, _, buffer_id, entry_excerpts) => {
2529 Some((*buffer_id, entry_excerpts))
2530 }
2531 FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
2532 Some((*buffer_id, entry_excerpts))
2533 }
2534 _ => None,
2535 }
2536 } else {
2537 None
2538 };
2539 if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
2540 if let Some(excerpts) = outline_panel.excerpts.get(&buffer_id) {
2541 for &entry_excerpt in entry_excerpts {
2542 let Some(excerpt) = excerpts.get(&entry_excerpt) else {
2543 continue;
2544 };
2545 let excerpt_depth = depth + 1;
2546 outline_panel.push_entry(
2547 &mut entries,
2548 &mut match_candidates,
2549 track_matches,
2550 EntryOwned::Excerpt(
2551 buffer_id,
2552 entry_excerpt,
2553 excerpt.range.clone(),
2554 ),
2555 excerpt_depth,
2556 cx,
2557 );
2558
2559 let mut outline_base_depth = excerpt_depth + 1;
2560 if is_singleton {
2561 outline_base_depth = 0;
2562 entries.clear();
2563 match_candidates.clear();
2564 } else if query.is_none()
2565 && outline_panel.collapsed_entries.contains(
2566 &CollapsedEntry::Excerpt(buffer_id, entry_excerpt),
2567 )
2568 {
2569 continue;
2570 }
2571
2572 for outline in excerpt.iter_outlines() {
2573 outline_panel.push_entry(
2574 &mut entries,
2575 &mut match_candidates,
2576 track_matches,
2577 EntryOwned::Outline(
2578 buffer_id,
2579 entry_excerpt,
2580 outline.clone(),
2581 ),
2582 outline_base_depth + outline.depth,
2583 cx,
2584 );
2585 }
2586 if is_singleton && entries.is_empty() {
2587 outline_panel.push_entry(
2588 &mut entries,
2589 &mut match_candidates,
2590 track_matches,
2591 EntryOwned::Entry(entry.clone()),
2592 0,
2593 cx,
2594 );
2595 }
2596 }
2597 }
2598 }
2599 }
2600
2601 if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
2602 let parent_expanded = parent_dirs
2603 .iter()
2604 .rev()
2605 .find(|(parent_path, ..)| {
2606 folded_dirs
2607 .iter()
2608 .all(|entry| entry.path.as_ref() != *parent_path)
2609 })
2610 .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2611 if parent_expanded || query.is_some() {
2612 outline_panel.push_entry(
2613 &mut entries,
2614 &mut match_candidates,
2615 track_matches,
2616 EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2617 folded_depth,
2618 cx,
2619 );
2620 }
2621 }
2622 }) else {
2623 return Vec::new();
2624 };
2625
2626 let Some(query) = query else {
2627 return entries;
2628 };
2629 let mut matched_ids = match_strings(
2630 &match_candidates,
2631 &query,
2632 true,
2633 usize::MAX,
2634 &AtomicBool::default(),
2635 cx.background_executor().clone(),
2636 )
2637 .await
2638 .into_iter()
2639 .map(|string_match| (string_match.candidate_id, string_match))
2640 .collect::<HashMap<_, _>>();
2641
2642 let mut id = 0;
2643 entries.retain_mut(|cached_entry| {
2644 let retain = match matched_ids.remove(&id) {
2645 Some(string_match) => {
2646 cached_entry.string_match = Some(string_match);
2647 true
2648 }
2649 None => false,
2650 };
2651 id += 1;
2652 retain
2653 });
2654
2655 entries
2656 })
2657 }
2658
2659 fn push_entry(
2660 &self,
2661 entries: &mut Vec<CachedEntry>,
2662 match_candidates: &mut Vec<StringMatchCandidate>,
2663 track_matches: bool,
2664 entry: EntryOwned,
2665 depth: usize,
2666 cx: &AppContext,
2667 ) {
2668 if track_matches {
2669 let id = entries.len();
2670 match &entry {
2671 EntryOwned::Entry(fs_entry) => {
2672 if let Some(file_name) =
2673 self.relative_path(fs_entry, cx).as_deref().map(file_name)
2674 {
2675 match_candidates.push(StringMatchCandidate {
2676 id,
2677 string: file_name.to_string(),
2678 char_bag: file_name.chars().collect(),
2679 });
2680 }
2681 }
2682 EntryOwned::FoldedDirs(worktree_id, entries) => {
2683 let dir_names = self.dir_names_string(entries, *worktree_id, cx);
2684 {
2685 match_candidates.push(StringMatchCandidate {
2686 id,
2687 string: dir_names.to_string(),
2688 char_bag: dir_names.chars().collect(),
2689 });
2690 }
2691 }
2692 EntryOwned::Outline(_, _, outline) => match_candidates.push(StringMatchCandidate {
2693 id,
2694 string: outline.text.clone(),
2695 char_bag: outline.text.chars().collect(),
2696 }),
2697 EntryOwned::Excerpt(..) => {}
2698 }
2699 }
2700 entries.push(CachedEntry {
2701 depth,
2702 entry,
2703 string_match: None,
2704 });
2705 }
2706
2707 fn dir_names_string(
2708 &self,
2709 entries: &[Entry],
2710 worktree_id: WorktreeId,
2711 cx: &AppContext,
2712 ) -> String {
2713 let dir_names_segment = entries
2714 .iter()
2715 .map(|entry| self.entry_name(&worktree_id, entry, cx))
2716 .collect::<PathBuf>();
2717 dir_names_segment.to_string_lossy().to_string()
2718 }
2719
2720 fn query(&self, cx: &AppContext) -> Option<String> {
2721 let query = self.filter_editor.read(cx).text(cx);
2722 if query.trim().is_empty() {
2723 None
2724 } else {
2725 Some(query)
2726 }
2727 }
2728
2729 fn is_expanded(&self, entry: &FsEntry) -> bool {
2730 let entry_to_check = match entry {
2731 FsEntry::ExternalFile(buffer_id, _) => CollapsedEntry::ExternalFile(*buffer_id),
2732 FsEntry::File(worktree_id, _, buffer_id, _) => {
2733 CollapsedEntry::File(*worktree_id, *buffer_id)
2734 }
2735 FsEntry::Directory(worktree_id, entry) => CollapsedEntry::Dir(*worktree_id, entry.id),
2736 };
2737 !self.collapsed_entries.contains(&entry_to_check)
2738 }
2739}
2740
2741fn back_to_common_visited_parent(
2742 visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
2743 worktree_id: &WorktreeId,
2744 new_entry: &Entry,
2745) -> Option<(WorktreeId, ProjectEntryId)> {
2746 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
2747 match new_entry.path.parent() {
2748 Some(parent_path) => {
2749 if parent_path == visited_path.as_ref() {
2750 return Some((*worktree_id, *visited_dir_id));
2751 }
2752 }
2753 None => {
2754 break;
2755 }
2756 }
2757 visited_dirs.pop();
2758 }
2759 None
2760}
2761
2762fn file_name(path: &Path) -> String {
2763 let mut current_path = path;
2764 loop {
2765 if let Some(file_name) = current_path.file_name() {
2766 return file_name.to_string_lossy().into_owned();
2767 }
2768 match current_path.parent() {
2769 Some(parent) => current_path = parent,
2770 None => return path.to_string_lossy().into_owned(),
2771 }
2772 }
2773}
2774
2775impl Panel for OutlinePanel {
2776 fn persistent_name() -> &'static str {
2777 "Outline Panel"
2778 }
2779
2780 fn position(&self, cx: &WindowContext) -> DockPosition {
2781 match OutlinePanelSettings::get_global(cx).dock {
2782 OutlinePanelDockPosition::Left => DockPosition::Left,
2783 OutlinePanelDockPosition::Right => DockPosition::Right,
2784 }
2785 }
2786
2787 fn position_is_valid(&self, position: DockPosition) -> bool {
2788 matches!(position, DockPosition::Left | DockPosition::Right)
2789 }
2790
2791 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2792 settings::update_settings_file::<OutlinePanelSettings>(
2793 self.fs.clone(),
2794 cx,
2795 move |settings| {
2796 let dock = match position {
2797 DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
2798 DockPosition::Right => OutlinePanelDockPosition::Right,
2799 };
2800 settings.dock = Some(dock);
2801 },
2802 );
2803 }
2804
2805 fn size(&self, cx: &WindowContext) -> Pixels {
2806 self.width
2807 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
2808 }
2809
2810 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
2811 self.width = size;
2812 self.serialize(cx);
2813 cx.notify();
2814 }
2815
2816 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
2817 OutlinePanelSettings::get_global(cx)
2818 .button
2819 .then(|| IconName::ListTree)
2820 }
2821
2822 fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
2823 Some("Outline Panel")
2824 }
2825
2826 fn toggle_action(&self) -> Box<dyn Action> {
2827 Box::new(ToggleFocus)
2828 }
2829
2830 fn starts_open(&self, _: &WindowContext) -> bool {
2831 self.active
2832 }
2833
2834 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2835 let old_active = self.active;
2836 self.active = active;
2837 if active && old_active != active {
2838 if let Some(active_editor) = self
2839 .active_item
2840 .as_ref()
2841 .and_then(|item| item.active_editor.upgrade())
2842 {
2843 if self.active_item.as_ref().map(|item| item.item_id)
2844 == Some(active_editor.item_id())
2845 {
2846 let new_selected_entry = self.location_for_editor_selection(&active_editor, cx);
2847 self.update_fs_entries(
2848 &active_editor,
2849 HashSet::default(),
2850 new_selected_entry,
2851 None,
2852 cx,
2853 )
2854 } else {
2855 self.replace_visible_entries(active_editor, cx);
2856 }
2857 }
2858 }
2859 self.serialize(cx);
2860 }
2861}
2862
2863impl FocusableView for OutlinePanel {
2864 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2865 self.filter_editor.focus_handle(cx).clone()
2866 }
2867}
2868
2869impl EventEmitter<Event> for OutlinePanel {}
2870
2871impl EventEmitter<PanelEvent> for OutlinePanel {}
2872
2873impl Render for OutlinePanel {
2874 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2875 let project = self.project.read(cx);
2876 let query = self.query(cx);
2877 let outline_panel = v_flex()
2878 .id("outline-panel")
2879 .size_full()
2880 .relative()
2881 .key_context(self.dispatch_context(cx))
2882 .on_action(cx.listener(Self::open))
2883 .on_action(cx.listener(Self::cancel))
2884 .on_action(cx.listener(Self::select_next))
2885 .on_action(cx.listener(Self::select_prev))
2886 .on_action(cx.listener(Self::select_first))
2887 .on_action(cx.listener(Self::select_last))
2888 .on_action(cx.listener(Self::select_parent))
2889 .on_action(cx.listener(Self::expand_selected_entry))
2890 .on_action(cx.listener(Self::collapse_selected_entry))
2891 .on_action(cx.listener(Self::expand_all_entries))
2892 .on_action(cx.listener(Self::collapse_all_entries))
2893 .on_action(cx.listener(Self::copy_path))
2894 .on_action(cx.listener(Self::copy_relative_path))
2895 .on_action(cx.listener(Self::unfold_directory))
2896 .on_action(cx.listener(Self::fold_directory))
2897 .when(project.is_local(), |el| {
2898 el.on_action(cx.listener(Self::reveal_in_finder))
2899 .on_action(cx.listener(Self::open_in_terminal))
2900 })
2901 .on_mouse_down(
2902 MouseButton::Right,
2903 cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
2904 if let Some(entry) = outline_panel.selected_entry.clone() {
2905 outline_panel.deploy_context_menu(event.position, entry.to_ref_entry(), cx)
2906 } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
2907 outline_panel.deploy_context_menu(
2908 event.position,
2909 EntryRef::Entry(&entry),
2910 cx,
2911 )
2912 }
2913 }),
2914 )
2915 .track_focus(&self.focus_handle);
2916
2917 if self.cached_entries_with_depth.is_empty() {
2918 let header = if self.updating_fs_entries {
2919 "Loading outlines"
2920 } else if query.is_some() {
2921 "No matches for query"
2922 } else {
2923 "No outlines available"
2924 };
2925
2926 outline_panel.child(
2927 v_flex()
2928 .justify_center()
2929 .size_full()
2930 .child(h_flex().justify_center().child(Label::new(header)))
2931 .when_some(query.clone(), |panel, query| {
2932 panel.child(h_flex().justify_center().child(Label::new(query)))
2933 })
2934 .child(
2935 h_flex()
2936 .pt(Spacing::Small.rems(cx))
2937 .justify_center()
2938 .child({
2939 let keystroke = match self.position(cx) {
2940 DockPosition::Left => {
2941 cx.keystroke_text_for(&workspace::ToggleLeftDock)
2942 }
2943 DockPosition::Bottom => {
2944 cx.keystroke_text_for(&workspace::ToggleBottomDock)
2945 }
2946 DockPosition::Right => {
2947 cx.keystroke_text_for(&workspace::ToggleRightDock)
2948 }
2949 };
2950 Label::new(format!("Toggle this panel with {keystroke}"))
2951 }),
2952 ),
2953 )
2954 } else {
2955 outline_panel.child({
2956 let items_len = self.cached_entries_with_depth.len();
2957 uniform_list(cx.view().clone(), "entries", items_len, {
2958 move |outline_panel, range, cx| {
2959 let entries = outline_panel.cached_entries_with_depth.get(range);
2960 entries
2961 .map(|entries| entries.to_vec())
2962 .unwrap_or_default()
2963 .into_iter()
2964 .filter_map(|cached_entry| match cached_entry.entry {
2965 EntryOwned::Entry(entry) => Some(outline_panel.render_entry(
2966 &entry,
2967 cached_entry.depth,
2968 cached_entry.string_match.as_ref(),
2969 cx,
2970 )),
2971 EntryOwned::FoldedDirs(worktree_id, entries) => {
2972 Some(outline_panel.render_folded_dirs(
2973 worktree_id,
2974 &entries,
2975 cached_entry.depth,
2976 cached_entry.string_match.as_ref(),
2977 cx,
2978 ))
2979 }
2980 EntryOwned::Excerpt(buffer_id, excerpt_id, excerpt) => {
2981 outline_panel.render_excerpt(
2982 buffer_id,
2983 excerpt_id,
2984 &excerpt,
2985 cached_entry.depth,
2986 cx,
2987 )
2988 }
2989 EntryOwned::Outline(buffer_id, excerpt_id, outline) => {
2990 Some(outline_panel.render_outline(
2991 buffer_id,
2992 excerpt_id,
2993 &outline,
2994 cached_entry.depth,
2995 cached_entry.string_match.as_ref(),
2996 cx,
2997 ))
2998 }
2999 })
3000 .collect()
3001 }
3002 })
3003 .size_full()
3004 .track_scroll(self.scroll_handle.clone())
3005 })
3006 }
3007 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3008 deferred(
3009 anchored()
3010 .position(*position)
3011 .anchor(gpui::AnchorCorner::TopLeft)
3012 .child(menu.clone()),
3013 )
3014 .with_priority(1)
3015 }))
3016 .child(
3017 v_flex()
3018 .child(div().mx_2().border_primary(cx).border_t_1())
3019 .child(v_flex().p_2().child(self.filter_editor.clone())),
3020 )
3021 }
3022}
3023
3024fn subscribe_for_editor_events(
3025 editor: &View<Editor>,
3026 cx: &mut ViewContext<OutlinePanel>,
3027) -> Subscription {
3028 let debounce = Some(UPDATE_DEBOUNCE);
3029 cx.subscribe(
3030 editor,
3031 move |outline_panel, editor, e: &EditorEvent, cx| match e {
3032 EditorEvent::SelectionsChanged { local: true } => {
3033 outline_panel.reveal_entry_for_selection(&editor, cx);
3034 cx.notify();
3035 }
3036 EditorEvent::ExcerptsAdded { excerpts, .. } => {
3037 outline_panel.update_fs_entries(
3038 &editor,
3039 excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
3040 None,
3041 debounce,
3042 cx,
3043 );
3044 }
3045 EditorEvent::ExcerptsRemoved { ids } => {
3046 let mut ids = ids.into_iter().collect::<HashSet<_>>();
3047 for excerpts in outline_panel.excerpts.values_mut() {
3048 excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
3049 if ids.is_empty() {
3050 break;
3051 }
3052 }
3053 outline_panel.update_fs_entries(&editor, HashSet::default(), None, debounce, cx);
3054 }
3055 EditorEvent::ExcerptsExpanded { ids } => {
3056 outline_panel.invalidate_outlines(ids);
3057 outline_panel.fetch_outdated_outlines(cx)
3058 }
3059 EditorEvent::ExcerptsEdited { ids } => {
3060 outline_panel.invalidate_outlines(ids);
3061 outline_panel.fetch_outdated_outlines(cx);
3062 }
3063 EditorEvent::Reparsed(buffer_id) => {
3064 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
3065 for (_, excerpt) in excerpts {
3066 excerpt.invalidate_outlines();
3067 }
3068 }
3069 outline_panel.fetch_outdated_outlines(cx);
3070 }
3071 _ => {}
3072 },
3073 )
3074}
3075
3076fn empty_icon() -> AnyElement {
3077 h_flex()
3078 .size(IconSize::default().rems())
3079 .invisible()
3080 .flex_none()
3081 .into_any_element()
3082}