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