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