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