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