1mod outline_panel_settings;
2
3use std::{
4 cell::OnceCell,
5 cmp,
6 hash::Hash,
7 ops::Range,
8 path::{Path, PathBuf},
9 sync::{atomic::AtomicBool, Arc, OnceLock},
10 time::Duration,
11 u32,
12};
13
14use anyhow::Context;
15use collections::{hash_map, BTreeSet, HashMap, HashSet};
16use db::kvp::KEY_VALUE_STORE;
17use editor::{
18 display_map::ToDisplayPoint,
19 items::{entry_git_aware_label_color, entry_label_color},
20 scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
21 AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorMode, ExcerptId, ExcerptRange,
22 MultiBufferSnapshot, RangeToAnchorExt,
23};
24use file_icons::FileIcons;
25use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
26use gpui::{
27 actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement,
28 AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId,
29 EventEmitter, FocusHandle, FocusableView, HighlightStyle, InteractiveElement, IntoElement,
30 KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
31 SharedString, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext,
32 VisualContext, WeakView, WindowContext,
33};
34use itertools::Itertools;
35use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
36use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrev};
37
38use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings};
39use project::{File, Fs, Item, Project};
40use search::{BufferSearchBar, ProjectSearchView};
41use serde::{Deserialize, Serialize};
42use settings::{Settings, SettingsStore};
43use smol::channel;
44use theme::SyntaxTheme;
45use util::{debug_panic, RangeExt, ResultExt, TryFutureExt};
46use workspace::{
47 dock::{DockPosition, Panel, PanelEvent},
48 item::ItemHandle,
49 searchable::{SearchEvent, SearchableItem},
50 ui::{
51 h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder,
52 HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label,
53 LabelCommon, ListItem, Selectable, Spacing, StyledExt, StyledTypography, Tooltip,
54 },
55 OpenInTerminal, WeakItemHandle, Workspace,
56};
57use worktree::{Entry, ProjectEntryId, WorktreeId};
58
59#[derive(Clone, Default, Deserialize, PartialEq)]
60pub struct Open {
61 change_selection: bool,
62}
63
64impl_actions!(outline_panel, [Open]);
65
66actions!(
67 outline_panel,
68 [
69 CollapseAllEntries,
70 CollapseSelectedEntry,
71 CopyPath,
72 CopyRelativePath,
73 ExpandAllEntries,
74 ExpandSelectedEntry,
75 FoldDirectory,
76 ToggleActiveEditorPin,
77 RevealInFileManager,
78 SelectParent,
79 ToggleFocus,
80 UnfoldDirectory,
81 ]
82);
83
84const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
85const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
86
87type Outline = OutlineItem<language::Anchor>;
88type HighlightStyleData = Arc<OnceLock<Vec<(Range<usize>, HighlightStyle)>>>;
89
90pub struct OutlinePanel {
91 fs: Arc<dyn Fs>,
92 width: Option<Pixels>,
93 project: Model<Project>,
94 workspace: WeakView<Workspace>,
95 active: bool,
96 pinned: bool,
97 scroll_handle: UniformListScrollHandle,
98 context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
99 focus_handle: FocusHandle,
100 pending_serialization: Task<Option<()>>,
101 fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
102 fs_entries: Vec<FsEntry>,
103 fs_children_count: HashMap<WorktreeId, HashMap<Arc<Path>, FsChildren>>,
104 collapsed_entries: HashSet<CollapsedEntry>,
105 unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
106 selected_entry: SelectedEntry,
107 active_item: Option<ActiveItem>,
108 _subscriptions: Vec<Subscription>,
109 updating_fs_entries: bool,
110 fs_entries_update_task: Task<()>,
111 cached_entries_update_task: Task<()>,
112 reveal_selection_task: Task<anyhow::Result<()>>,
113 outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
114 excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
115 cached_entries: Vec<CachedEntry>,
116 filter_editor: View<Editor>,
117 mode: ItemsDisplayMode,
118}
119
120enum ItemsDisplayMode {
121 Search(SearchState),
122 Outline,
123}
124
125struct SearchState {
126 kind: SearchKind,
127 query: String,
128 matches: Vec<(Range<editor::Anchor>, OnceCell<Arc<SearchData>>)>,
129 highlight_search_match_tx: channel::Sender<HighlightArguments>,
130 _search_match_highlighter: Task<()>,
131 _search_match_notify: Task<()>,
132}
133
134struct HighlightArguments {
135 multi_buffer_snapshot: MultiBufferSnapshot,
136 search_data: Arc<SearchData>,
137}
138
139impl SearchState {
140 fn new(
141 kind: SearchKind,
142 query: String,
143 new_matches: Vec<Range<editor::Anchor>>,
144 theme: Arc<SyntaxTheme>,
145 cx: &mut ViewContext<'_, OutlinePanel>,
146 ) -> Self {
147 let (highlight_search_match_tx, highlight_search_match_rx) = channel::unbounded();
148 let (notify_tx, notify_rx) = channel::bounded::<()>(1);
149 Self {
150 kind,
151 query,
152 matches: new_matches
153 .into_iter()
154 .map(|range| (range, OnceCell::new()))
155 .collect(),
156 highlight_search_match_tx,
157 _search_match_highlighter: cx.background_executor().spawn(async move {
158 while let Ok(highlight_arguments) = highlight_search_match_rx.recv().await {
159 let highlight_data = &highlight_arguments.search_data.highlights_data;
160 if highlight_data.get().is_some() {
161 continue;
162 }
163 let mut left_whitespaces_count = 0;
164 let mut non_whitespace_symbol_occurred = false;
165 let context_offset_range = highlight_arguments
166 .search_data
167 .context_range
168 .to_offset(&highlight_arguments.multi_buffer_snapshot);
169 let mut offset = context_offset_range.start;
170 let mut context_text = String::new();
171 let mut highlight_ranges = Vec::new();
172 for mut chunk in highlight_arguments
173 .multi_buffer_snapshot
174 .chunks(context_offset_range.start..context_offset_range.end, true)
175 {
176 if !non_whitespace_symbol_occurred {
177 for c in chunk.text.chars() {
178 if c.is_whitespace() {
179 left_whitespaces_count += c.len_utf8();
180 } else {
181 non_whitespace_symbol_occurred = true;
182 break;
183 }
184 }
185 }
186
187 if chunk.text.len() > context_offset_range.end - offset {
188 chunk.text = &chunk.text[0..(context_offset_range.end - offset)];
189 offset = context_offset_range.end;
190 } else {
191 offset += chunk.text.len();
192 }
193 let style = chunk
194 .syntax_highlight_id
195 .and_then(|highlight| highlight.style(&theme));
196 if let Some(style) = style {
197 let start = context_text.len();
198 let end = start + chunk.text.len();
199 highlight_ranges.push((start..end, style));
200 }
201 context_text.push_str(chunk.text);
202 if offset >= context_offset_range.end {
203 break;
204 }
205 }
206
207 highlight_ranges.iter_mut().for_each(|(range, _)| {
208 range.start = range.start.saturating_sub(left_whitespaces_count);
209 range.end = range.end.saturating_sub(left_whitespaces_count);
210 });
211 if highlight_data.set(highlight_ranges).ok().is_some() {
212 notify_tx.try_send(()).ok();
213 }
214
215 let trimmed_text = context_text[left_whitespaces_count..].to_owned();
216 debug_assert_eq!(
217 trimmed_text, highlight_arguments.search_data.context_text,
218 "Highlighted text that does not match the buffer text"
219 );
220 }
221 }),
222 _search_match_notify: cx.spawn(|outline_panel, mut cx| async move {
223 while let Ok(()) = notify_rx.recv().await {
224 let update_result = outline_panel.update(&mut cx, |_, cx| {
225 cx.notify();
226 });
227 if update_result.is_err() {
228 break;
229 }
230 }
231 }),
232 }
233 }
234
235 fn highlight_search_match(
236 &mut self,
237 match_range: &Range<editor::Anchor>,
238 multi_buffer_snapshot: &MultiBufferSnapshot,
239 ) {
240 if let Some((_, search_data)) = self.matches.iter().find(|(range, _)| range == match_range)
241 {
242 let search_data = search_data
243 .get_or_init(|| Arc::new(SearchData::new(match_range, multi_buffer_snapshot)));
244 self.highlight_search_match_tx
245 .send_blocking(HighlightArguments {
246 multi_buffer_snapshot: multi_buffer_snapshot.clone(),
247 search_data: Arc::clone(search_data),
248 })
249 .ok();
250 }
251 }
252}
253
254#[derive(Debug)]
255enum SelectedEntry {
256 Invalidated(Option<PanelEntry>),
257 Valid(PanelEntry),
258 None,
259}
260
261impl SelectedEntry {
262 fn invalidate(&mut self) {
263 match std::mem::replace(self, SelectedEntry::None) {
264 Self::Valid(entry) => *self = Self::Invalidated(Some(entry)),
265 Self::None => *self = Self::Invalidated(None),
266 other => *self = other,
267 }
268 }
269
270 fn is_invalidated(&self) -> bool {
271 matches!(self, Self::Invalidated(_))
272 }
273}
274
275#[derive(Debug, Clone, Copy, Default)]
276struct FsChildren {
277 files: usize,
278 dirs: usize,
279}
280
281impl FsChildren {
282 fn may_be_fold_part(&self) -> bool {
283 self.dirs == 0 || (self.dirs == 1 && self.files == 0)
284 }
285}
286
287#[derive(Clone, Debug)]
288struct CachedEntry {
289 depth: usize,
290 string_match: Option<StringMatch>,
291 entry: PanelEntry,
292}
293
294#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
295enum CollapsedEntry {
296 Dir(WorktreeId, ProjectEntryId),
297 File(WorktreeId, BufferId),
298 ExternalFile(BufferId),
299 Excerpt(BufferId, ExcerptId),
300}
301
302#[derive(Debug)]
303struct Excerpt {
304 range: ExcerptRange<language::Anchor>,
305 outlines: ExcerptOutlines,
306}
307
308impl Excerpt {
309 fn invalidate_outlines(&mut self) {
310 if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines {
311 self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines));
312 }
313 }
314
315 fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {
316 match &self.outlines {
317 ExcerptOutlines::Outlines(outlines) => outlines.iter(),
318 ExcerptOutlines::Invalidated(outlines) => outlines.iter(),
319 ExcerptOutlines::NotFetched => [].iter(),
320 }
321 }
322
323 fn should_fetch_outlines(&self) -> bool {
324 match &self.outlines {
325 ExcerptOutlines::Outlines(_) => false,
326 ExcerptOutlines::Invalidated(_) => true,
327 ExcerptOutlines::NotFetched => true,
328 }
329 }
330}
331
332#[derive(Debug)]
333enum ExcerptOutlines {
334 Outlines(Vec<Outline>),
335 Invalidated(Vec<Outline>),
336 NotFetched,
337}
338
339#[derive(Clone, Debug)]
340enum PanelEntry {
341 Fs(FsEntry),
342 FoldedDirs(WorktreeId, Vec<Entry>),
343 Outline(OutlineEntry),
344 Search(SearchEntry),
345}
346
347#[derive(Clone, Debug)]
348struct SearchEntry {
349 match_range: Range<editor::Anchor>,
350 kind: SearchKind,
351 render_data: Arc<SearchData>,
352}
353
354#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
355enum SearchKind {
356 Project,
357 Buffer,
358}
359
360#[derive(Clone, Debug)]
361struct SearchData {
362 context_range: Range<editor::Anchor>,
363 context_text: String,
364 truncated_left: bool,
365 truncated_right: bool,
366 search_match_indices: Vec<Range<usize>>,
367 highlights_data: HighlightStyleData,
368}
369
370impl PartialEq for PanelEntry {
371 fn eq(&self, other: &Self) -> bool {
372 match (self, other) {
373 (Self::Fs(a), Self::Fs(b)) => a == b,
374 (Self::FoldedDirs(a1, a2), Self::FoldedDirs(b1, b2)) => a1 == b1 && a2 == b2,
375 (Self::Outline(a), Self::Outline(b)) => a == b,
376 (
377 Self::Search(SearchEntry {
378 match_range: match_range_a,
379 kind: kind_a,
380 ..
381 }),
382 Self::Search(SearchEntry {
383 match_range: match_range_b,
384 kind: kind_b,
385 ..
386 }),
387 ) => match_range_a == match_range_b && kind_a == kind_b,
388 _ => false,
389 }
390 }
391}
392
393impl Eq for PanelEntry {}
394
395const SEARCH_MATCH_CONTEXT_SIZE: u32 = 40;
396const TRUNCATED_CONTEXT_MARK: &str = "…";
397
398impl SearchData {
399 fn new(
400 match_range: &Range<editor::Anchor>,
401 multi_buffer_snapshot: &MultiBufferSnapshot,
402 ) -> Self {
403 let match_point_range = match_range.to_point(multi_buffer_snapshot);
404 let context_left_border = multi_buffer_snapshot.clip_point(
405 language::Point::new(
406 match_point_range.start.row,
407 match_point_range
408 .start
409 .column
410 .saturating_sub(SEARCH_MATCH_CONTEXT_SIZE),
411 ),
412 Bias::Left,
413 );
414 let context_right_border = multi_buffer_snapshot.clip_point(
415 language::Point::new(
416 match_point_range.end.row,
417 match_point_range.end.column + SEARCH_MATCH_CONTEXT_SIZE,
418 ),
419 Bias::Right,
420 );
421
422 let context_anchor_range =
423 (context_left_border..context_right_border).to_anchors(multi_buffer_snapshot);
424 let context_offset_range = context_anchor_range.to_offset(multi_buffer_snapshot);
425 let match_offset_range = match_range.to_offset(multi_buffer_snapshot);
426
427 let mut search_match_indices = vec![
428 multi_buffer_snapshot.clip_offset(
429 match_offset_range.start - context_offset_range.start,
430 Bias::Left,
431 )
432 ..multi_buffer_snapshot.clip_offset(
433 match_offset_range.end - context_offset_range.start,
434 Bias::Right,
435 ),
436 ];
437
438 let entire_context_text = multi_buffer_snapshot
439 .text_for_range(context_offset_range.clone())
440 .collect::<String>();
441 let left_whitespaces_offset = entire_context_text
442 .chars()
443 .take_while(|c| c.is_whitespace())
444 .map(|c| c.len_utf8())
445 .sum::<usize>();
446
447 let mut extended_context_left_border = context_left_border;
448 extended_context_left_border.column = extended_context_left_border.column.saturating_sub(1);
449 let extended_context_left_border =
450 multi_buffer_snapshot.clip_point(extended_context_left_border, Bias::Left);
451 let mut extended_context_right_border = context_right_border;
452 extended_context_right_border.column += 1;
453 let extended_context_right_border =
454 multi_buffer_snapshot.clip_point(extended_context_right_border, Bias::Right);
455
456 let truncated_left = left_whitespaces_offset == 0
457 && extended_context_left_border < context_left_border
458 && multi_buffer_snapshot
459 .chars_at(extended_context_left_border)
460 .last()
461 .map_or(false, |c| !c.is_whitespace());
462 let truncated_right = entire_context_text
463 .chars()
464 .last()
465 .map_or(true, |c| !c.is_whitespace())
466 && extended_context_right_border > context_right_border
467 && multi_buffer_snapshot
468 .chars_at(extended_context_right_border)
469 .next()
470 .map_or(false, |c| !c.is_whitespace());
471 search_match_indices.iter_mut().for_each(|range| {
472 range.start = multi_buffer_snapshot.clip_offset(
473 range.start.saturating_sub(left_whitespaces_offset),
474 Bias::Left,
475 );
476 range.end = multi_buffer_snapshot.clip_offset(
477 range.end.saturating_sub(left_whitespaces_offset),
478 Bias::Right,
479 );
480 });
481
482 let trimmed_row_offset_range =
483 context_offset_range.start + left_whitespaces_offset..context_offset_range.end;
484 let trimmed_text = entire_context_text[left_whitespaces_offset..].to_owned();
485 Self {
486 highlights_data: Arc::default(),
487 search_match_indices,
488 context_range: trimmed_row_offset_range.to_anchors(multi_buffer_snapshot),
489 context_text: trimmed_text,
490 truncated_left,
491 truncated_right,
492 }
493 }
494}
495
496#[derive(Clone, Debug, PartialEq, Eq)]
497enum OutlineEntry {
498 Excerpt(BufferId, ExcerptId, ExcerptRange<language::Anchor>),
499 Outline(BufferId, ExcerptId, Outline),
500}
501
502#[derive(Clone, Debug, Eq)]
503enum FsEntry {
504 ExternalFile(BufferId, Vec<ExcerptId>),
505 Directory(WorktreeId, Entry),
506 File(WorktreeId, Entry, BufferId, Vec<ExcerptId>),
507}
508
509impl PartialEq for FsEntry {
510 fn eq(&self, other: &Self) -> bool {
511 match (self, other) {
512 (Self::ExternalFile(id_a, _), Self::ExternalFile(id_b, _)) => id_a == id_b,
513 (Self::Directory(id_a, entry_a), Self::Directory(id_b, entry_b)) => {
514 id_a == id_b && entry_a.id == entry_b.id
515 }
516 (
517 Self::File(worktree_a, entry_a, id_a, ..),
518 Self::File(worktree_b, entry_b, id_b, ..),
519 ) => worktree_a == worktree_b && entry_a.id == entry_b.id && id_a == id_b,
520 _ => false,
521 }
522 }
523}
524
525impl Hash for FsEntry {
526 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
527 match self {
528 Self::ExternalFile(buffer_id, _) => {
529 buffer_id.hash(state);
530 }
531 Self::Directory(worktree_id, entry) => {
532 worktree_id.hash(state);
533 entry.id.hash(state);
534 }
535 Self::File(worktree_id, entry, buffer_id, _) => {
536 worktree_id.hash(state);
537 entry.id.hash(state);
538 buffer_id.hash(state);
539 }
540 }
541 }
542}
543
544struct ActiveItem {
545 item_handle: Box<dyn WeakItemHandle>,
546 active_editor: WeakView<Editor>,
547 _buffer_search_subscription: Subscription,
548 _editor_subscrpiption: Subscription,
549}
550
551#[derive(Debug)]
552pub enum Event {
553 Focus,
554}
555
556#[derive(Serialize, Deserialize)]
557struct SerializedOutlinePanel {
558 width: Option<Pixels>,
559 active: Option<bool>,
560}
561
562pub fn init_settings(cx: &mut AppContext) {
563 OutlinePanelSettings::register(cx);
564}
565
566pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
567 init_settings(cx);
568 file_icons::init(assets, cx);
569
570 cx.observe_new_views(|workspace: &mut Workspace, _| {
571 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
572 workspace.toggle_panel_focus::<OutlinePanel>(cx);
573 });
574 })
575 .detach();
576}
577
578impl OutlinePanel {
579 pub async fn load(
580 workspace: WeakView<Workspace>,
581 mut cx: AsyncWindowContext,
582 ) -> anyhow::Result<View<Self>> {
583 let serialized_panel = cx
584 .background_executor()
585 .spawn(async move { KEY_VALUE_STORE.read_kvp(OUTLINE_PANEL_KEY) })
586 .await
587 .context("loading outline panel")
588 .log_err()
589 .flatten()
590 .map(|panel| serde_json::from_str::<SerializedOutlinePanel>(&panel))
591 .transpose()
592 .log_err()
593 .flatten();
594
595 workspace.update(&mut cx, |workspace, cx| {
596 let panel = Self::new(workspace, cx);
597 if let Some(serialized_panel) = serialized_panel {
598 panel.update(cx, |panel, cx| {
599 panel.width = serialized_panel.width.map(|px| px.round());
600 panel.active = serialized_panel.active.unwrap_or(false);
601 cx.notify();
602 });
603 }
604 panel
605 })
606 }
607
608 fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
609 let project = workspace.project().clone();
610 let workspace_handle = cx.view().downgrade();
611 let outline_panel = cx.new_view(|cx| {
612 let filter_editor = cx.new_view(|cx| {
613 let mut editor = Editor::single_line(cx);
614 editor.set_placeholder_text("Filter...", cx);
615 editor
616 });
617 let filter_update_subscription =
618 cx.subscribe(&filter_editor, |outline_panel: &mut Self, _, event, cx| {
619 if let editor::EditorEvent::BufferEdited = event {
620 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
621 }
622 });
623
624 let focus_handle = cx.focus_handle();
625 let focus_subscription = cx.on_focus(&focus_handle, Self::focus_in);
626 let workspace_subscription = cx.subscribe(
627 &workspace
628 .weak_handle()
629 .upgrade()
630 .expect("have a &mut Workspace"),
631 move |outline_panel, workspace, event, cx| {
632 if let workspace::Event::ActiveItemChanged = event {
633 if let Some((new_active_item, new_active_editor)) =
634 workspace_active_editor(workspace.read(cx), cx)
635 {
636 if outline_panel.should_replace_active_item(new_active_item.as_ref()) {
637 outline_panel.replace_active_editor(
638 new_active_item,
639 new_active_editor,
640 cx,
641 );
642 }
643 } else {
644 outline_panel.clear_previous(cx);
645 cx.notify();
646 }
647 }
648 },
649 );
650
651 let icons_subscription = cx.observe_global::<FileIcons>(|_, cx| {
652 cx.notify();
653 });
654
655 let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx);
656 let settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
657 let new_settings = *OutlinePanelSettings::get_global(cx);
658 if outline_panel_settings != new_settings {
659 outline_panel_settings = new_settings;
660 cx.notify();
661 }
662 });
663
664 let mut outline_panel = Self {
665 mode: ItemsDisplayMode::Outline,
666 active: false,
667 pinned: false,
668 workspace: workspace_handle,
669 project,
670 fs: workspace.app_state().fs.clone(),
671 scroll_handle: UniformListScrollHandle::new(),
672 focus_handle,
673 filter_editor,
674 fs_entries: Vec::new(),
675 fs_entries_depth: HashMap::default(),
676 fs_children_count: HashMap::default(),
677 collapsed_entries: HashSet::default(),
678 unfolded_dirs: HashMap::default(),
679 selected_entry: SelectedEntry::None,
680 context_menu: None,
681 width: None,
682 active_item: None,
683 pending_serialization: Task::ready(None),
684 updating_fs_entries: false,
685 fs_entries_update_task: Task::ready(()),
686 cached_entries_update_task: Task::ready(()),
687 reveal_selection_task: Task::ready(Ok(())),
688 outline_fetch_tasks: HashMap::default(),
689 excerpts: HashMap::default(),
690 cached_entries: Vec::new(),
691 _subscriptions: vec![
692 settings_subscription,
693 icons_subscription,
694 focus_subscription,
695 workspace_subscription,
696 filter_update_subscription,
697 ],
698 };
699 if let Some((item, editor)) = workspace_active_editor(workspace, cx) {
700 outline_panel.replace_active_editor(item, editor, cx);
701 }
702 outline_panel
703 });
704
705 outline_panel
706 }
707
708 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
709 let width = self.width;
710 let active = Some(self.active);
711 self.pending_serialization = cx.background_executor().spawn(
712 async move {
713 KEY_VALUE_STORE
714 .write_kvp(
715 OUTLINE_PANEL_KEY.into(),
716 serde_json::to_string(&SerializedOutlinePanel { width, active })?,
717 )
718 .await?;
719 anyhow::Ok(())
720 }
721 .log_err(),
722 );
723 }
724
725 fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
726 let mut dispatch_context = KeyContext::new_with_defaults();
727 dispatch_context.add("OutlinePanel");
728 dispatch_context.add("menu");
729 let identifier = if self.filter_editor.focus_handle(cx).is_focused(cx) {
730 "editing"
731 } else {
732 "not_editing"
733 };
734 dispatch_context.add(identifier);
735 dispatch_context
736 }
737
738 fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
739 if let Some(PanelEntry::FoldedDirs(worktree_id, entries)) = self.selected_entry().cloned() {
740 self.unfolded_dirs
741 .entry(worktree_id)
742 .or_default()
743 .extend(entries.iter().map(|entry| entry.id));
744 self.update_cached_entries(None, cx);
745 }
746 }
747
748 fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext<Self>) {
749 let (worktree_id, entry) = match self.selected_entry().cloned() {
750 Some(PanelEntry::Fs(FsEntry::Directory(worktree_id, entry))) => {
751 (worktree_id, Some(entry))
752 }
753 Some(PanelEntry::FoldedDirs(worktree_id, entries)) => {
754 (worktree_id, entries.last().cloned())
755 }
756 _ => return,
757 };
758 let Some(entry) = entry else {
759 return;
760 };
761 let unfolded_dirs = self.unfolded_dirs.get_mut(&worktree_id);
762 let worktree = self
763 .project
764 .read(cx)
765 .worktree_for_id(worktree_id, cx)
766 .map(|w| w.read(cx).snapshot());
767 let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
768 return;
769 };
770
771 unfolded_dirs.remove(&entry.id);
772 self.update_cached_entries(None, cx);
773 }
774
775 fn open(&mut self, open: &Open, cx: &mut ViewContext<Self>) {
776 if self.filter_editor.focus_handle(cx).is_focused(cx) {
777 cx.propagate()
778 } else if let Some(selected_entry) = self.selected_entry().cloned() {
779 self.open_entry(&selected_entry, open.change_selection, cx);
780 }
781 }
782
783 fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
784 if self.filter_editor.focus_handle(cx).is_focused(cx) {
785 self.focus_handle.focus(cx);
786 } else {
787 self.filter_editor.focus_handle(cx).focus(cx);
788 }
789
790 if self.context_menu.is_some() {
791 self.context_menu.take();
792 cx.notify();
793 }
794 }
795
796 fn open_entry(
797 &mut self,
798 entry: &PanelEntry,
799 change_selection: bool,
800 cx: &mut ViewContext<OutlinePanel>,
801 ) {
802 let Some(active_editor) = self.active_editor() else {
803 return;
804 };
805 let active_multi_buffer = active_editor.read(cx).buffer().clone();
806 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
807 let offset_from_top = if active_multi_buffer.read(cx).is_singleton() {
808 Point::default()
809 } else {
810 Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
811 };
812
813 self.toggle_expanded(entry, cx);
814 let scroll_target = match entry {
815 PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None,
816 PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
817 let scroll_target = multi_buffer_snapshot.excerpts().find_map(
818 |(excerpt_id, buffer_snapshot, excerpt_range)| {
819 if &buffer_snapshot.remote_id() == buffer_id {
820 multi_buffer_snapshot
821 .anchor_in_excerpt(excerpt_id, excerpt_range.context.start)
822 } else {
823 None
824 }
825 },
826 );
827 Some(offset_from_top).zip(scroll_target)
828 }
829 PanelEntry::Fs(FsEntry::File(_, file_entry, ..)) => {
830 let scroll_target = self
831 .project
832 .update(cx, |project, cx| {
833 project
834 .path_for_entry(file_entry.id, cx)
835 .and_then(|path| project.get_open_buffer(&path, cx))
836 })
837 .map(|buffer| {
838 active_multi_buffer
839 .read(cx)
840 .excerpts_for_buffer(&buffer, cx)
841 })
842 .and_then(|excerpts| {
843 let (excerpt_id, excerpt_range) = excerpts.first()?;
844 multi_buffer_snapshot
845 .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
846 });
847 Some(offset_from_top).zip(scroll_target)
848 }
849 PanelEntry::Outline(OutlineEntry::Outline(_, excerpt_id, outline)) => {
850 let scroll_target = multi_buffer_snapshot
851 .anchor_in_excerpt(*excerpt_id, outline.range.start)
852 .or_else(|| {
853 multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, outline.range.end)
854 });
855 Some(Point::default()).zip(scroll_target)
856 }
857 PanelEntry::Outline(OutlineEntry::Excerpt(_, excerpt_id, excerpt_range)) => {
858 let scroll_target = multi_buffer_snapshot
859 .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start);
860 Some(Point::default()).zip(scroll_target)
861 }
862 PanelEntry::Search(SearchEntry { match_range, .. }) => {
863 Some((Point::default(), match_range.start))
864 }
865 };
866
867 if let Some((offset, anchor)) = scroll_target {
868 let activate = self
869 .workspace
870 .update(cx, |workspace, cx| match self.active_item() {
871 Some(active_item) => {
872 workspace.activate_item(active_item.as_ref(), true, change_selection, cx)
873 }
874 None => workspace.activate_item(&active_editor, true, change_selection, cx),
875 });
876
877 if activate.is_ok() {
878 self.select_entry(entry.clone(), true, cx);
879 if change_selection {
880 active_editor.update(cx, |editor, cx| {
881 editor.change_selections(
882 Some(Autoscroll::Strategy(AutoscrollStrategy::Top)),
883 cx,
884 |s| s.select_ranges(Some(anchor..anchor)),
885 );
886 });
887 active_editor.focus_handle(cx).focus(cx);
888 } else {
889 active_editor.update(cx, |editor, cx| {
890 editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, cx);
891 });
892 self.focus_handle.focus(cx);
893 }
894 }
895 }
896 }
897
898 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
899 if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
900 self.cached_entries
901 .iter()
902 .map(|cached_entry| &cached_entry.entry)
903 .skip_while(|entry| entry != &selected_entry)
904 .nth(1)
905 .cloned()
906 }) {
907 self.select_entry(entry_to_select, true, cx);
908 } else {
909 self.select_first(&SelectFirst {}, cx)
910 }
911 }
912
913 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
914 if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
915 self.cached_entries
916 .iter()
917 .rev()
918 .map(|cached_entry| &cached_entry.entry)
919 .skip_while(|entry| entry != &selected_entry)
920 .nth(1)
921 .cloned()
922 }) {
923 self.select_entry(entry_to_select, true, cx);
924 } else {
925 self.select_last(&SelectLast, cx)
926 }
927 }
928
929 fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext<Self>) {
930 if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
931 let mut previous_entries = self
932 .cached_entries
933 .iter()
934 .rev()
935 .map(|cached_entry| &cached_entry.entry)
936 .skip_while(|entry| entry != &selected_entry)
937 .skip(1);
938 match &selected_entry {
939 PanelEntry::Fs(fs_entry) => match fs_entry {
940 FsEntry::ExternalFile(..) => None,
941 FsEntry::File(worktree_id, entry, ..)
942 | FsEntry::Directory(worktree_id, entry) => {
943 entry.path.parent().and_then(|parent_path| {
944 previous_entries.find(|entry| match entry {
945 PanelEntry::Fs(FsEntry::Directory(dir_worktree_id, dir_entry)) => {
946 dir_worktree_id == worktree_id
947 && dir_entry.path.as_ref() == parent_path
948 }
949 PanelEntry::FoldedDirs(dirs_worktree_id, dirs) => {
950 dirs_worktree_id == worktree_id
951 && dirs
952 .last()
953 .map_or(false, |dir| dir.path.as_ref() == parent_path)
954 }
955 _ => false,
956 })
957 })
958 }
959 },
960 PanelEntry::FoldedDirs(worktree_id, entries) => entries
961 .first()
962 .and_then(|entry| entry.path.parent())
963 .and_then(|parent_path| {
964 previous_entries.find(|entry| {
965 if let PanelEntry::Fs(FsEntry::Directory(dir_worktree_id, dir_entry)) =
966 entry
967 {
968 dir_worktree_id == worktree_id
969 && dir_entry.path.as_ref() == parent_path
970 } else {
971 false
972 }
973 })
974 }),
975 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt_buffer_id, excerpt_id, _)) => {
976 previous_entries.find(|entry| match entry {
977 PanelEntry::Fs(FsEntry::File(_, _, file_buffer_id, file_excerpts)) => {
978 file_buffer_id == excerpt_buffer_id
979 && file_excerpts.contains(excerpt_id)
980 }
981 PanelEntry::Fs(FsEntry::ExternalFile(file_buffer_id, file_excerpts)) => {
982 file_buffer_id == excerpt_buffer_id
983 && file_excerpts.contains(excerpt_id)
984 }
985 _ => false,
986 })
987 }
988 PanelEntry::Outline(OutlineEntry::Outline(
989 outline_buffer_id,
990 outline_excerpt_id,
991 _,
992 )) => previous_entries.find(|entry| {
993 if let PanelEntry::Outline(OutlineEntry::Excerpt(
994 excerpt_buffer_id,
995 excerpt_id,
996 _,
997 )) = entry
998 {
999 outline_buffer_id == excerpt_buffer_id && outline_excerpt_id == excerpt_id
1000 } else {
1001 false
1002 }
1003 }),
1004 PanelEntry::Search(_) => {
1005 previous_entries.find(|entry| !matches!(entry, PanelEntry::Search(_)))
1006 }
1007 }
1008 }) {
1009 self.select_entry(entry_to_select.clone(), true, cx);
1010 } else {
1011 self.select_first(&SelectFirst {}, cx);
1012 }
1013 }
1014
1015 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
1016 if let Some(first_entry) = self.cached_entries.first() {
1017 self.select_entry(first_entry.entry.clone(), true, cx);
1018 }
1019 }
1020
1021 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
1022 if let Some(new_selection) = self
1023 .cached_entries
1024 .iter()
1025 .rev()
1026 .map(|cached_entry| &cached_entry.entry)
1027 .next()
1028 {
1029 self.select_entry(new_selection.clone(), true, cx);
1030 }
1031 }
1032
1033 fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
1034 if let Some(selected_entry) = self.selected_entry() {
1035 let index = self
1036 .cached_entries
1037 .iter()
1038 .position(|cached_entry| &cached_entry.entry == selected_entry);
1039 if let Some(index) = index {
1040 self.scroll_handle.scroll_to_item(index);
1041 cx.notify();
1042 }
1043 }
1044 }
1045
1046 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
1047 if !self.focus_handle.contains_focused(cx) {
1048 cx.emit(Event::Focus);
1049 }
1050 }
1051
1052 fn deploy_context_menu(
1053 &mut self,
1054 position: Point<Pixels>,
1055 entry: PanelEntry,
1056 cx: &mut ViewContext<Self>,
1057 ) {
1058 self.select_entry(entry.clone(), true, cx);
1059 let is_root = match &entry {
1060 PanelEntry::Fs(FsEntry::File(worktree_id, entry, ..))
1061 | PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self
1062 .project
1063 .read(cx)
1064 .worktree_for_id(*worktree_id, cx)
1065 .map(|worktree| {
1066 worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1067 })
1068 .unwrap_or(false),
1069 PanelEntry::FoldedDirs(worktree_id, entries) => entries
1070 .first()
1071 .and_then(|entry| {
1072 self.project
1073 .read(cx)
1074 .worktree_for_id(*worktree_id, cx)
1075 .map(|worktree| {
1076 worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1077 })
1078 })
1079 .unwrap_or(false),
1080 PanelEntry::Fs(FsEntry::ExternalFile(..)) => false,
1081 PanelEntry::Outline(..) => {
1082 cx.notify();
1083 return;
1084 }
1085 PanelEntry::Search(_) => {
1086 cx.notify();
1087 return;
1088 }
1089 };
1090 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
1091 let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(&entry);
1092 let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(&entry);
1093
1094 let context_menu = ContextMenu::build(cx, |menu, _| {
1095 menu.context(self.focus_handle.clone())
1096 .when(cfg!(target_os = "macos"), |menu| {
1097 menu.action("Reveal in Finder", Box::new(RevealInFileManager))
1098 })
1099 .when(cfg!(not(target_os = "macos")), |menu| {
1100 menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
1101 })
1102 .action("Open in Terminal", Box::new(OpenInTerminal))
1103 .when(is_unfoldable, |menu| {
1104 menu.action("Unfold Directory", Box::new(UnfoldDirectory))
1105 })
1106 .when(is_foldable, |menu| {
1107 menu.action("Fold Directory", Box::new(FoldDirectory))
1108 })
1109 .separator()
1110 .action("Copy Path", Box::new(CopyPath))
1111 .action("Copy Relative Path", Box::new(CopyRelativePath))
1112 });
1113 cx.focus_view(&context_menu);
1114 let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
1115 outline_panel.context_menu.take();
1116 cx.notify();
1117 });
1118 self.context_menu = Some((context_menu, position, subscription));
1119 cx.notify();
1120 }
1121
1122 fn is_unfoldable(&self, entry: &PanelEntry) -> bool {
1123 matches!(entry, PanelEntry::FoldedDirs(..))
1124 }
1125
1126 fn is_foldable(&self, entry: &PanelEntry) -> bool {
1127 let (directory_worktree, directory_entry) = match entry {
1128 PanelEntry::Fs(FsEntry::Directory(directory_worktree, directory_entry)) => {
1129 (*directory_worktree, Some(directory_entry))
1130 }
1131 _ => return false,
1132 };
1133 let Some(directory_entry) = directory_entry else {
1134 return false;
1135 };
1136
1137 if self
1138 .unfolded_dirs
1139 .get(&directory_worktree)
1140 .map_or(true, |unfolded_dirs| {
1141 !unfolded_dirs.contains(&directory_entry.id)
1142 })
1143 {
1144 return false;
1145 }
1146
1147 let children = self
1148 .fs_children_count
1149 .get(&directory_worktree)
1150 .and_then(|entries| entries.get(&directory_entry.path))
1151 .copied()
1152 .unwrap_or_default();
1153
1154 children.may_be_fold_part() && children.dirs > 0
1155 }
1156
1157 fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
1158 let entry_to_expand = match self.selected_entry() {
1159 Some(PanelEntry::FoldedDirs(worktree_id, dir_entries)) => dir_entries
1160 .last()
1161 .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)),
1162 Some(PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry))) => {
1163 Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
1164 }
1165 Some(PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _))) => {
1166 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1167 }
1168 Some(PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _))) => {
1169 Some(CollapsedEntry::ExternalFile(*buffer_id))
1170 }
1171 Some(PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _))) => {
1172 Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1173 }
1174 None | Some(PanelEntry::Search(_)) | Some(PanelEntry::Outline(..)) => None,
1175 };
1176 let Some(collapsed_entry) = entry_to_expand else {
1177 return;
1178 };
1179 let expanded = self.collapsed_entries.remove(&collapsed_entry);
1180 if expanded {
1181 if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
1182 self.project.update(cx, |project, cx| {
1183 project.expand_entry(worktree_id, dir_entry_id, cx);
1184 });
1185 }
1186 self.update_cached_entries(None, cx);
1187 } else {
1188 self.select_next(&SelectNext, cx)
1189 }
1190 }
1191
1192 fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
1193 let Some(selected_entry) = self.selected_entry().cloned() else {
1194 return;
1195 };
1196 match &selected_entry {
1197 PanelEntry::Fs(FsEntry::Directory(worktree_id, selected_dir_entry)) => {
1198 self.collapsed_entries
1199 .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id));
1200 self.select_entry(selected_entry, true, cx);
1201 self.update_cached_entries(None, cx);
1202 }
1203 PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1204 self.collapsed_entries
1205 .insert(CollapsedEntry::File(*worktree_id, *buffer_id));
1206 self.select_entry(selected_entry, true, cx);
1207 self.update_cached_entries(None, cx);
1208 }
1209 PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
1210 self.collapsed_entries
1211 .insert(CollapsedEntry::ExternalFile(*buffer_id));
1212 self.select_entry(selected_entry, true, cx);
1213 self.update_cached_entries(None, cx);
1214 }
1215 PanelEntry::FoldedDirs(worktree_id, dir_entries) => {
1216 if let Some(dir_entry) = dir_entries.last() {
1217 if self
1218 .collapsed_entries
1219 .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
1220 {
1221 self.select_entry(selected_entry, true, cx);
1222 self.update_cached_entries(None, cx);
1223 }
1224 }
1225 }
1226 PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
1227 if self
1228 .collapsed_entries
1229 .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1230 {
1231 self.select_entry(selected_entry, true, cx);
1232 self.update_cached_entries(None, cx);
1233 }
1234 }
1235 PanelEntry::Search(_) | PanelEntry::Outline(..) => {}
1236 }
1237 }
1238
1239 pub fn expand_all_entries(&mut self, _: &ExpandAllEntries, cx: &mut ViewContext<Self>) {
1240 let expanded_entries =
1241 self.fs_entries
1242 .iter()
1243 .fold(HashSet::default(), |mut entries, fs_entry| {
1244 match fs_entry {
1245 FsEntry::ExternalFile(buffer_id, _) => {
1246 entries.insert(CollapsedEntry::ExternalFile(*buffer_id));
1247 entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
1248 |excerpts| {
1249 excerpts.iter().map(|(excerpt_id, _)| {
1250 CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
1251 })
1252 },
1253 ));
1254 }
1255 FsEntry::Directory(worktree_id, entry) => {
1256 entries.insert(CollapsedEntry::Dir(*worktree_id, entry.id));
1257 }
1258 FsEntry::File(worktree_id, _, buffer_id, _) => {
1259 entries.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
1260 entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
1261 |excerpts| {
1262 excerpts.iter().map(|(excerpt_id, _)| {
1263 CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
1264 })
1265 },
1266 ));
1267 }
1268 }
1269 entries
1270 });
1271 self.collapsed_entries
1272 .retain(|entry| !expanded_entries.contains(entry));
1273 self.update_cached_entries(None, cx);
1274 }
1275
1276 pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
1277 let new_entries = self
1278 .cached_entries
1279 .iter()
1280 .flat_map(|cached_entry| match &cached_entry.entry {
1281 PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => {
1282 Some(CollapsedEntry::Dir(*worktree_id, entry.id))
1283 }
1284 PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1285 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1286 }
1287 PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
1288 Some(CollapsedEntry::ExternalFile(*buffer_id))
1289 }
1290 PanelEntry::FoldedDirs(worktree_id, entries) => {
1291 Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id))
1292 }
1293 PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
1294 Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1295 }
1296 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1297 })
1298 .collect::<Vec<_>>();
1299 self.collapsed_entries.extend(new_entries);
1300 self.update_cached_entries(None, cx);
1301 }
1302
1303 fn toggle_expanded(&mut self, entry: &PanelEntry, cx: &mut ViewContext<Self>) {
1304 match entry {
1305 PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry)) => {
1306 let entry_id = dir_entry.id;
1307 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1308 if self.collapsed_entries.remove(&collapsed_entry) {
1309 self.project
1310 .update(cx, |project, cx| {
1311 project.expand_entry(*worktree_id, entry_id, cx)
1312 })
1313 .unwrap_or_else(|| Task::ready(Ok(())))
1314 .detach_and_log_err(cx);
1315 } else {
1316 self.collapsed_entries.insert(collapsed_entry);
1317 }
1318 }
1319 PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1320 let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1321 if !self.collapsed_entries.remove(&collapsed_entry) {
1322 self.collapsed_entries.insert(collapsed_entry);
1323 }
1324 }
1325 PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
1326 let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id);
1327 if !self.collapsed_entries.remove(&collapsed_entry) {
1328 self.collapsed_entries.insert(collapsed_entry);
1329 }
1330 }
1331 PanelEntry::FoldedDirs(worktree_id, dir_entries) => {
1332 if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
1333 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1334 if self.collapsed_entries.remove(&collapsed_entry) {
1335 self.project
1336 .update(cx, |project, cx| {
1337 project.expand_entry(*worktree_id, entry_id, cx)
1338 })
1339 .unwrap_or_else(|| Task::ready(Ok(())))
1340 .detach_and_log_err(cx);
1341 } else {
1342 self.collapsed_entries.insert(collapsed_entry);
1343 }
1344 }
1345 }
1346 PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
1347 let collapsed_entry = CollapsedEntry::Excerpt(*buffer_id, *excerpt_id);
1348 if !self.collapsed_entries.remove(&collapsed_entry) {
1349 self.collapsed_entries.insert(collapsed_entry);
1350 }
1351 }
1352 PanelEntry::Search(_) | PanelEntry::Outline(..) => return,
1353 }
1354
1355 self.select_entry(entry.clone(), true, cx);
1356 self.update_cached_entries(None, cx);
1357 }
1358
1359 fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
1360 if let Some(clipboard_text) = self
1361 .selected_entry()
1362 .and_then(|entry| self.abs_path(entry, cx))
1363 .map(|p| p.to_string_lossy().to_string())
1364 {
1365 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1366 }
1367 }
1368
1369 fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1370 if let Some(clipboard_text) = self
1371 .selected_entry()
1372 .and_then(|entry| match entry {
1373 PanelEntry::Fs(entry) => self.relative_path(entry, cx),
1374 PanelEntry::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.clone()),
1375 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1376 })
1377 .map(|p| p.to_string_lossy().to_string())
1378 {
1379 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1380 }
1381 }
1382
1383 fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
1384 if let Some(abs_path) = self
1385 .selected_entry()
1386 .and_then(|entry| self.abs_path(entry, cx))
1387 {
1388 cx.reveal_path(&abs_path);
1389 }
1390 }
1391
1392 fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
1393 let selected_entry = self.selected_entry();
1394 let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx));
1395 let working_directory = if let (
1396 Some(abs_path),
1397 Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1398 ) = (&abs_path, selected_entry)
1399 {
1400 abs_path.parent().map(|p| p.to_owned())
1401 } else {
1402 abs_path
1403 };
1404
1405 if let Some(working_directory) = working_directory {
1406 cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
1407 }
1408 }
1409
1410 fn reveal_entry_for_selection(
1411 &mut self,
1412 editor: &View<Editor>,
1413 cx: &mut ViewContext<'_, Self>,
1414 ) {
1415 if !self.active {
1416 return;
1417 }
1418 if !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
1419 return;
1420 }
1421 let Some(entry_with_selection) = self.location_for_editor_selection(editor, cx) else {
1422 self.selected_entry = SelectedEntry::None;
1423 cx.notify();
1424 return;
1425 };
1426
1427 let project = self.project.clone();
1428 self.reveal_selection_task = cx.spawn(|outline_panel, mut cx| async move {
1429 cx.background_executor().timer(UPDATE_DEBOUNCE).await;
1430 let related_buffer_entry = match &entry_with_selection {
1431 PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1432 project.update(&mut cx, |project, cx| {
1433 let entry_id = project
1434 .buffer_for_id(*buffer_id, cx)
1435 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1436 project
1437 .worktree_for_id(*worktree_id, cx)
1438 .zip(entry_id)
1439 .and_then(|(worktree, entry_id)| {
1440 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1441 Some((worktree, entry))
1442 })
1443 })?
1444 }
1445 PanelEntry::Outline(outline_entry) => {
1446 let &(OutlineEntry::Outline(buffer_id, excerpt_id, _)
1447 | OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) = outline_entry;
1448 outline_panel.update(&mut cx, |outline_panel, cx| {
1449 outline_panel
1450 .collapsed_entries
1451 .remove(&CollapsedEntry::ExternalFile(buffer_id));
1452 outline_panel
1453 .collapsed_entries
1454 .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1455 let project = outline_panel.project.read(cx);
1456 let entry_id = project
1457 .buffer_for_id(buffer_id, cx)
1458 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1459
1460 entry_id.and_then(|entry_id| {
1461 project
1462 .worktree_for_entry(entry_id, cx)
1463 .and_then(|worktree| {
1464 let worktree_id = worktree.read(cx).id();
1465 outline_panel
1466 .collapsed_entries
1467 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1468 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1469 Some((worktree, entry))
1470 })
1471 })
1472 })?
1473 }
1474 PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
1475 PanelEntry::Search(SearchEntry { match_range, .. }) => match_range
1476 .start
1477 .buffer_id
1478 .or(match_range.end.buffer_id)
1479 .map(|buffer_id| {
1480 outline_panel.update(&mut cx, |outline_panel, cx| {
1481 outline_panel
1482 .collapsed_entries
1483 .remove(&CollapsedEntry::ExternalFile(buffer_id));
1484 let project = project.read(cx);
1485 let entry_id = project
1486 .buffer_for_id(buffer_id, cx)
1487 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1488
1489 entry_id.and_then(|entry_id| {
1490 project
1491 .worktree_for_entry(entry_id, cx)
1492 .and_then(|worktree| {
1493 let worktree_id = worktree.read(cx).id();
1494 outline_panel
1495 .collapsed_entries
1496 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1497 let entry =
1498 worktree.read(cx).entry_for_id(entry_id)?.clone();
1499 Some((worktree, entry))
1500 })
1501 })
1502 })
1503 })
1504 .transpose()?
1505 .flatten(),
1506 _ => return anyhow::Ok(()),
1507 };
1508 if let Some((worktree, buffer_entry)) = related_buffer_entry {
1509 outline_panel.update(&mut cx, |outline_panel, cx| {
1510 let worktree_id = worktree.read(cx).id();
1511 let mut dirs_to_expand = Vec::new();
1512 {
1513 let mut traversal = worktree.read(cx).traverse_from_path(
1514 true,
1515 true,
1516 true,
1517 buffer_entry.path.as_ref(),
1518 );
1519 let mut current_entry = buffer_entry;
1520 loop {
1521 if current_entry.is_dir()
1522 && outline_panel
1523 .collapsed_entries
1524 .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
1525 {
1526 dirs_to_expand.push(current_entry.id);
1527 }
1528
1529 if traversal.back_to_parent() {
1530 if let Some(parent_entry) = traversal.entry() {
1531 current_entry = parent_entry.clone();
1532 continue;
1533 }
1534 }
1535 break;
1536 }
1537 }
1538 for dir_to_expand in dirs_to_expand {
1539 project
1540 .update(cx, |project, cx| {
1541 project.expand_entry(worktree_id, dir_to_expand, cx)
1542 })
1543 .unwrap_or_else(|| Task::ready(Ok(())))
1544 .detach_and_log_err(cx)
1545 }
1546 })?
1547 }
1548
1549 outline_panel.update(&mut cx, |outline_panel, cx| {
1550 outline_panel.select_entry(entry_with_selection, false, cx);
1551 outline_panel.update_cached_entries(None, cx);
1552 })?;
1553
1554 anyhow::Ok(())
1555 });
1556 }
1557
1558 fn render_excerpt(
1559 &self,
1560 buffer_id: BufferId,
1561 excerpt_id: ExcerptId,
1562 range: &ExcerptRange<language::Anchor>,
1563 depth: usize,
1564 cx: &mut ViewContext<OutlinePanel>,
1565 ) -> Option<Stateful<Div>> {
1566 let item_id = ElementId::from(excerpt_id.to_proto() as usize);
1567 let is_active = match self.selected_entry() {
1568 Some(PanelEntry::Outline(OutlineEntry::Excerpt(
1569 selected_buffer_id,
1570 selected_excerpt_id,
1571 _,
1572 ))) => selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id,
1573 _ => false,
1574 };
1575 let has_outlines = self
1576 .excerpts
1577 .get(&buffer_id)
1578 .and_then(|excerpts| match &excerpts.get(&excerpt_id)?.outlines {
1579 ExcerptOutlines::Outlines(outlines) => Some(outlines),
1580 ExcerptOutlines::Invalidated(outlines) => Some(outlines),
1581 ExcerptOutlines::NotFetched => None,
1582 })
1583 .map_or(false, |outlines| !outlines.is_empty());
1584 let is_expanded = !self
1585 .collapsed_entries
1586 .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1587 let color = entry_git_aware_label_color(None, false, is_active);
1588 let icon = if has_outlines {
1589 FileIcons::get_chevron_icon(is_expanded, cx)
1590 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1591 } else {
1592 None
1593 }
1594 .unwrap_or_else(empty_icon);
1595
1596 let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
1597 let excerpt_range = range.context.to_point(&buffer_snapshot);
1598 let label_element = Label::new(format!(
1599 "Lines {}- {}",
1600 excerpt_range.start.row + 1,
1601 excerpt_range.end.row + 1,
1602 ))
1603 .single_line()
1604 .color(color)
1605 .into_any_element();
1606
1607 Some(self.entry_element(
1608 PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, range.clone())),
1609 item_id,
1610 depth,
1611 Some(icon),
1612 is_active,
1613 label_element,
1614 cx,
1615 ))
1616 }
1617
1618 fn render_outline(
1619 &self,
1620 buffer_id: BufferId,
1621 excerpt_id: ExcerptId,
1622 rendered_outline: &Outline,
1623 depth: usize,
1624 string_match: Option<&StringMatch>,
1625 cx: &mut ViewContext<Self>,
1626 ) -> Stateful<Div> {
1627 let (item_id, label_element) = (
1628 ElementId::from(SharedString::from(format!(
1629 "{buffer_id:?}|{excerpt_id:?}{:?}|{:?}",
1630 rendered_outline.range, &rendered_outline.text,
1631 ))),
1632 language::render_item(
1633 rendered_outline,
1634 string_match
1635 .map(|string_match| string_match.ranges().collect::<Vec<_>>())
1636 .unwrap_or_default(),
1637 cx,
1638 )
1639 .into_any_element(),
1640 );
1641 let is_active = match self.selected_entry() {
1642 Some(PanelEntry::Outline(OutlineEntry::Outline(
1643 selected_buffer_id,
1644 selected_excerpt_id,
1645 selected_entry,
1646 ))) => {
1647 selected_buffer_id == &buffer_id
1648 && selected_excerpt_id == &excerpt_id
1649 && selected_entry == rendered_outline
1650 }
1651 _ => false,
1652 };
1653 let icon = if self.is_singleton_active(cx) {
1654 None
1655 } else {
1656 Some(empty_icon())
1657 };
1658 self.entry_element(
1659 PanelEntry::Outline(OutlineEntry::Outline(
1660 buffer_id,
1661 excerpt_id,
1662 rendered_outline.clone(),
1663 )),
1664 item_id,
1665 depth,
1666 icon,
1667 is_active,
1668 label_element,
1669 cx,
1670 )
1671 }
1672
1673 fn render_entry(
1674 &self,
1675 rendered_entry: &FsEntry,
1676 depth: usize,
1677 string_match: Option<&StringMatch>,
1678 cx: &mut ViewContext<Self>,
1679 ) -> Stateful<Div> {
1680 let settings = OutlinePanelSettings::get_global(cx);
1681 let is_active = match self.selected_entry() {
1682 Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry,
1683 _ => false,
1684 };
1685 let (item_id, label_element, icon) = match rendered_entry {
1686 FsEntry::File(worktree_id, entry, ..) => {
1687 let name = self.entry_name(worktree_id, entry, cx);
1688 let color =
1689 entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1690 let icon = if settings.file_icons {
1691 FileIcons::get_icon(&entry.path, cx)
1692 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1693 } else {
1694 None
1695 };
1696 (
1697 ElementId::from(entry.id.to_proto() as usize),
1698 HighlightedLabel::new(
1699 name,
1700 string_match
1701 .map(|string_match| string_match.positions.clone())
1702 .unwrap_or_default(),
1703 )
1704 .color(color)
1705 .into_any_element(),
1706 icon.unwrap_or_else(empty_icon),
1707 )
1708 }
1709 FsEntry::Directory(worktree_id, entry) => {
1710 let name = self.entry_name(worktree_id, entry, cx);
1711
1712 let is_expanded = !self
1713 .collapsed_entries
1714 .contains(&CollapsedEntry::Dir(*worktree_id, entry.id));
1715 let color =
1716 entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1717 let icon = if settings.folder_icons {
1718 FileIcons::get_folder_icon(is_expanded, cx)
1719 } else {
1720 FileIcons::get_chevron_icon(is_expanded, cx)
1721 }
1722 .map(Icon::from_path)
1723 .map(|icon| icon.color(color).into_any_element());
1724 (
1725 ElementId::from(entry.id.to_proto() as usize),
1726 HighlightedLabel::new(
1727 name,
1728 string_match
1729 .map(|string_match| string_match.positions.clone())
1730 .unwrap_or_default(),
1731 )
1732 .color(color)
1733 .into_any_element(),
1734 icon.unwrap_or_else(empty_icon),
1735 )
1736 }
1737 FsEntry::ExternalFile(buffer_id, ..) => {
1738 let color = entry_label_color(is_active);
1739 let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) {
1740 Some(buffer_snapshot) => match buffer_snapshot.file() {
1741 Some(file) => {
1742 let path = file.path();
1743 let icon = if settings.file_icons {
1744 FileIcons::get_icon(path.as_ref(), cx)
1745 } else {
1746 None
1747 }
1748 .map(Icon::from_path)
1749 .map(|icon| icon.color(color).into_any_element());
1750 (icon, file_name(path.as_ref()))
1751 }
1752 None => (None, "Untitled".to_string()),
1753 },
1754 None => (None, "Unknown buffer".to_string()),
1755 };
1756 (
1757 ElementId::from(buffer_id.to_proto() as usize),
1758 HighlightedLabel::new(
1759 name,
1760 string_match
1761 .map(|string_match| string_match.positions.clone())
1762 .unwrap_or_default(),
1763 )
1764 .color(color)
1765 .into_any_element(),
1766 icon.unwrap_or_else(empty_icon),
1767 )
1768 }
1769 };
1770
1771 self.entry_element(
1772 PanelEntry::Fs(rendered_entry.clone()),
1773 item_id,
1774 depth,
1775 Some(icon),
1776 is_active,
1777 label_element,
1778 cx,
1779 )
1780 }
1781
1782 fn render_folded_dirs(
1783 &self,
1784 worktree_id: WorktreeId,
1785 dir_entries: &[Entry],
1786 depth: usize,
1787 string_match: Option<&StringMatch>,
1788 cx: &mut ViewContext<OutlinePanel>,
1789 ) -> Stateful<Div> {
1790 let settings = OutlinePanelSettings::get_global(cx);
1791 let is_active = match self.selected_entry() {
1792 Some(PanelEntry::FoldedDirs(selected_worktree_id, selected_entries)) => {
1793 selected_worktree_id == &worktree_id && selected_entries == dir_entries
1794 }
1795 _ => false,
1796 };
1797 let (item_id, label_element, icon) = {
1798 let name = self.dir_names_string(dir_entries, worktree_id, cx);
1799
1800 let is_expanded = dir_entries.iter().all(|dir| {
1801 !self
1802 .collapsed_entries
1803 .contains(&CollapsedEntry::Dir(worktree_id, dir.id))
1804 });
1805 let is_ignored = dir_entries.iter().any(|entry| entry.is_ignored);
1806 let git_status = dir_entries.first().and_then(|entry| entry.git_status);
1807 let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
1808 let icon = if settings.folder_icons {
1809 FileIcons::get_folder_icon(is_expanded, cx)
1810 } else {
1811 FileIcons::get_chevron_icon(is_expanded, cx)
1812 }
1813 .map(Icon::from_path)
1814 .map(|icon| icon.color(color).into_any_element());
1815 (
1816 ElementId::from(
1817 dir_entries
1818 .last()
1819 .map(|entry| entry.id.to_proto())
1820 .unwrap_or_else(|| worktree_id.to_proto()) as usize,
1821 ),
1822 HighlightedLabel::new(
1823 name,
1824 string_match
1825 .map(|string_match| string_match.positions.clone())
1826 .unwrap_or_default(),
1827 )
1828 .color(color)
1829 .into_any_element(),
1830 icon.unwrap_or_else(empty_icon),
1831 )
1832 };
1833
1834 self.entry_element(
1835 PanelEntry::FoldedDirs(worktree_id, dir_entries.to_vec()),
1836 item_id,
1837 depth,
1838 Some(icon),
1839 is_active,
1840 label_element,
1841 cx,
1842 )
1843 }
1844
1845 #[allow(clippy::too_many_arguments)]
1846 fn render_search_match(
1847 &mut self,
1848 multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
1849 match_range: &Range<editor::Anchor>,
1850 search_data: &Arc<SearchData>,
1851 kind: SearchKind,
1852 depth: usize,
1853 string_match: Option<&StringMatch>,
1854 cx: &mut ViewContext<Self>,
1855 ) -> Stateful<Div> {
1856 if let ItemsDisplayMode::Search(search_state) = &mut self.mode {
1857 if let Some(multi_buffer_snapshot) = multi_buffer_snapshot {
1858 search_state.highlight_search_match(match_range, multi_buffer_snapshot);
1859 }
1860 }
1861
1862 let search_matches = string_match
1863 .iter()
1864 .flat_map(|string_match| string_match.ranges())
1865 .collect::<Vec<_>>();
1866 let match_ranges = if search_matches.is_empty() {
1867 &search_data.search_match_indices
1868 } else {
1869 &search_matches
1870 };
1871 let label_element = language::render_item(
1872 &OutlineItem {
1873 depth,
1874 annotation_range: None,
1875 range: search_data.context_range.clone(),
1876 text: search_data.context_text.clone(),
1877 highlight_ranges: search_data
1878 .highlights_data
1879 .get()
1880 .cloned()
1881 .unwrap_or_default(),
1882 name_ranges: search_data.search_match_indices.clone(),
1883 body_range: Some(search_data.context_range.clone()),
1884 },
1885 match_ranges.iter().cloned(),
1886 cx,
1887 );
1888 let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
1889 let entire_label = h_flex()
1890 .justify_center()
1891 .p_0()
1892 .when(search_data.truncated_left, |parent| {
1893 parent.child(truncated_contents_label())
1894 })
1895 .child(label_element)
1896 .when(search_data.truncated_right, |parent| {
1897 parent.child(truncated_contents_label())
1898 })
1899 .into_any_element();
1900
1901 let is_active = match self.selected_entry() {
1902 Some(PanelEntry::Search(SearchEntry {
1903 match_range: selected_match_range,
1904 ..
1905 })) => match_range == selected_match_range,
1906 _ => false,
1907 };
1908 self.entry_element(
1909 PanelEntry::Search(SearchEntry {
1910 kind,
1911 match_range: match_range.clone(),
1912 render_data: Arc::clone(search_data),
1913 }),
1914 ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
1915 depth,
1916 None,
1917 is_active,
1918 entire_label,
1919 cx,
1920 )
1921 }
1922
1923 #[allow(clippy::too_many_arguments)]
1924 fn entry_element(
1925 &self,
1926 rendered_entry: PanelEntry,
1927 item_id: ElementId,
1928 depth: usize,
1929 icon_element: Option<AnyElement>,
1930 is_active: bool,
1931 label_element: gpui::AnyElement,
1932 cx: &mut ViewContext<OutlinePanel>,
1933 ) -> Stateful<Div> {
1934 let settings = OutlinePanelSettings::get_global(cx);
1935 div()
1936 .text_ui(cx)
1937 .id(item_id.clone())
1938 .child(
1939 ListItem::new(item_id)
1940 .indent_level(depth)
1941 .indent_step_size(px(settings.indent_size))
1942 .selected(is_active)
1943 .when_some(icon_element, |list_item, icon_element| {
1944 list_item.child(h_flex().child(icon_element))
1945 })
1946 .child(h_flex().h_6().child(label_element).ml_1())
1947 .on_click({
1948 let clicked_entry = rendered_entry.clone();
1949 cx.listener(move |outline_panel, event: &gpui::ClickEvent, cx| {
1950 if event.down.button == MouseButton::Right || event.down.first_mouse {
1951 return;
1952 }
1953 let change_selection = event.down.click_count > 1;
1954 outline_panel.open_entry(&clicked_entry, change_selection, cx);
1955 })
1956 })
1957 .on_secondary_mouse_down(cx.listener(
1958 move |outline_panel, event: &MouseDownEvent, cx| {
1959 // Stop propagation to prevent the catch-all context menu for the project
1960 // panel from being deployed.
1961 cx.stop_propagation();
1962 outline_panel.deploy_context_menu(
1963 event.position,
1964 rendered_entry.clone(),
1965 cx,
1966 )
1967 },
1968 )),
1969 )
1970 .border_1()
1971 .border_r_2()
1972 .rounded_none()
1973 .hover(|style| {
1974 if is_active {
1975 style
1976 } else {
1977 let hover_color = cx.theme().colors().ghost_element_hover;
1978 style.bg(hover_color).border_color(hover_color)
1979 }
1980 })
1981 .when(is_active && self.focus_handle.contains_focused(cx), |div| {
1982 div.border_color(Color::Selected.color(cx))
1983 })
1984 }
1985
1986 fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &AppContext) -> String {
1987 let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
1988 Some(worktree) => {
1989 let worktree = worktree.read(cx);
1990 match worktree.snapshot().root_entry() {
1991 Some(root_entry) => {
1992 if root_entry.id == entry.id {
1993 file_name(worktree.abs_path().as_ref())
1994 } else {
1995 let path = worktree.absolutize(entry.path.as_ref()).ok();
1996 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
1997 file_name(path)
1998 }
1999 }
2000 None => {
2001 let path = worktree.absolutize(entry.path.as_ref()).ok();
2002 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2003 file_name(path)
2004 }
2005 }
2006 }
2007 None => file_name(entry.path.as_ref()),
2008 };
2009 name
2010 }
2011
2012 fn update_fs_entries(
2013 &mut self,
2014 active_editor: &View<Editor>,
2015 new_entries: HashSet<ExcerptId>,
2016 debounce: Option<Duration>,
2017 cx: &mut ViewContext<Self>,
2018 ) {
2019 if !self.active {
2020 return;
2021 }
2022
2023 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2024 let active_multi_buffer = active_editor.read(cx).buffer().clone();
2025 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
2026 let mut new_collapsed_entries = self.collapsed_entries.clone();
2027 let mut new_unfolded_dirs = self.unfolded_dirs.clone();
2028 let mut root_entries = HashSet::default();
2029 let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
2030 let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
2031 HashMap::default(),
2032 |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
2033 let buffer_id = buffer_snapshot.remote_id();
2034 let file = File::from_dyn(buffer_snapshot.file());
2035 let entry_id = file.and_then(|file| file.project_entry_id(cx));
2036 let worktree = file.map(|file| file.worktree.read(cx).snapshot());
2037 let is_new =
2038 new_entries.contains(&excerpt_id) || !self.excerpts.contains_key(&buffer_id);
2039 buffer_excerpts
2040 .entry(buffer_id)
2041 .or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
2042 .1
2043 .push(excerpt_id);
2044
2045 let outlines = match self
2046 .excerpts
2047 .get(&buffer_id)
2048 .and_then(|excerpts| excerpts.get(&excerpt_id))
2049 {
2050 Some(old_excerpt) => match &old_excerpt.outlines {
2051 ExcerptOutlines::Outlines(outlines) => {
2052 ExcerptOutlines::Outlines(outlines.clone())
2053 }
2054 ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
2055 ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
2056 },
2057 None => ExcerptOutlines::NotFetched,
2058 };
2059 new_excerpts.entry(buffer_id).or_default().insert(
2060 excerpt_id,
2061 Excerpt {
2062 range: excerpt_range,
2063 outlines,
2064 },
2065 );
2066 buffer_excerpts
2067 },
2068 );
2069
2070 self.updating_fs_entries = true;
2071 self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
2072 if let Some(debounce) = debounce {
2073 cx.background_executor().timer(debounce).await;
2074 }
2075 let Some((
2076 new_collapsed_entries,
2077 new_unfolded_dirs,
2078 new_fs_entries,
2079 new_depth_map,
2080 new_children_count,
2081 )) = cx
2082 .background_executor()
2083 .spawn(async move {
2084 let mut processed_external_buffers = HashSet::default();
2085 let mut new_worktree_entries =
2086 HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
2087 let mut worktree_excerpts = HashMap::<
2088 WorktreeId,
2089 HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
2090 >::default();
2091 let mut external_excerpts = HashMap::default();
2092
2093 for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
2094 if is_new {
2095 match &worktree {
2096 Some(worktree) => {
2097 new_collapsed_entries
2098 .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
2099 }
2100 None => {
2101 new_collapsed_entries
2102 .remove(&CollapsedEntry::ExternalFile(buffer_id));
2103 }
2104 }
2105 }
2106
2107 if let Some(worktree) = worktree {
2108 let worktree_id = worktree.id();
2109 let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
2110
2111 match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
2112 Some(entry) => {
2113 let mut traversal = worktree.traverse_from_path(
2114 true,
2115 true,
2116 true,
2117 entry.path.as_ref(),
2118 );
2119
2120 let mut entries_to_add = HashSet::default();
2121 worktree_excerpts
2122 .entry(worktree_id)
2123 .or_default()
2124 .insert(entry.id, (buffer_id, excerpts));
2125 let mut current_entry = entry;
2126 loop {
2127 if current_entry.is_dir() {
2128 let is_root =
2129 worktree.root_entry().map(|entry| entry.id)
2130 == Some(current_entry.id);
2131 if is_root {
2132 root_entries.insert(current_entry.id);
2133 if auto_fold_dirs {
2134 unfolded_dirs.insert(current_entry.id);
2135 }
2136 }
2137 if is_new {
2138 new_collapsed_entries.remove(&CollapsedEntry::Dir(
2139 worktree_id,
2140 current_entry.id,
2141 ));
2142 }
2143 }
2144
2145 let new_entry_added = entries_to_add.insert(current_entry);
2146 if new_entry_added && traversal.back_to_parent() {
2147 if let Some(parent_entry) = traversal.entry() {
2148 current_entry = parent_entry.clone();
2149 continue;
2150 }
2151 }
2152 break;
2153 }
2154 new_worktree_entries
2155 .entry(worktree_id)
2156 .or_insert_with(|| (worktree.clone(), HashSet::default()))
2157 .1
2158 .extend(entries_to_add);
2159 }
2160 None => {
2161 if processed_external_buffers.insert(buffer_id) {
2162 external_excerpts
2163 .entry(buffer_id)
2164 .or_insert_with(Vec::new)
2165 .extend(excerpts);
2166 }
2167 }
2168 }
2169 } else if processed_external_buffers.insert(buffer_id) {
2170 external_excerpts
2171 .entry(buffer_id)
2172 .or_insert_with(Vec::new)
2173 .extend(excerpts);
2174 }
2175 }
2176
2177 let mut new_children_count =
2178 HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
2179
2180 let worktree_entries = new_worktree_entries
2181 .into_iter()
2182 .map(|(worktree_id, (worktree_snapshot, entries))| {
2183 let mut entries = entries.into_iter().collect::<Vec<_>>();
2184 // For a proper git status propagation, we have to keep the entries sorted lexicographically.
2185 entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
2186 worktree_snapshot.propagate_git_statuses(&mut entries);
2187 project::sort_worktree_entries(&mut entries);
2188 (worktree_id, entries)
2189 })
2190 .flat_map(|(worktree_id, entries)| {
2191 {
2192 entries
2193 .into_iter()
2194 .filter_map(|entry| {
2195 if auto_fold_dirs {
2196 if let Some(parent) = entry.path.parent() {
2197 let children = new_children_count
2198 .entry(worktree_id)
2199 .or_default()
2200 .entry(Arc::from(parent))
2201 .or_default();
2202 if entry.is_dir() {
2203 children.dirs += 1;
2204 } else {
2205 children.files += 1;
2206 }
2207 }
2208 }
2209
2210 if entry.is_dir() {
2211 Some(FsEntry::Directory(worktree_id, entry))
2212 } else {
2213 let (buffer_id, excerpts) = worktree_excerpts
2214 .get_mut(&worktree_id)
2215 .and_then(|worktree_excerpts| {
2216 worktree_excerpts.remove(&entry.id)
2217 })?;
2218 Some(FsEntry::File(
2219 worktree_id,
2220 entry,
2221 buffer_id,
2222 excerpts,
2223 ))
2224 }
2225 })
2226 .collect::<Vec<_>>()
2227 }
2228 })
2229 .collect::<Vec<_>>();
2230
2231 let mut visited_dirs = Vec::new();
2232 let mut new_depth_map = HashMap::default();
2233 let new_visible_entries = external_excerpts
2234 .into_iter()
2235 .sorted_by_key(|(id, _)| *id)
2236 .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
2237 .chain(worktree_entries)
2238 .filter(|visible_item| {
2239 match visible_item {
2240 FsEntry::Directory(worktree_id, dir_entry) => {
2241 let parent_id = back_to_common_visited_parent(
2242 &mut visited_dirs,
2243 worktree_id,
2244 dir_entry,
2245 );
2246
2247 let depth = if root_entries.contains(&dir_entry.id) {
2248 0
2249 } else {
2250 if auto_fold_dirs {
2251 let children = new_children_count
2252 .get(worktree_id)
2253 .and_then(|children_count| {
2254 children_count.get(&dir_entry.path)
2255 })
2256 .copied()
2257 .unwrap_or_default();
2258
2259 if !children.may_be_fold_part()
2260 || (children.dirs == 0
2261 && visited_dirs
2262 .last()
2263 .map(|(parent_dir_id, _)| {
2264 new_unfolded_dirs
2265 .get(worktree_id)
2266 .map_or(true, |unfolded_dirs| {
2267 unfolded_dirs
2268 .contains(parent_dir_id)
2269 })
2270 })
2271 .unwrap_or(true))
2272 {
2273 new_unfolded_dirs
2274 .entry(*worktree_id)
2275 .or_default()
2276 .insert(dir_entry.id);
2277 }
2278 }
2279
2280 parent_id
2281 .and_then(|(worktree_id, id)| {
2282 new_depth_map.get(&(worktree_id, id)).copied()
2283 })
2284 .unwrap_or(0)
2285 + 1
2286 };
2287 visited_dirs.push((dir_entry.id, dir_entry.path.clone()));
2288 new_depth_map.insert((*worktree_id, dir_entry.id), depth);
2289 }
2290 FsEntry::File(worktree_id, file_entry, ..) => {
2291 let parent_id = back_to_common_visited_parent(
2292 &mut visited_dirs,
2293 worktree_id,
2294 file_entry,
2295 );
2296 let depth = if root_entries.contains(&file_entry.id) {
2297 0
2298 } else {
2299 parent_id
2300 .and_then(|(worktree_id, id)| {
2301 new_depth_map.get(&(worktree_id, id)).copied()
2302 })
2303 .unwrap_or(0)
2304 + 1
2305 };
2306 new_depth_map.insert((*worktree_id, file_entry.id), depth);
2307 }
2308 FsEntry::ExternalFile(..) => {
2309 visited_dirs.clear();
2310 }
2311 }
2312
2313 true
2314 })
2315 .collect::<Vec<_>>();
2316
2317 anyhow::Ok((
2318 new_collapsed_entries,
2319 new_unfolded_dirs,
2320 new_visible_entries,
2321 new_depth_map,
2322 new_children_count,
2323 ))
2324 })
2325 .await
2326 .log_err()
2327 else {
2328 return;
2329 };
2330
2331 outline_panel
2332 .update(&mut cx, |outline_panel, cx| {
2333 outline_panel.updating_fs_entries = false;
2334 outline_panel.excerpts = new_excerpts;
2335 outline_panel.collapsed_entries = new_collapsed_entries;
2336 outline_panel.unfolded_dirs = new_unfolded_dirs;
2337 outline_panel.fs_entries = new_fs_entries;
2338 outline_panel.fs_entries_depth = new_depth_map;
2339 outline_panel.fs_children_count = new_children_count;
2340 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
2341 outline_panel.update_non_fs_items(cx);
2342
2343 cx.notify();
2344 })
2345 .ok();
2346 });
2347 }
2348
2349 fn replace_active_editor(
2350 &mut self,
2351 new_active_item: Box<dyn ItemHandle>,
2352 new_active_editor: View<Editor>,
2353 cx: &mut ViewContext<Self>,
2354 ) {
2355 self.clear_previous(cx);
2356 let buffer_search_subscription = cx.subscribe(
2357 &new_active_editor,
2358 |outline_panel: &mut Self, _, e: &SearchEvent, cx: &mut ViewContext<'_, Self>| {
2359 if matches!(e, SearchEvent::MatchesInvalidated) {
2360 outline_panel.update_search_matches(cx);
2361 };
2362 outline_panel.autoscroll(cx);
2363 },
2364 );
2365 self.active_item = Some(ActiveItem {
2366 _buffer_search_subscription: buffer_search_subscription,
2367 _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, cx),
2368 item_handle: new_active_item.downgrade_item(),
2369 active_editor: new_active_editor.downgrade(),
2370 });
2371 let new_entries =
2372 HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
2373 self.selected_entry.invalidate();
2374 self.update_fs_entries(&new_active_editor, new_entries, None, cx);
2375 }
2376
2377 fn clear_previous(&mut self, cx: &mut WindowContext<'_>) {
2378 self.filter_editor.update(cx, |editor, cx| editor.clear(cx));
2379 self.collapsed_entries.clear();
2380 self.unfolded_dirs.clear();
2381 self.selected_entry = SelectedEntry::None;
2382 self.fs_entries_update_task = Task::ready(());
2383 self.cached_entries_update_task = Task::ready(());
2384 self.active_item = None;
2385 self.fs_entries.clear();
2386 self.fs_entries_depth.clear();
2387 self.fs_children_count.clear();
2388 self.outline_fetch_tasks.clear();
2389 self.excerpts.clear();
2390 self.cached_entries = Vec::new();
2391 self.pinned = false;
2392 self.mode = ItemsDisplayMode::Outline;
2393 }
2394
2395 fn location_for_editor_selection(
2396 &mut self,
2397 editor: &View<Editor>,
2398 cx: &mut ViewContext<Self>,
2399 ) -> Option<PanelEntry> {
2400 let selection = editor
2401 .read(cx)
2402 .selections
2403 .newest::<language::Point>(cx)
2404 .head();
2405 let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx));
2406 let multi_buffer = editor.read(cx).buffer();
2407 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
2408 let (excerpt_id, buffer, _) = editor
2409 .read(cx)
2410 .buffer()
2411 .read(cx)
2412 .excerpt_containing(selection, cx)?;
2413 let buffer_id = buffer.read(cx).remote_id();
2414 let selection_display_point = selection.to_display_point(&editor_snapshot);
2415
2416 match &self.mode {
2417 ItemsDisplayMode::Search(search_state) => search_state
2418 .matches
2419 .iter()
2420 .rev()
2421 .min_by_key(|&(match_range, _)| {
2422 let match_display_range =
2423 match_range.clone().to_display_points(&editor_snapshot);
2424 let start_distance = if selection_display_point < match_display_range.start {
2425 match_display_range.start - selection_display_point
2426 } else {
2427 selection_display_point - match_display_range.start
2428 };
2429 let end_distance = if selection_display_point < match_display_range.end {
2430 match_display_range.end - selection_display_point
2431 } else {
2432 selection_display_point - match_display_range.end
2433 };
2434 start_distance + end_distance
2435 })
2436 .and_then(|(closest_range, _)| {
2437 self.cached_entries.iter().find_map(|cached_entry| {
2438 if let PanelEntry::Search(SearchEntry { match_range, .. }) =
2439 &cached_entry.entry
2440 {
2441 if match_range == closest_range {
2442 Some(cached_entry.entry.clone())
2443 } else {
2444 None
2445 }
2446 } else {
2447 None
2448 }
2449 })
2450 }),
2451 ItemsDisplayMode::Outline => self.outline_location(
2452 buffer_id,
2453 excerpt_id,
2454 multi_buffer_snapshot,
2455 editor_snapshot,
2456 selection_display_point,
2457 ),
2458 }
2459 }
2460
2461 fn outline_location(
2462 &mut self,
2463 buffer_id: BufferId,
2464 excerpt_id: ExcerptId,
2465 multi_buffer_snapshot: editor::MultiBufferSnapshot,
2466 editor_snapshot: editor::EditorSnapshot,
2467 selection_display_point: DisplayPoint,
2468 ) -> Option<PanelEntry> {
2469 let excerpt_outlines = self
2470 .excerpts
2471 .get(&buffer_id)
2472 .and_then(|excerpts| excerpts.get(&excerpt_id))
2473 .into_iter()
2474 .flat_map(|excerpt| excerpt.iter_outlines())
2475 .flat_map(|outline| {
2476 let start = multi_buffer_snapshot
2477 .anchor_in_excerpt(excerpt_id, outline.range.start)?
2478 .to_display_point(&editor_snapshot);
2479 let end = multi_buffer_snapshot
2480 .anchor_in_excerpt(excerpt_id, outline.range.end)?
2481 .to_display_point(&editor_snapshot);
2482 Some((start..end, outline))
2483 })
2484 .collect::<Vec<_>>();
2485
2486 let mut matching_outline_indices = Vec::new();
2487 let mut children = HashMap::default();
2488 let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
2489
2490 for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
2491 if outline_range
2492 .to_inclusive()
2493 .contains(&selection_display_point)
2494 {
2495 matching_outline_indices.push(i);
2496 } else if (outline_range.start.row()..outline_range.end.row())
2497 .to_inclusive()
2498 .contains(&selection_display_point.row())
2499 {
2500 matching_outline_indices.push(i);
2501 }
2502
2503 while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
2504 if parent_outline.depth >= outline.depth
2505 || !parent_range.contains(&outline_range.start)
2506 {
2507 parents_stack.pop();
2508 } else {
2509 break;
2510 }
2511 }
2512 if let Some((_, _, parent_index)) = parents_stack.last_mut() {
2513 children
2514 .entry(*parent_index)
2515 .or_insert_with(Vec::new)
2516 .push(i);
2517 }
2518 parents_stack.push((outline_range, outline, i));
2519 }
2520
2521 let outline_item = matching_outline_indices
2522 .into_iter()
2523 .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
2524 .filter(|(i, _)| {
2525 children
2526 .get(i)
2527 .map(|children| {
2528 children.iter().all(|child_index| {
2529 excerpt_outlines
2530 .get(*child_index)
2531 .map(|(child_range, _)| child_range.start > selection_display_point)
2532 .unwrap_or(false)
2533 })
2534 })
2535 .unwrap_or(true)
2536 })
2537 .min_by_key(|(_, (outline_range, outline))| {
2538 let distance_from_start = if outline_range.start > selection_display_point {
2539 outline_range.start - selection_display_point
2540 } else {
2541 selection_display_point - outline_range.start
2542 };
2543 let distance_from_end = if outline_range.end > selection_display_point {
2544 outline_range.end - selection_display_point
2545 } else {
2546 selection_display_point - outline_range.end
2547 };
2548
2549 (
2550 cmp::Reverse(outline.depth),
2551 distance_from_start + distance_from_end,
2552 )
2553 })
2554 .map(|(_, (_, outline))| *outline)
2555 .cloned();
2556
2557 let closest_container = match outline_item {
2558 Some(outline) => {
2559 PanelEntry::Outline(OutlineEntry::Outline(buffer_id, excerpt_id, outline))
2560 }
2561 None => {
2562 self.cached_entries.iter().rev().find_map(|cached_entry| {
2563 match &cached_entry.entry {
2564 PanelEntry::Outline(OutlineEntry::Excerpt(
2565 entry_buffer_id,
2566 entry_excerpt_id,
2567 _,
2568 )) => {
2569 if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
2570 Some(cached_entry.entry.clone())
2571 } else {
2572 None
2573 }
2574 }
2575 PanelEntry::Fs(
2576 FsEntry::ExternalFile(file_buffer_id, file_excerpts)
2577 | FsEntry::File(_, _, file_buffer_id, file_excerpts),
2578 ) => {
2579 if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
2580 Some(cached_entry.entry.clone())
2581 } else {
2582 None
2583 }
2584 }
2585 _ => None,
2586 }
2587 })?
2588 }
2589 };
2590 Some(closest_container)
2591 }
2592
2593 fn fetch_outdated_outlines(&mut self, cx: &mut ViewContext<Self>) {
2594 let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
2595 if excerpt_fetch_ranges.is_empty() {
2596 return;
2597 }
2598
2599 let syntax_theme = cx.theme().syntax().clone();
2600 for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
2601 for (excerpt_id, excerpt_range) in excerpt_ranges {
2602 let syntax_theme = syntax_theme.clone();
2603 let buffer_snapshot = buffer_snapshot.clone();
2604 self.outline_fetch_tasks.insert(
2605 (buffer_id, excerpt_id),
2606 cx.spawn(|outline_panel, mut cx| async move {
2607 let fetched_outlines = cx
2608 .background_executor()
2609 .spawn(async move {
2610 buffer_snapshot
2611 .outline_items_containing(
2612 excerpt_range.context,
2613 false,
2614 Some(&syntax_theme),
2615 )
2616 .unwrap_or_default()
2617 })
2618 .await;
2619 outline_panel
2620 .update(&mut cx, |outline_panel, cx| {
2621 if let Some(excerpt) = outline_panel
2622 .excerpts
2623 .entry(buffer_id)
2624 .or_default()
2625 .get_mut(&excerpt_id)
2626 {
2627 excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
2628 }
2629 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
2630 })
2631 .ok();
2632 }),
2633 );
2634 }
2635 }
2636 }
2637
2638 fn is_singleton_active(&self, cx: &AppContext) -> bool {
2639 self.active_editor().map_or(false, |active_editor| {
2640 active_editor.read(cx).buffer().read(cx).is_singleton()
2641 })
2642 }
2643
2644 fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
2645 self.outline_fetch_tasks.clear();
2646 let mut ids = ids.iter().collect::<HashSet<_>>();
2647 for excerpts in self.excerpts.values_mut() {
2648 ids.retain(|id| {
2649 if let Some(excerpt) = excerpts.get_mut(id) {
2650 excerpt.invalidate_outlines();
2651 false
2652 } else {
2653 true
2654 }
2655 });
2656 if ids.is_empty() {
2657 break;
2658 }
2659 }
2660 }
2661
2662 fn excerpt_fetch_ranges(
2663 &self,
2664 cx: &AppContext,
2665 ) -> HashMap<
2666 BufferId,
2667 (
2668 BufferSnapshot,
2669 HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
2670 ),
2671 > {
2672 self.fs_entries
2673 .iter()
2674 .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
2675 match fs_entry {
2676 FsEntry::File(_, _, buffer_id, file_excerpts)
2677 | FsEntry::ExternalFile(buffer_id, file_excerpts) => {
2678 let excerpts = self.excerpts.get(buffer_id);
2679 for &file_excerpt in file_excerpts {
2680 if let Some(excerpt) = excerpts
2681 .and_then(|excerpts| excerpts.get(&file_excerpt))
2682 .filter(|excerpt| excerpt.should_fetch_outlines())
2683 {
2684 match excerpts_to_fetch.entry(*buffer_id) {
2685 hash_map::Entry::Occupied(mut o) => {
2686 o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
2687 }
2688 hash_map::Entry::Vacant(v) => {
2689 if let Some(buffer_snapshot) =
2690 self.buffer_snapshot_for_id(*buffer_id, cx)
2691 {
2692 v.insert((buffer_snapshot, HashMap::default()))
2693 .1
2694 .insert(file_excerpt, excerpt.range.clone());
2695 }
2696 }
2697 }
2698 }
2699 }
2700 }
2701 FsEntry::Directory(..) => {}
2702 }
2703 excerpts_to_fetch
2704 })
2705 }
2706
2707 fn buffer_snapshot_for_id(
2708 &self,
2709 buffer_id: BufferId,
2710 cx: &AppContext,
2711 ) -> Option<BufferSnapshot> {
2712 let editor = self.active_editor()?;
2713 Some(
2714 editor
2715 .read(cx)
2716 .buffer()
2717 .read(cx)
2718 .buffer(buffer_id)?
2719 .read(cx)
2720 .snapshot(),
2721 )
2722 }
2723
2724 fn abs_path(&self, entry: &PanelEntry, cx: &AppContext) -> Option<PathBuf> {
2725 match entry {
2726 PanelEntry::Fs(
2727 FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
2728 ) => self
2729 .buffer_snapshot_for_id(*buffer_id, cx)
2730 .and_then(|buffer_snapshot| {
2731 let file = File::from_dyn(buffer_snapshot.file())?;
2732 file.worktree.read(cx).absolutize(&file.path).ok()
2733 }),
2734 PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self
2735 .project
2736 .read(cx)
2737 .worktree_for_id(*worktree_id, cx)?
2738 .read(cx)
2739 .absolutize(&entry.path)
2740 .ok(),
2741 PanelEntry::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
2742 self.project
2743 .read(cx)
2744 .worktree_for_id(*worktree_id, cx)
2745 .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
2746 }),
2747 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
2748 }
2749 }
2750
2751 fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option<Arc<Path>> {
2752 match entry {
2753 FsEntry::ExternalFile(buffer_id, _) => {
2754 let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
2755 Some(buffer_snapshot.file()?.path().clone())
2756 }
2757 FsEntry::Directory(_, entry) => Some(entry.path.clone()),
2758 FsEntry::File(_, entry, ..) => Some(entry.path.clone()),
2759 }
2760 }
2761
2762 fn update_cached_entries(
2763 &mut self,
2764 debounce: Option<Duration>,
2765 cx: &mut ViewContext<OutlinePanel>,
2766 ) {
2767 if !self.active {
2768 return;
2769 }
2770
2771 let is_singleton = self.is_singleton_active(cx);
2772 let query = self.query(cx);
2773 self.cached_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
2774 if let Some(debounce) = debounce {
2775 cx.background_executor().timer(debounce).await;
2776 }
2777 let Some(new_cached_entries) = outline_panel
2778 .update(&mut cx, |outline_panel, cx| {
2779 outline_panel.generate_cached_entries(is_singleton, query, cx)
2780 })
2781 .ok()
2782 else {
2783 return;
2784 };
2785 let new_cached_entries = new_cached_entries.await;
2786 outline_panel
2787 .update(&mut cx, |outline_panel, cx| {
2788 outline_panel.cached_entries = new_cached_entries;
2789 if outline_panel.selected_entry.is_invalidated() {
2790 if let Some(new_selected_entry) =
2791 outline_panel.active_editor().and_then(|active_editor| {
2792 outline_panel.location_for_editor_selection(&active_editor, cx)
2793 })
2794 {
2795 outline_panel.select_entry(new_selected_entry, false, cx);
2796 }
2797 }
2798
2799 outline_panel.autoscroll(cx);
2800 cx.notify();
2801 })
2802 .ok();
2803 });
2804 }
2805
2806 fn generate_cached_entries(
2807 &self,
2808 is_singleton: bool,
2809 query: Option<String>,
2810 cx: &mut ViewContext<'_, Self>,
2811 ) -> Task<Vec<CachedEntry>> {
2812 let project = self.project.clone();
2813 cx.spawn(|outline_panel, mut cx| async move {
2814 let mut entries = Vec::new();
2815 let mut match_candidates = Vec::new();
2816 let mut added_contexts = HashSet::default();
2817
2818 let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
2819 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2820 let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
2821 let track_matches = query.is_some();
2822
2823 #[derive(Debug)]
2824 struct ParentStats {
2825 path: Arc<Path>,
2826 folded: bool,
2827 expanded: bool,
2828 depth: usize,
2829 }
2830 let mut parent_dirs = Vec::<ParentStats>::new();
2831 for entry in outline_panel.fs_entries.clone() {
2832 let is_expanded = outline_panel.is_expanded(&entry);
2833 let (depth, should_add) = match &entry {
2834 FsEntry::Directory(worktree_id, dir_entry) => {
2835 let mut should_add = true;
2836 let is_root = project
2837 .read(cx)
2838 .worktree_for_id(*worktree_id, cx)
2839 .map_or(false, |worktree| {
2840 worktree.read(cx).root_entry() == Some(dir_entry)
2841 });
2842 let folded = auto_fold_dirs
2843 && !is_root
2844 && outline_panel
2845 .unfolded_dirs
2846 .get(worktree_id)
2847 .map_or(true, |unfolded_dirs| {
2848 !unfolded_dirs.contains(&dir_entry.id)
2849 });
2850 let fs_depth = outline_panel
2851 .fs_entries_depth
2852 .get(&(*worktree_id, dir_entry.id))
2853 .copied()
2854 .unwrap_or(0);
2855 while let Some(parent) = parent_dirs.last() {
2856 if dir_entry.path.starts_with(&parent.path) {
2857 break;
2858 }
2859 parent_dirs.pop();
2860 }
2861 let auto_fold = match parent_dirs.last() {
2862 Some(parent) => {
2863 parent.folded
2864 && Some(parent.path.as_ref()) == dir_entry.path.parent()
2865 && outline_panel
2866 .fs_children_count
2867 .get(worktree_id)
2868 .and_then(|entries| entries.get(&dir_entry.path))
2869 .copied()
2870 .unwrap_or_default()
2871 .may_be_fold_part()
2872 }
2873 None => false,
2874 };
2875 let folded = folded || auto_fold;
2876 let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
2877 Some(parent) => {
2878 let parent_folded = parent.folded;
2879 let parent_expanded = parent.expanded;
2880 let new_depth = if parent_folded {
2881 parent.depth
2882 } else {
2883 parent.depth + 1
2884 };
2885 parent_dirs.push(ParentStats {
2886 path: dir_entry.path.clone(),
2887 folded,
2888 expanded: parent_expanded && is_expanded,
2889 depth: new_depth,
2890 });
2891 (new_depth, parent_expanded, parent_folded)
2892 }
2893 None => {
2894 parent_dirs.push(ParentStats {
2895 path: dir_entry.path.clone(),
2896 folded,
2897 expanded: is_expanded,
2898 depth: fs_depth,
2899 });
2900 (fs_depth, true, false)
2901 }
2902 };
2903
2904 if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) =
2905 folded_dirs_entry.take()
2906 {
2907 if folded
2908 && worktree_id == &folded_worktree_id
2909 && dir_entry.path.parent()
2910 == folded_dirs.last().map(|entry| entry.path.as_ref())
2911 {
2912 folded_dirs.push(dir_entry.clone());
2913 folded_dirs_entry =
2914 Some((folded_depth, folded_worktree_id, folded_dirs))
2915 } else {
2916 if !is_singleton {
2917 let start_of_collapsed_dir_sequence = !parent_expanded
2918 && parent_dirs
2919 .iter()
2920 .rev()
2921 .nth(folded_dirs.len() + 1)
2922 .map_or(true, |parent| parent.expanded);
2923 if start_of_collapsed_dir_sequence
2924 || parent_expanded
2925 || query.is_some()
2926 {
2927 if parent_folded {
2928 folded_dirs.push(dir_entry.clone());
2929 should_add = false;
2930 }
2931 let new_folded_dirs = PanelEntry::FoldedDirs(
2932 folded_worktree_id,
2933 folded_dirs,
2934 );
2935 outline_panel.push_entry(
2936 &mut entries,
2937 &mut match_candidates,
2938 &mut added_contexts,
2939 track_matches,
2940 new_folded_dirs,
2941 folded_depth,
2942 cx,
2943 );
2944 }
2945 }
2946
2947 folded_dirs_entry = if parent_folded {
2948 None
2949 } else {
2950 Some((depth, *worktree_id, vec![dir_entry.clone()]))
2951 };
2952 }
2953 } else if folded {
2954 folded_dirs_entry =
2955 Some((depth, *worktree_id, vec![dir_entry.clone()]));
2956 }
2957
2958 let should_add =
2959 should_add && parent_expanded && folded_dirs_entry.is_none();
2960 (depth, should_add)
2961 }
2962 FsEntry::ExternalFile(..) => {
2963 if let Some((folded_depth, worktree_id, folded_dirs)) =
2964 folded_dirs_entry.take()
2965 {
2966 let parent_expanded = parent_dirs
2967 .iter()
2968 .rev()
2969 .find(|parent| {
2970 folded_dirs.iter().all(|entry| entry.path != parent.path)
2971 })
2972 .map_or(true, |parent| parent.expanded);
2973 if !is_singleton && (parent_expanded || query.is_some()) {
2974 outline_panel.push_entry(
2975 &mut entries,
2976 &mut match_candidates,
2977 &mut added_contexts,
2978 track_matches,
2979 PanelEntry::FoldedDirs(worktree_id, folded_dirs),
2980 folded_depth,
2981 cx,
2982 );
2983 }
2984 }
2985 parent_dirs.clear();
2986 (0, true)
2987 }
2988 FsEntry::File(worktree_id, file_entry, ..) => {
2989 if let Some((folded_depth, worktree_id, folded_dirs)) =
2990 folded_dirs_entry.take()
2991 {
2992 let parent_expanded = parent_dirs
2993 .iter()
2994 .rev()
2995 .find(|parent| {
2996 folded_dirs.iter().all(|entry| entry.path != parent.path)
2997 })
2998 .map_or(true, |parent| parent.expanded);
2999 if !is_singleton && (parent_expanded || query.is_some()) {
3000 outline_panel.push_entry(
3001 &mut entries,
3002 &mut match_candidates,
3003 &mut added_contexts,
3004 track_matches,
3005 PanelEntry::FoldedDirs(worktree_id, folded_dirs),
3006 folded_depth,
3007 cx,
3008 );
3009 }
3010 }
3011
3012 let fs_depth = outline_panel
3013 .fs_entries_depth
3014 .get(&(*worktree_id, file_entry.id))
3015 .copied()
3016 .unwrap_or(0);
3017 while let Some(parent) = parent_dirs.last() {
3018 if file_entry.path.starts_with(&parent.path) {
3019 break;
3020 }
3021 parent_dirs.pop();
3022 }
3023 let (depth, should_add) = match parent_dirs.last() {
3024 Some(parent) => {
3025 let new_depth = parent.depth + 1;
3026 (new_depth, parent.expanded)
3027 }
3028 None => (fs_depth, true),
3029 };
3030 (depth, should_add)
3031 }
3032 };
3033
3034 if !is_singleton
3035 && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3036 {
3037 outline_panel.push_entry(
3038 &mut entries,
3039 &mut match_candidates,
3040 &mut added_contexts,
3041 track_matches,
3042 PanelEntry::Fs(entry.clone()),
3043 depth,
3044 cx,
3045 );
3046 }
3047
3048 match outline_panel.mode {
3049 ItemsDisplayMode::Search(_) => {
3050 if is_singleton || query.is_some() || (should_add && is_expanded) {
3051 outline_panel.add_search_entries(
3052 &mut entries,
3053 &mut match_candidates,
3054 &mut added_contexts,
3055 entry.clone(),
3056 depth,
3057 query.clone(),
3058 is_singleton,
3059 cx,
3060 );
3061 }
3062 }
3063 ItemsDisplayMode::Outline => {
3064 let excerpts_to_consider =
3065 if is_singleton || query.is_some() || (should_add && is_expanded) {
3066 match &entry {
3067 FsEntry::File(_, _, buffer_id, entry_excerpts) => {
3068 Some((*buffer_id, entry_excerpts))
3069 }
3070 FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
3071 Some((*buffer_id, entry_excerpts))
3072 }
3073 _ => None,
3074 }
3075 } else {
3076 None
3077 };
3078 if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
3079 outline_panel.add_excerpt_entries(
3080 buffer_id,
3081 entry_excerpts,
3082 depth,
3083 track_matches,
3084 is_singleton,
3085 query.as_deref(),
3086 &mut entries,
3087 &mut match_candidates,
3088 &mut added_contexts,
3089 cx,
3090 );
3091 }
3092 }
3093 }
3094
3095 if is_singleton
3096 && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
3097 && !entries.iter().any(|item| {
3098 matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
3099 })
3100 {
3101 outline_panel.push_entry(
3102 &mut entries,
3103 &mut match_candidates,
3104 &mut added_contexts,
3105 track_matches,
3106 PanelEntry::Fs(entry.clone()),
3107 0,
3108 cx,
3109 );
3110 }
3111 }
3112
3113 if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
3114 let parent_expanded = parent_dirs
3115 .iter()
3116 .rev()
3117 .find(|parent| folded_dirs.iter().all(|entry| entry.path != parent.path))
3118 .map_or(true, |parent| parent.expanded);
3119 if parent_expanded || query.is_some() {
3120 outline_panel.push_entry(
3121 &mut entries,
3122 &mut match_candidates,
3123 &mut added_contexts,
3124 track_matches,
3125 PanelEntry::FoldedDirs(worktree_id, folded_dirs),
3126 folded_depth,
3127 cx,
3128 );
3129 }
3130 }
3131 }) else {
3132 return Vec::new();
3133 };
3134
3135 outline_panel
3136 .update(&mut cx, |outline_panel, _| {
3137 if matches!(outline_panel.mode, ItemsDisplayMode::Search(_)) {
3138 cleanup_fs_entries_without_search_children(
3139 &outline_panel.collapsed_entries,
3140 &mut entries,
3141 &mut match_candidates,
3142 &mut added_contexts,
3143 );
3144 }
3145 })
3146 .ok();
3147
3148 let Some(query) = query else {
3149 return entries;
3150 };
3151 let mut matched_ids = match_strings(
3152 &match_candidates,
3153 &query,
3154 true,
3155 usize::MAX,
3156 &AtomicBool::default(),
3157 cx.background_executor().clone(),
3158 )
3159 .await
3160 .into_iter()
3161 .map(|string_match| (string_match.candidate_id, string_match))
3162 .collect::<HashMap<_, _>>();
3163
3164 let mut id = 0;
3165 entries.retain_mut(|cached_entry| {
3166 let retain = match matched_ids.remove(&id) {
3167 Some(string_match) => {
3168 cached_entry.string_match = Some(string_match);
3169 true
3170 }
3171 None => false,
3172 };
3173 id += 1;
3174 retain
3175 });
3176
3177 entries
3178 })
3179 }
3180
3181 #[allow(clippy::too_many_arguments)]
3182 fn push_entry(
3183 &self,
3184 entries: &mut Vec<CachedEntry>,
3185 match_candidates: &mut Vec<StringMatchCandidate>,
3186 added_contexts: &mut HashSet<String>,
3187 track_matches: bool,
3188 entry: PanelEntry,
3189 depth: usize,
3190 cx: &mut WindowContext,
3191 ) {
3192 let entry = if let PanelEntry::FoldedDirs(worktree_id, entries) = &entry {
3193 match entries.len() {
3194 0 => {
3195 debug_panic!("Empty folded dirs receiver");
3196 return;
3197 }
3198 1 => PanelEntry::Fs(FsEntry::Directory(*worktree_id, entries[0].clone())),
3199 _ => entry,
3200 }
3201 } else {
3202 entry
3203 };
3204
3205 if track_matches {
3206 let id = entries.len();
3207 match &entry {
3208 PanelEntry::Fs(fs_entry) => {
3209 if let Some(file_name) =
3210 self.relative_path(fs_entry, cx).as_deref().map(file_name)
3211 {
3212 if added_contexts.insert(file_name.clone()) {
3213 match_candidates.push(StringMatchCandidate {
3214 id,
3215 string: file_name.to_string(),
3216 char_bag: file_name.chars().collect(),
3217 });
3218 }
3219 }
3220 }
3221 PanelEntry::FoldedDirs(worktree_id, entries) => {
3222 let dir_names = self.dir_names_string(entries, *worktree_id, cx);
3223 {
3224 if added_contexts.insert(dir_names.clone()) {
3225 match_candidates.push(StringMatchCandidate {
3226 id,
3227 string: dir_names.clone(),
3228 char_bag: dir_names.chars().collect(),
3229 });
3230 }
3231 }
3232 }
3233 PanelEntry::Outline(outline_entry) => match outline_entry {
3234 OutlineEntry::Outline(_, _, outline) => {
3235 if added_contexts.insert(outline.text.clone()) {
3236 match_candidates.push(StringMatchCandidate {
3237 id,
3238 string: outline.text.clone(),
3239 char_bag: outline.text.chars().collect(),
3240 });
3241 }
3242 }
3243 OutlineEntry::Excerpt(..) => {}
3244 },
3245 PanelEntry::Search(new_search_entry) => {
3246 if added_contexts.insert(new_search_entry.render_data.context_text.clone()) {
3247 match_candidates.push(StringMatchCandidate {
3248 id,
3249 char_bag: new_search_entry.render_data.context_text.chars().collect(),
3250 string: new_search_entry.render_data.context_text.clone(),
3251 });
3252 }
3253 }
3254 }
3255 }
3256 entries.push(CachedEntry {
3257 depth,
3258 entry,
3259 string_match: None,
3260 });
3261 }
3262
3263 fn dir_names_string(
3264 &self,
3265 entries: &[Entry],
3266 worktree_id: WorktreeId,
3267 cx: &AppContext,
3268 ) -> String {
3269 let dir_names_segment = entries
3270 .iter()
3271 .map(|entry| self.entry_name(&worktree_id, entry, cx))
3272 .collect::<PathBuf>();
3273 dir_names_segment.to_string_lossy().to_string()
3274 }
3275
3276 fn query(&self, cx: &AppContext) -> Option<String> {
3277 let query = self.filter_editor.read(cx).text(cx);
3278 if query.trim().is_empty() {
3279 None
3280 } else {
3281 Some(query)
3282 }
3283 }
3284
3285 fn is_expanded(&self, entry: &FsEntry) -> bool {
3286 let entry_to_check = match entry {
3287 FsEntry::ExternalFile(buffer_id, _) => CollapsedEntry::ExternalFile(*buffer_id),
3288 FsEntry::File(worktree_id, _, buffer_id, _) => {
3289 CollapsedEntry::File(*worktree_id, *buffer_id)
3290 }
3291 FsEntry::Directory(worktree_id, entry) => CollapsedEntry::Dir(*worktree_id, entry.id),
3292 };
3293 !self.collapsed_entries.contains(&entry_to_check)
3294 }
3295
3296 fn update_non_fs_items(&mut self, cx: &mut ViewContext<OutlinePanel>) {
3297 if !self.active {
3298 return;
3299 }
3300
3301 self.update_search_matches(cx);
3302 self.fetch_outdated_outlines(cx);
3303 self.autoscroll(cx);
3304 }
3305
3306 fn update_search_matches(&mut self, cx: &mut ViewContext<OutlinePanel>) {
3307 if !self.active {
3308 return;
3309 }
3310
3311 let project_search = self
3312 .active_item()
3313 .and_then(|item| item.downcast::<ProjectSearchView>());
3314 let project_search_matches = project_search
3315 .as_ref()
3316 .map(|project_search| project_search.read(cx).get_matches(cx))
3317 .unwrap_or_default();
3318
3319 let buffer_search = self
3320 .active_item()
3321 .as_deref()
3322 .and_then(|active_item| {
3323 self.workspace
3324 .upgrade()
3325 .and_then(|workspace| workspace.read(cx).pane_for(active_item))
3326 })
3327 .and_then(|pane| {
3328 pane.read(cx)
3329 .toolbar()
3330 .read(cx)
3331 .item_of_type::<BufferSearchBar>()
3332 });
3333 let buffer_search_matches = self
3334 .active_editor()
3335 .map(|active_editor| active_editor.update(cx, |editor, cx| editor.get_matches(cx)))
3336 .unwrap_or_default();
3337
3338 let mut update_cached_entries = false;
3339 if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
3340 if matches!(self.mode, ItemsDisplayMode::Search(_)) {
3341 self.mode = ItemsDisplayMode::Outline;
3342 update_cached_entries = true;
3343 }
3344 } else {
3345 let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
3346 (
3347 SearchKind::Project,
3348 project_search_matches,
3349 project_search
3350 .map(|project_search| project_search.read(cx).search_query_text(cx))
3351 .unwrap_or_default(),
3352 )
3353 } else {
3354 (
3355 SearchKind::Buffer,
3356 buffer_search_matches,
3357 buffer_search
3358 .map(|buffer_search| buffer_search.read(cx).query(cx))
3359 .unwrap_or_default(),
3360 )
3361 };
3362
3363 update_cached_entries = match &self.mode {
3364 ItemsDisplayMode::Search(current_search_state) => {
3365 current_search_state.query != new_search_query
3366 || current_search_state.kind != kind
3367 || current_search_state.matches.is_empty()
3368 || current_search_state.matches.iter().enumerate().any(
3369 |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
3370 )
3371 }
3372 ItemsDisplayMode::Outline => true,
3373 };
3374 self.mode = ItemsDisplayMode::Search(SearchState::new(
3375 kind,
3376 new_search_query,
3377 new_search_matches,
3378 cx.theme().syntax().clone(),
3379 cx,
3380 ));
3381 }
3382 if update_cached_entries {
3383 self.selected_entry.invalidate();
3384 self.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
3385 }
3386 }
3387
3388 #[allow(clippy::too_many_arguments)]
3389 fn add_excerpt_entries(
3390 &self,
3391 buffer_id: BufferId,
3392 entries_to_add: &[ExcerptId],
3393 parent_depth: usize,
3394 track_matches: bool,
3395 is_singleton: bool,
3396 query: Option<&str>,
3397 entries: &mut Vec<CachedEntry>,
3398 match_candidates: &mut Vec<StringMatchCandidate>,
3399 added_contexts: &mut HashSet<String>,
3400 cx: &mut ViewContext<Self>,
3401 ) {
3402 if let Some(excerpts) = self.excerpts.get(&buffer_id) {
3403 for &excerpt_id in entries_to_add {
3404 let Some(excerpt) = excerpts.get(&excerpt_id) else {
3405 continue;
3406 };
3407 let excerpt_depth = parent_depth + 1;
3408 self.push_entry(
3409 entries,
3410 match_candidates,
3411 added_contexts,
3412 track_matches,
3413 PanelEntry::Outline(OutlineEntry::Excerpt(
3414 buffer_id,
3415 excerpt_id,
3416 excerpt.range.clone(),
3417 )),
3418 excerpt_depth,
3419 cx,
3420 );
3421
3422 let mut outline_base_depth = excerpt_depth + 1;
3423 if is_singleton {
3424 outline_base_depth = 0;
3425 entries.clear();
3426 match_candidates.clear();
3427 } else if query.is_none()
3428 && self
3429 .collapsed_entries
3430 .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
3431 {
3432 continue;
3433 }
3434
3435 for outline in excerpt.iter_outlines() {
3436 self.push_entry(
3437 entries,
3438 match_candidates,
3439 added_contexts,
3440 track_matches,
3441 PanelEntry::Outline(OutlineEntry::Outline(
3442 buffer_id,
3443 excerpt_id,
3444 outline.clone(),
3445 )),
3446 outline_base_depth + outline.depth,
3447 cx,
3448 );
3449 }
3450 }
3451 }
3452 }
3453
3454 #[allow(clippy::too_many_arguments)]
3455 fn add_search_entries(
3456 &mut self,
3457 entries: &mut Vec<CachedEntry>,
3458 match_candidates: &mut Vec<StringMatchCandidate>,
3459 added_contexts: &mut HashSet<String>,
3460 parent_entry: FsEntry,
3461 parent_depth: usize,
3462 filter_query: Option<String>,
3463 is_singleton: bool,
3464 cx: &mut ViewContext<Self>,
3465 ) {
3466 let Some(active_editor) = self.active_editor() else {
3467 return;
3468 };
3469 let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
3470 return;
3471 };
3472
3473 let kind = search_state.kind;
3474 let related_excerpts = match &parent_entry {
3475 FsEntry::Directory(_, _) => return,
3476 FsEntry::ExternalFile(_, excerpts) => excerpts,
3477 FsEntry::File(_, _, _, excerpts) => excerpts,
3478 }
3479 .iter()
3480 .copied()
3481 .collect::<HashSet<_>>();
3482
3483 let depth = if is_singleton { 0 } else { parent_depth + 1 };
3484 let multi_buffer_snapshot = active_editor.read(cx).buffer().read(cx).snapshot(cx);
3485 let new_search_matches = search_state.matches.iter().filter(|(match_range, _)| {
3486 related_excerpts.contains(&match_range.start.excerpt_id)
3487 || related_excerpts.contains(&match_range.end.excerpt_id)
3488 });
3489
3490 let previous_search_matches = entries
3491 .iter()
3492 .skip_while(|entry| {
3493 if let PanelEntry::Fs(entry) = &entry.entry {
3494 entry == &parent_entry
3495 } else {
3496 true
3497 }
3498 })
3499 .take_while(|entry| matches!(entry.entry, PanelEntry::Search(_)))
3500 .fold(
3501 HashMap::default(),
3502 |mut previous_matches, previous_entry| match &previous_entry.entry {
3503 PanelEntry::Search(search_entry) => {
3504 previous_matches.insert(
3505 (search_entry.kind, &search_entry.match_range),
3506 &search_entry.render_data,
3507 );
3508 previous_matches
3509 }
3510 _ => previous_matches,
3511 },
3512 );
3513
3514 let new_search_entries = new_search_matches
3515 .map(|(match_range, search_data)| {
3516 let previous_search_data =
3517 previous_search_matches.get(&(kind, match_range)).copied();
3518 let render_data = search_data
3519 .get()
3520 .or(previous_search_data)
3521 .unwrap_or_else(|| {
3522 search_data.get_or_init(|| {
3523 Arc::new(SearchData::new(match_range, &multi_buffer_snapshot))
3524 })
3525 });
3526 if let (Some(previous_highlights), None) = (
3527 previous_search_data.and_then(|data| data.highlights_data.get()),
3528 render_data.highlights_data.get(),
3529 ) {
3530 render_data
3531 .highlights_data
3532 .set(previous_highlights.clone())
3533 .ok();
3534 }
3535
3536 SearchEntry {
3537 match_range: match_range.clone(),
3538 kind,
3539 render_data: Arc::clone(render_data),
3540 }
3541 })
3542 .collect::<Vec<_>>();
3543 for new_search_entry in new_search_entries {
3544 self.push_entry(
3545 entries,
3546 match_candidates,
3547 added_contexts,
3548 filter_query.is_some(),
3549 PanelEntry::Search(new_search_entry),
3550 depth,
3551 cx,
3552 );
3553 }
3554 }
3555
3556 fn active_editor(&self) -> Option<View<Editor>> {
3557 self.active_item.as_ref()?.active_editor.upgrade()
3558 }
3559
3560 fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
3561 self.active_item.as_ref()?.item_handle.upgrade()
3562 }
3563
3564 fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
3565 self.active_item().map_or(true, |active_item| {
3566 !self.pinned && active_item.item_id() != new_active_item.item_id()
3567 })
3568 }
3569
3570 pub fn toggle_active_editor_pin(
3571 &mut self,
3572 _: &ToggleActiveEditorPin,
3573 cx: &mut ViewContext<Self>,
3574 ) {
3575 self.pinned = !self.pinned;
3576 if !self.pinned {
3577 if let Some((active_item, active_editor)) = self
3578 .workspace
3579 .upgrade()
3580 .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
3581 {
3582 if self.should_replace_active_item(active_item.as_ref()) {
3583 self.replace_active_editor(active_item, active_editor, cx);
3584 }
3585 }
3586 }
3587
3588 cx.notify();
3589 }
3590
3591 fn selected_entry(&self) -> Option<&PanelEntry> {
3592 match &self.selected_entry {
3593 SelectedEntry::Invalidated(entry) => entry.as_ref(),
3594 SelectedEntry::Valid(entry) => Some(entry),
3595 SelectedEntry::None => None,
3596 }
3597 }
3598
3599 fn select_entry(&mut self, entry: PanelEntry, focus: bool, cx: &mut ViewContext<Self>) {
3600 if focus {
3601 self.focus_handle.focus(cx);
3602 }
3603 self.selected_entry = SelectedEntry::Valid(entry);
3604 self.autoscroll(cx);
3605 cx.notify();
3606 }
3607}
3608
3609fn cleanup_fs_entries_without_search_children(
3610 collapsed_entries: &HashSet<CollapsedEntry>,
3611 entries: &mut Vec<CachedEntry>,
3612 string_match_candidates: &mut Vec<StringMatchCandidate>,
3613 added_contexts: &mut HashSet<String>,
3614) {
3615 let mut match_ids_to_remove = BTreeSet::new();
3616 let mut previous_entry = None::<&PanelEntry>;
3617 for (id, entry) in entries.iter().enumerate().rev() {
3618 let has_search_items = match (previous_entry, &entry.entry) {
3619 (Some(PanelEntry::Outline(_)), _) => unreachable!(),
3620 (_, PanelEntry::Outline(_)) => false,
3621 (_, PanelEntry::Search(_)) => true,
3622 (None, PanelEntry::FoldedDirs(_, _) | PanelEntry::Fs(_)) => false,
3623 (
3624 Some(PanelEntry::Search(_)),
3625 PanelEntry::FoldedDirs(_, _) | PanelEntry::Fs(FsEntry::Directory(..)),
3626 ) => false,
3627 (Some(PanelEntry::FoldedDirs(..)), PanelEntry::FoldedDirs(..)) => true,
3628 (
3629 Some(PanelEntry::Search(_)),
3630 PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..)),
3631 ) => true,
3632 (
3633 Some(PanelEntry::Fs(previous_fs)),
3634 PanelEntry::FoldedDirs(folded_worktree, folded_dirs),
3635 ) => {
3636 let expected_parent = folded_dirs.last().map(|dir_entry| dir_entry.path.as_ref());
3637 match previous_fs {
3638 FsEntry::ExternalFile(..) => false,
3639 FsEntry::File(file_worktree, file_entry, ..) => {
3640 file_worktree == folded_worktree
3641 && file_entry.path.parent() == expected_parent
3642 }
3643 FsEntry::Directory(directory_wortree, directory_entry) => {
3644 directory_wortree == folded_worktree
3645 && directory_entry.path.parent() == expected_parent
3646 }
3647 }
3648 }
3649 (
3650 Some(PanelEntry::FoldedDirs(folded_worktree, folded_dirs)),
3651 PanelEntry::Fs(fs_entry),
3652 ) => match fs_entry {
3653 FsEntry::File(..) | FsEntry::ExternalFile(..) => false,
3654 FsEntry::Directory(directory_wortree, maybe_parent_directory) => {
3655 directory_wortree == folded_worktree
3656 && Some(maybe_parent_directory.path.as_ref())
3657 == folded_dirs
3658 .first()
3659 .and_then(|dir_entry| dir_entry.path.parent())
3660 }
3661 },
3662 (Some(PanelEntry::Fs(previous_entry)), PanelEntry::Fs(maybe_parent_entry)) => {
3663 match (previous_entry, maybe_parent_entry) {
3664 (FsEntry::ExternalFile(..), _) | (_, FsEntry::ExternalFile(..)) => false,
3665 (FsEntry::Directory(..) | FsEntry::File(..), FsEntry::File(..)) => false,
3666 (
3667 FsEntry::Directory(previous_worktree, previous_directory),
3668 FsEntry::Directory(new_worktree, maybe_parent_directory),
3669 ) => {
3670 previous_worktree == new_worktree
3671 && previous_directory.path.parent()
3672 == Some(maybe_parent_directory.path.as_ref())
3673 }
3674 (
3675 FsEntry::File(previous_worktree, previous_file, ..),
3676 FsEntry::Directory(new_worktree, maybe_parent_directory),
3677 ) => {
3678 previous_worktree == new_worktree
3679 && previous_file.path.parent()
3680 == Some(maybe_parent_directory.path.as_ref())
3681 }
3682 }
3683 }
3684 };
3685
3686 if has_search_items {
3687 previous_entry = Some(&entry.entry);
3688 } else {
3689 let collapsed_entries_to_check = match &entry.entry {
3690 PanelEntry::FoldedDirs(worktree_id, entries) => entries
3691 .iter()
3692 .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id))
3693 .collect(),
3694 PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => {
3695 vec![CollapsedEntry::Dir(*worktree_id, entry.id)]
3696 }
3697 PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
3698 vec![CollapsedEntry::ExternalFile(*buffer_id)]
3699 }
3700 PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
3701 vec![CollapsedEntry::File(*worktree_id, *buffer_id)]
3702 }
3703 PanelEntry::Search(_) | PanelEntry::Outline(_) => Vec::new(),
3704 };
3705 if !collapsed_entries_to_check.is_empty()
3706 && collapsed_entries_to_check
3707 .iter()
3708 .any(|collapsed_entry| collapsed_entries.contains(collapsed_entry))
3709 {
3710 previous_entry = Some(&entry.entry);
3711 continue;
3712 }
3713 match_ids_to_remove.insert(id);
3714 previous_entry = None;
3715 }
3716 }
3717
3718 if match_ids_to_remove.is_empty() {
3719 return;
3720 }
3721
3722 string_match_candidates.retain(|candidate| {
3723 let retain = !match_ids_to_remove.contains(&candidate.id);
3724 if !retain {
3725 added_contexts.remove(&candidate.string);
3726 }
3727 retain
3728 });
3729 match_ids_to_remove.into_iter().rev().for_each(|id| {
3730 entries.remove(id);
3731 });
3732}
3733
3734fn workspace_active_editor(
3735 workspace: &Workspace,
3736 cx: &AppContext,
3737) -> Option<(Box<dyn ItemHandle>, View<Editor>)> {
3738 let active_item = workspace.active_item(cx)?;
3739 let active_editor = active_item
3740 .act_as::<Editor>(cx)
3741 .filter(|editor| editor.read(cx).mode() == EditorMode::Full)?;
3742 Some((active_item, active_editor))
3743}
3744
3745fn back_to_common_visited_parent(
3746 visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
3747 worktree_id: &WorktreeId,
3748 new_entry: &Entry,
3749) -> Option<(WorktreeId, ProjectEntryId)> {
3750 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
3751 match new_entry.path.parent() {
3752 Some(parent_path) => {
3753 if parent_path == visited_path.as_ref() {
3754 return Some((*worktree_id, *visited_dir_id));
3755 }
3756 }
3757 None => {
3758 break;
3759 }
3760 }
3761 visited_dirs.pop();
3762 }
3763 None
3764}
3765
3766fn file_name(path: &Path) -> String {
3767 let mut current_path = path;
3768 loop {
3769 if let Some(file_name) = current_path.file_name() {
3770 return file_name.to_string_lossy().into_owned();
3771 }
3772 match current_path.parent() {
3773 Some(parent) => current_path = parent,
3774 None => return path.to_string_lossy().into_owned(),
3775 }
3776 }
3777}
3778
3779impl Panel for OutlinePanel {
3780 fn persistent_name() -> &'static str {
3781 "Outline Panel"
3782 }
3783
3784 fn position(&self, cx: &WindowContext) -> DockPosition {
3785 match OutlinePanelSettings::get_global(cx).dock {
3786 OutlinePanelDockPosition::Left => DockPosition::Left,
3787 OutlinePanelDockPosition::Right => DockPosition::Right,
3788 }
3789 }
3790
3791 fn position_is_valid(&self, position: DockPosition) -> bool {
3792 matches!(position, DockPosition::Left | DockPosition::Right)
3793 }
3794
3795 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3796 settings::update_settings_file::<OutlinePanelSettings>(
3797 self.fs.clone(),
3798 cx,
3799 move |settings, _| {
3800 let dock = match position {
3801 DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
3802 DockPosition::Right => OutlinePanelDockPosition::Right,
3803 };
3804 settings.dock = Some(dock);
3805 },
3806 );
3807 }
3808
3809 fn size(&self, cx: &WindowContext) -> Pixels {
3810 self.width
3811 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
3812 }
3813
3814 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
3815 self.width = size;
3816 self.serialize(cx);
3817 cx.notify();
3818 }
3819
3820 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
3821 OutlinePanelSettings::get_global(cx)
3822 .button
3823 .then_some(IconName::ListTree)
3824 }
3825
3826 fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
3827 Some("Outline Panel")
3828 }
3829
3830 fn toggle_action(&self) -> Box<dyn Action> {
3831 Box::new(ToggleFocus)
3832 }
3833
3834 fn starts_open(&self, _: &WindowContext) -> bool {
3835 self.active
3836 }
3837
3838 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3839 cx.spawn(|outline_panel, mut cx| async move {
3840 outline_panel
3841 .update(&mut cx, |outline_panel, cx| {
3842 let old_active = outline_panel.active;
3843 outline_panel.active = active;
3844 if active && old_active != active {
3845 if let Some((active_item, active_editor)) = outline_panel
3846 .workspace
3847 .upgrade()
3848 .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
3849 {
3850 if outline_panel.should_replace_active_item(active_item.as_ref()) {
3851 outline_panel.replace_active_editor(active_item, active_editor, cx);
3852 } else {
3853 outline_panel.update_fs_entries(
3854 &active_editor,
3855 HashSet::default(),
3856 None,
3857 cx,
3858 )
3859 }
3860 } else if !outline_panel.pinned {
3861 outline_panel.clear_previous(cx);
3862 }
3863 }
3864 outline_panel.serialize(cx);
3865 })
3866 .ok();
3867 })
3868 .detach()
3869 }
3870}
3871
3872impl FocusableView for OutlinePanel {
3873 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3874 self.filter_editor.focus_handle(cx).clone()
3875 }
3876}
3877
3878impl EventEmitter<Event> for OutlinePanel {}
3879
3880impl EventEmitter<PanelEvent> for OutlinePanel {}
3881
3882impl Render for OutlinePanel {
3883 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3884 let project = self.project.read(cx);
3885 let query = self.query(cx);
3886 let pinned = self.pinned;
3887
3888 let outline_panel = v_flex()
3889 .id("outline-panel")
3890 .size_full()
3891 .relative()
3892 .key_context(self.dispatch_context(cx))
3893 .on_action(cx.listener(Self::open))
3894 .on_action(cx.listener(Self::cancel))
3895 .on_action(cx.listener(Self::select_next))
3896 .on_action(cx.listener(Self::select_prev))
3897 .on_action(cx.listener(Self::select_first))
3898 .on_action(cx.listener(Self::select_last))
3899 .on_action(cx.listener(Self::select_parent))
3900 .on_action(cx.listener(Self::expand_selected_entry))
3901 .on_action(cx.listener(Self::collapse_selected_entry))
3902 .on_action(cx.listener(Self::expand_all_entries))
3903 .on_action(cx.listener(Self::collapse_all_entries))
3904 .on_action(cx.listener(Self::copy_path))
3905 .on_action(cx.listener(Self::copy_relative_path))
3906 .on_action(cx.listener(Self::toggle_active_editor_pin))
3907 .on_action(cx.listener(Self::unfold_directory))
3908 .on_action(cx.listener(Self::fold_directory))
3909 .when(project.is_local(), |el| {
3910 el.on_action(cx.listener(Self::reveal_in_finder))
3911 })
3912 .when(project.is_local() || project.is_via_ssh(), |el| {
3913 el.on_action(cx.listener(Self::open_in_terminal))
3914 })
3915 .on_mouse_down(
3916 MouseButton::Right,
3917 cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
3918 if let Some(entry) = outline_panel.selected_entry().cloned() {
3919 outline_panel.deploy_context_menu(event.position, entry, cx)
3920 } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
3921 outline_panel.deploy_context_menu(event.position, PanelEntry::Fs(entry), cx)
3922 }
3923 }),
3924 )
3925 .track_focus(&self.focus_handle);
3926
3927 if self.cached_entries.is_empty() {
3928 let header = if self.updating_fs_entries {
3929 "Loading outlines"
3930 } else if query.is_some() {
3931 "No matches for query"
3932 } else {
3933 "No outlines available"
3934 };
3935
3936 outline_panel.child(
3937 v_flex()
3938 .justify_center()
3939 .size_full()
3940 .child(h_flex().justify_center().child(Label::new(header)))
3941 .when_some(query.clone(), |panel, query| {
3942 panel.child(h_flex().justify_center().child(Label::new(query)))
3943 })
3944 .child(
3945 h_flex()
3946 .pt(Spacing::Small.rems(cx))
3947 .justify_center()
3948 .child({
3949 let keystroke = match self.position(cx) {
3950 DockPosition::Left => {
3951 cx.keystroke_text_for(&workspace::ToggleLeftDock)
3952 }
3953 DockPosition::Bottom => {
3954 cx.keystroke_text_for(&workspace::ToggleBottomDock)
3955 }
3956 DockPosition::Right => {
3957 cx.keystroke_text_for(&workspace::ToggleRightDock)
3958 }
3959 };
3960 Label::new(format!("Toggle this panel with {keystroke}"))
3961 }),
3962 ),
3963 )
3964 } else {
3965 let search_query = match &self.mode {
3966 ItemsDisplayMode::Search(search_query) => Some(search_query),
3967 _ => None,
3968 };
3969 outline_panel
3970 .when_some(search_query, |outline_panel, search_state| {
3971 outline_panel.child(
3972 div()
3973 .mx_2()
3974 .child(
3975 Label::new(format!("Searching: '{}'", search_state.query))
3976 .color(Color::Muted),
3977 )
3978 .child(horizontal_separator(cx)),
3979 )
3980 })
3981 .child({
3982 let items_len = self.cached_entries.len();
3983 let multi_buffer_snapshot = self
3984 .active_editor()
3985 .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
3986 uniform_list(cx.view().clone(), "entries", items_len, {
3987 move |outline_panel, range, cx| {
3988 let entries = outline_panel.cached_entries.get(range);
3989 entries
3990 .map(|entries| entries.to_vec())
3991 .unwrap_or_default()
3992 .into_iter()
3993 .filter_map(|cached_entry| match cached_entry.entry {
3994 PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
3995 &entry,
3996 cached_entry.depth,
3997 cached_entry.string_match.as_ref(),
3998 cx,
3999 )),
4000 PanelEntry::FoldedDirs(worktree_id, entries) => {
4001 Some(outline_panel.render_folded_dirs(
4002 worktree_id,
4003 &entries,
4004 cached_entry.depth,
4005 cached_entry.string_match.as_ref(),
4006 cx,
4007 ))
4008 }
4009 PanelEntry::Outline(OutlineEntry::Excerpt(
4010 buffer_id,
4011 excerpt_id,
4012 excerpt,
4013 )) => outline_panel.render_excerpt(
4014 buffer_id,
4015 excerpt_id,
4016 &excerpt,
4017 cached_entry.depth,
4018 cx,
4019 ),
4020 PanelEntry::Outline(OutlineEntry::Outline(
4021 buffer_id,
4022 excerpt_id,
4023 outline,
4024 )) => Some(outline_panel.render_outline(
4025 buffer_id,
4026 excerpt_id,
4027 &outline,
4028 cached_entry.depth,
4029 cached_entry.string_match.as_ref(),
4030 cx,
4031 )),
4032 PanelEntry::Search(SearchEntry {
4033 match_range,
4034 render_data,
4035 kind,
4036 ..
4037 }) => Some(outline_panel.render_search_match(
4038 multi_buffer_snapshot.as_ref(),
4039 &match_range,
4040 &render_data,
4041 kind,
4042 cached_entry.depth,
4043 cached_entry.string_match.as_ref(),
4044 cx,
4045 )),
4046 })
4047 .collect()
4048 }
4049 })
4050 .size_full()
4051 .track_scroll(self.scroll_handle.clone())
4052 })
4053 }
4054 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4055 deferred(
4056 anchored()
4057 .position(*position)
4058 .anchor(gpui::AnchorCorner::TopLeft)
4059 .child(menu.clone()),
4060 )
4061 .with_priority(1)
4062 }))
4063 .child(
4064 v_flex().child(horizontal_separator(cx)).child(
4065 h_flex().p_2().child(self.filter_editor.clone()).child(
4066 div().child(
4067 IconButton::new(
4068 "outline-panel-menu",
4069 if pinned {
4070 IconName::Unpin
4071 } else {
4072 IconName::Pin
4073 },
4074 )
4075 .tooltip(move |cx| {
4076 Tooltip::text(
4077 if pinned {
4078 "Unpin Outline"
4079 } else {
4080 "Pin Active Outline"
4081 },
4082 cx,
4083 )
4084 })
4085 .shape(IconButtonShape::Square)
4086 .on_click(cx.listener(|outline_panel, _, cx| {
4087 outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, cx);
4088 })),
4089 ),
4090 ),
4091 ),
4092 )
4093 }
4094}
4095
4096fn subscribe_for_editor_events(
4097 editor: &View<Editor>,
4098 cx: &mut ViewContext<OutlinePanel>,
4099) -> Subscription {
4100 let debounce = Some(UPDATE_DEBOUNCE);
4101 cx.subscribe(
4102 editor,
4103 move |outline_panel, editor, e: &EditorEvent, cx| match e {
4104 EditorEvent::SelectionsChanged { local: true } => {
4105 outline_panel.reveal_entry_for_selection(&editor, cx);
4106 cx.notify();
4107 }
4108 EditorEvent::ExcerptsAdded { excerpts, .. } => {
4109 outline_panel.update_fs_entries(
4110 &editor,
4111 excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
4112 debounce,
4113 cx,
4114 );
4115 }
4116 EditorEvent::ExcerptsRemoved { ids } => {
4117 let mut ids = ids.iter().collect::<HashSet<_>>();
4118 for excerpts in outline_panel.excerpts.values_mut() {
4119 excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
4120 if ids.is_empty() {
4121 break;
4122 }
4123 }
4124 outline_panel.update_fs_entries(&editor, HashSet::default(), debounce, cx);
4125 }
4126 EditorEvent::ExcerptsExpanded { ids } => {
4127 outline_panel.invalidate_outlines(ids);
4128 outline_panel.update_non_fs_items(cx);
4129 }
4130 EditorEvent::ExcerptsEdited { ids } => {
4131 outline_panel.invalidate_outlines(ids);
4132 outline_panel.update_non_fs_items(cx);
4133 }
4134 EditorEvent::Reparsed(buffer_id) => {
4135 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
4136 for (_, excerpt) in excerpts {
4137 excerpt.invalidate_outlines();
4138 }
4139 }
4140 outline_panel.update_non_fs_items(cx);
4141 }
4142 _ => {}
4143 },
4144 )
4145}
4146
4147fn empty_icon() -> AnyElement {
4148 h_flex()
4149 .size(IconSize::default().rems())
4150 .invisible()
4151 .flex_none()
4152 .into_any_element()
4153}
4154
4155fn horizontal_separator(cx: &mut WindowContext) -> Div {
4156 div().mx_2().border_primary(cx).border_t_1()
4157}
4158
4159#[cfg(test)]
4160mod tests {
4161 use gpui::{TestAppContext, VisualTestContext, WindowHandle};
4162 use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
4163 use pretty_assertions::assert_eq;
4164 use project::FakeFs;
4165 use search::project_search::{self, perform_project_search};
4166 use serde_json::json;
4167
4168 use super::*;
4169
4170 const SELECTED_MARKER: &str = " <==== selected";
4171
4172 #[gpui::test]
4173 async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
4174 init_test(cx);
4175
4176 let fs = FakeFs::new(cx.background_executor.clone());
4177 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
4178 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
4179 project.read_with(cx, |project, _| {
4180 project.languages().add(Arc::new(rust_lang()))
4181 });
4182 let workspace = add_outline_panel(&project, cx).await;
4183 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4184 let outline_panel = outline_panel(&workspace, cx);
4185 outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
4186
4187 workspace
4188 .update(cx, |workspace, cx| {
4189 ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
4190 })
4191 .unwrap();
4192 let search_view = workspace
4193 .update(cx, |workspace, cx| {
4194 workspace
4195 .active_pane()
4196 .read(cx)
4197 .items()
4198 .find_map(|item| item.downcast::<ProjectSearchView>())
4199 .expect("Project search view expected to appear after new search event trigger")
4200 })
4201 .unwrap();
4202
4203 let query = "param_names_for_lifetime_elision_hints";
4204 perform_project_search(&search_view, query, cx);
4205 search_view.update(cx, |search_view, cx| {
4206 search_view
4207 .results_editor()
4208 .update(cx, |results_editor, cx| {
4209 assert_eq!(
4210 results_editor.display_text(cx).match_indices(query).count(),
4211 9
4212 );
4213 });
4214 });
4215
4216 let all_matches = r#"/
4217 crates/
4218 ide/src/
4219 inlay_hints/
4220 fn_lifetime_fn.rs
4221 search: match config.param_names_for_lifetime_elision_hints {
4222 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
4223 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
4224 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
4225 inlay_hints.rs
4226 search: pub param_names_for_lifetime_elision_hints: bool,
4227 search: param_names_for_lifetime_elision_hints: self
4228 static_index.rs
4229 search: param_names_for_lifetime_elision_hints: false,
4230 rust-analyzer/src/
4231 cli/
4232 analysis_stats.rs
4233 search: param_names_for_lifetime_elision_hints: true,
4234 config.rs
4235 search: param_names_for_lifetime_elision_hints: self"#;
4236 let select_first_in_all_matches = |line_to_select: &str| {
4237 assert!(all_matches.contains(line_to_select));
4238 all_matches.replacen(
4239 line_to_select,
4240 &format!("{line_to_select}{SELECTED_MARKER}"),
4241 1,
4242 )
4243 };
4244
4245 cx.executor()
4246 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4247 cx.run_until_parked();
4248 outline_panel.update(cx, |outline_panel, _| {
4249 assert_eq!(
4250 display_entries(
4251 &outline_panel.cached_entries,
4252 outline_panel.selected_entry()
4253 ),
4254 select_first_in_all_matches(
4255 "search: match config.param_names_for_lifetime_elision_hints {"
4256 )
4257 );
4258 });
4259
4260 outline_panel.update(cx, |outline_panel, cx| {
4261 outline_panel.select_parent(&SelectParent, cx);
4262 assert_eq!(
4263 display_entries(
4264 &outline_panel.cached_entries,
4265 outline_panel.selected_entry()
4266 ),
4267 select_first_in_all_matches("fn_lifetime_fn.rs")
4268 );
4269 });
4270 outline_panel.update(cx, |outline_panel, cx| {
4271 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
4272 });
4273 cx.run_until_parked();
4274 outline_panel.update(cx, |outline_panel, _| {
4275 assert_eq!(
4276 display_entries(
4277 &outline_panel.cached_entries,
4278 outline_panel.selected_entry()
4279 ),
4280 format!(
4281 r#"/
4282 crates/
4283 ide/src/
4284 inlay_hints/
4285 fn_lifetime_fn.rs{SELECTED_MARKER}
4286 inlay_hints.rs
4287 search: pub param_names_for_lifetime_elision_hints: bool,
4288 search: param_names_for_lifetime_elision_hints: self
4289 static_index.rs
4290 search: param_names_for_lifetime_elision_hints: false,
4291 rust-analyzer/src/
4292 cli/
4293 analysis_stats.rs
4294 search: param_names_for_lifetime_elision_hints: true,
4295 config.rs
4296 search: param_names_for_lifetime_elision_hints: self"#,
4297 )
4298 );
4299 });
4300
4301 outline_panel.update(cx, |outline_panel, cx| {
4302 outline_panel.expand_all_entries(&ExpandAllEntries, cx);
4303 });
4304 cx.run_until_parked();
4305 outline_panel.update(cx, |outline_panel, cx| {
4306 outline_panel.select_parent(&SelectParent, cx);
4307 assert_eq!(
4308 display_entries(
4309 &outline_panel.cached_entries,
4310 outline_panel.selected_entry()
4311 ),
4312 select_first_in_all_matches("inlay_hints/")
4313 );
4314 });
4315
4316 outline_panel.update(cx, |outline_panel, cx| {
4317 outline_panel.select_parent(&SelectParent, cx);
4318 assert_eq!(
4319 display_entries(
4320 &outline_panel.cached_entries,
4321 outline_panel.selected_entry()
4322 ),
4323 select_first_in_all_matches("ide/src/")
4324 );
4325 });
4326
4327 outline_panel.update(cx, |outline_panel, cx| {
4328 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
4329 });
4330 cx.run_until_parked();
4331 outline_panel.update(cx, |outline_panel, _| {
4332 assert_eq!(
4333 display_entries(
4334 &outline_panel.cached_entries,
4335 outline_panel.selected_entry()
4336 ),
4337 format!(
4338 r#"/
4339 crates/
4340 ide/src/{SELECTED_MARKER}
4341 rust-analyzer/src/
4342 cli/
4343 analysis_stats.rs
4344 search: param_names_for_lifetime_elision_hints: true,
4345 config.rs
4346 search: param_names_for_lifetime_elision_hints: self"#,
4347 )
4348 );
4349 });
4350 outline_panel.update(cx, |outline_panel, cx| {
4351 outline_panel.expand_selected_entry(&ExpandSelectedEntry, cx);
4352 });
4353 cx.run_until_parked();
4354 outline_panel.update(cx, |outline_panel, _| {
4355 assert_eq!(
4356 display_entries(
4357 &outline_panel.cached_entries,
4358 outline_panel.selected_entry()
4359 ),
4360 select_first_in_all_matches("ide/src/")
4361 );
4362 });
4363 }
4364
4365 #[gpui::test]
4366 async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
4367 init_test(cx);
4368
4369 let root = "/frontend-project";
4370 let fs = FakeFs::new(cx.background_executor.clone());
4371 fs.insert_tree(
4372 root,
4373 json!({
4374 "public": {
4375 "lottie": {
4376 "syntax-tree.json": r#"{ "something": "static" }"#
4377 }
4378 },
4379 "src": {
4380 "app": {
4381 "(site)": {
4382 "(about)": {
4383 "jobs": {
4384 "[slug]": {
4385 "page.tsx": r#"static"#
4386 }
4387 }
4388 },
4389 "(blog)": {
4390 "post": {
4391 "[slug]": {
4392 "page.tsx": r#"static"#
4393 }
4394 }
4395 },
4396 }
4397 },
4398 "components": {
4399 "ErrorBoundary.tsx": r#"static"#,
4400 }
4401 }
4402
4403 }),
4404 )
4405 .await;
4406 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
4407 let workspace = add_outline_panel(&project, cx).await;
4408 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4409 let outline_panel = outline_panel(&workspace, cx);
4410 outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
4411
4412 workspace
4413 .update(cx, |workspace, cx| {
4414 ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
4415 })
4416 .unwrap();
4417 let search_view = workspace
4418 .update(cx, |workspace, cx| {
4419 workspace
4420 .active_pane()
4421 .read(cx)
4422 .items()
4423 .find_map(|item| item.downcast::<ProjectSearchView>())
4424 .expect("Project search view expected to appear after new search event trigger")
4425 })
4426 .unwrap();
4427
4428 let query = "static";
4429 perform_project_search(&search_view, query, cx);
4430 search_view.update(cx, |search_view, cx| {
4431 search_view
4432 .results_editor()
4433 .update(cx, |results_editor, cx| {
4434 assert_eq!(
4435 results_editor.display_text(cx).match_indices(query).count(),
4436 4
4437 );
4438 });
4439 });
4440
4441 cx.executor()
4442 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4443 cx.run_until_parked();
4444 outline_panel.update(cx, |outline_panel, _| {
4445 assert_eq!(
4446 display_entries(
4447 &outline_panel.cached_entries,
4448 outline_panel.selected_entry()
4449 ),
4450 r#"/
4451 public/lottie/
4452 syntax-tree.json
4453 search: { "something": "static" } <==== selected
4454 src/
4455 app/(site)/
4456 (about)/jobs/[slug]/
4457 page.tsx
4458 search: static
4459 (blog)/post/[slug]/
4460 page.tsx
4461 search: static
4462 components/
4463 ErrorBoundary.tsx
4464 search: static"#
4465 );
4466 });
4467
4468 outline_panel.update(cx, |outline_panel, cx| {
4469 outline_panel.select_next(&SelectNext, cx);
4470 outline_panel.select_next(&SelectNext, cx);
4471 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
4472 });
4473 cx.run_until_parked();
4474 outline_panel.update(cx, |outline_panel, _| {
4475 assert_eq!(
4476 display_entries(
4477 &outline_panel.cached_entries,
4478 outline_panel.selected_entry()
4479 ),
4480 r#"/
4481 public/lottie/
4482 syntax-tree.json
4483 search: { "something": "static" }
4484 src/
4485 app/(site)/ <==== selected
4486 components/
4487 ErrorBoundary.tsx
4488 search: static"#
4489 );
4490 });
4491 }
4492
4493 async fn add_outline_panel(
4494 project: &Model<Project>,
4495 cx: &mut TestAppContext,
4496 ) -> WindowHandle<Workspace> {
4497 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4498
4499 let outline_panel = window
4500 .update(cx, |_, cx| cx.spawn(OutlinePanel::load))
4501 .unwrap()
4502 .await
4503 .expect("Failed to load outline panel");
4504
4505 window
4506 .update(cx, |workspace, cx| {
4507 workspace.add_panel(outline_panel, cx);
4508 })
4509 .unwrap();
4510 window
4511 }
4512
4513 fn outline_panel(
4514 workspace: &WindowHandle<Workspace>,
4515 cx: &mut TestAppContext,
4516 ) -> View<OutlinePanel> {
4517 workspace
4518 .update(cx, |workspace, cx| {
4519 workspace
4520 .panel::<OutlinePanel>(cx)
4521 .expect("no outline panel")
4522 })
4523 .unwrap()
4524 }
4525
4526 fn display_entries(
4527 cached_entries: &[CachedEntry],
4528 selected_entry: Option<&PanelEntry>,
4529 ) -> String {
4530 let mut display_string = String::new();
4531 for entry in cached_entries {
4532 if !display_string.is_empty() {
4533 display_string += "\n";
4534 }
4535 for _ in 0..entry.depth {
4536 display_string += " ";
4537 }
4538 display_string += &match &entry.entry {
4539 PanelEntry::Fs(entry) => match entry {
4540 FsEntry::ExternalFile(_, _) => {
4541 panic!("Did not cover external files with tests")
4542 }
4543 FsEntry::Directory(_, dir_entry) => format!(
4544 "{}/",
4545 dir_entry
4546 .path
4547 .file_name()
4548 .map(|name| name.to_string_lossy().to_string())
4549 .unwrap_or_default()
4550 ),
4551 FsEntry::File(_, file_entry, ..) => file_entry
4552 .path
4553 .file_name()
4554 .map(|name| name.to_string_lossy().to_string())
4555 .unwrap_or_default(),
4556 },
4557 PanelEntry::FoldedDirs(_, dirs) => dirs
4558 .iter()
4559 .filter_map(|dir| dir.path.file_name())
4560 .map(|name| name.to_string_lossy().to_string() + "/")
4561 .collect(),
4562 PanelEntry::Outline(outline_entry) => match outline_entry {
4563 OutlineEntry::Excerpt(_, _, _) => continue,
4564 OutlineEntry::Outline(_, _, outline) => format!("outline: {}", outline.text),
4565 },
4566 PanelEntry::Search(SearchEntry { render_data, .. }) => {
4567 format!("search: {}", render_data.context_text)
4568 }
4569 };
4570
4571 if Some(&entry.entry) == selected_entry {
4572 display_string += SELECTED_MARKER;
4573 }
4574 }
4575 display_string
4576 }
4577
4578 fn init_test(cx: &mut TestAppContext) {
4579 cx.update(|cx| {
4580 let settings = SettingsStore::test(cx);
4581 cx.set_global(settings);
4582
4583 theme::init(theme::LoadThemes::JustBase, cx);
4584
4585 language::init(cx);
4586 editor::init(cx);
4587 workspace::init_settings(cx);
4588 Project::init_settings(cx);
4589 project_search::init(cx);
4590 super::init((), cx);
4591 });
4592 }
4593
4594 // Based on https://github.com/rust-lang/rust-analyzer/
4595 async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
4596 fs.insert_tree(
4597 root,
4598 json!({
4599 "crates": {
4600 "ide": {
4601 "src": {
4602 "inlay_hints": {
4603 "fn_lifetime_fn.rs": r##"
4604 pub(super) fn hints(
4605 acc: &mut Vec<InlayHint>,
4606 config: &InlayHintsConfig,
4607 func: ast::Fn,
4608 ) -> Option<()> {
4609 // ... snip
4610
4611 let mut used_names: FxHashMap<SmolStr, usize> =
4612 match config.param_names_for_lifetime_elision_hints {
4613 true => generic_param_list
4614 .iter()
4615 .flat_map(|gpl| gpl.lifetime_params())
4616 .filter_map(|param| param.lifetime())
4617 .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
4618 .collect(),
4619 false => Default::default(),
4620 };
4621 {
4622 let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
4623 if self_param.is_some() && potential_lt_refs.next().is_some() {
4624 allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
4625 // self can't be used as a lifetime, so no need to check for collisions
4626 "'self".into()
4627 } else {
4628 gen_idx_name()
4629 });
4630 }
4631 potential_lt_refs.for_each(|(name, ..)| {
4632 let name = match name {
4633 Some(it) if config.param_names_for_lifetime_elision_hints => {
4634 if let Some(c) = used_names.get_mut(it.text().as_str()) {
4635 *c += 1;
4636 SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
4637 } else {
4638 used_names.insert(it.text().as_str().into(), 0);
4639 SmolStr::from_iter(["\'", it.text().as_str()])
4640 }
4641 }
4642 _ => gen_idx_name(),
4643 };
4644 allocated_lifetimes.push(name);
4645 });
4646 }
4647
4648 // ... snip
4649 }
4650
4651 // ... snip
4652
4653 #[test]
4654 fn hints_lifetimes_named() {
4655 check_with_config(
4656 InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
4657 r#"
4658 fn nested_in<'named>(named: & &X< &()>) {}
4659 // ^'named1, 'named2, 'named3, $
4660 //^'named1 ^'named2 ^'named3
4661 "#,
4662 );
4663 }
4664
4665 // ... snip
4666 "##,
4667 },
4668 "inlay_hints.rs": r#"
4669 #[derive(Clone, Debug, PartialEq, Eq)]
4670 pub struct InlayHintsConfig {
4671 // ... snip
4672 pub param_names_for_lifetime_elision_hints: bool,
4673 pub max_length: Option<usize>,
4674 // ... snip
4675 }
4676
4677 impl Config {
4678 pub fn inlay_hints(&self) -> InlayHintsConfig {
4679 InlayHintsConfig {
4680 // ... snip
4681 param_names_for_lifetime_elision_hints: self
4682 .inlayHints_lifetimeElisionHints_useParameterNames()
4683 .to_owned(),
4684 max_length: self.inlayHints_maxLength().to_owned(),
4685 // ... snip
4686 }
4687 }
4688 }
4689 "#,
4690 "static_index.rs": r#"
4691// ... snip
4692 fn add_file(&mut self, file_id: FileId) {
4693 let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
4694 let folds = self.analysis.folding_ranges(file_id).unwrap();
4695 let inlay_hints = self
4696 .analysis
4697 .inlay_hints(
4698 &InlayHintsConfig {
4699 // ... snip
4700 closure_style: hir::ClosureStyle::ImplFn,
4701 param_names_for_lifetime_elision_hints: false,
4702 binding_mode_hints: false,
4703 max_length: Some(25),
4704 closure_capture_hints: false,
4705 // ... snip
4706 },
4707 file_id,
4708 None,
4709 )
4710 .unwrap();
4711 // ... snip
4712 }
4713// ... snip
4714 "#
4715 }
4716 },
4717 "rust-analyzer": {
4718 "src": {
4719 "cli": {
4720 "analysis_stats.rs": r#"
4721 // ... snip
4722 for &file_id in &file_ids {
4723 _ = analysis.inlay_hints(
4724 &InlayHintsConfig {
4725 // ... snip
4726 implicit_drop_hints: true,
4727 lifetime_elision_hints: ide::LifetimeElisionHints::Always,
4728 param_names_for_lifetime_elision_hints: true,
4729 hide_named_constructor_hints: false,
4730 hide_closure_initialization_hints: false,
4731 closure_style: hir::ClosureStyle::ImplFn,
4732 max_length: Some(25),
4733 closing_brace_hints_min_lines: Some(20),
4734 fields_to_resolve: InlayFieldsToResolve::empty(),
4735 range_exclusive_hints: true,
4736 },
4737 file_id.into(),
4738 None,
4739 );
4740 }
4741 // ... snip
4742 "#,
4743 },
4744 "config.rs": r#"
4745 config_data! {
4746 /// Configs that only make sense when they are set by a client. As such they can only be defined
4747 /// by setting them using client's settings (e.g `settings.json` on VS Code).
4748 client: struct ClientDefaultConfigData <- ClientConfigInput -> {
4749 // ... snip
4750 /// Maximum length for inlay hints. Set to null to have an unlimited length.
4751 inlayHints_maxLength: Option<usize> = Some(25),
4752 // ... snip
4753 /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
4754 inlayHints_lifetimeElisionHints_useParameterNames: bool = false,
4755 // ... snip
4756 }
4757 }
4758
4759 impl Config {
4760 // ... snip
4761 pub fn inlay_hints(&self) -> InlayHintsConfig {
4762 InlayHintsConfig {
4763 // ... snip
4764 param_names_for_lifetime_elision_hints: self
4765 .inlayHints_lifetimeElisionHints_useParameterNames()
4766 .to_owned(),
4767 max_length: self.inlayHints_maxLength().to_owned(),
4768 // ... snip
4769 }
4770 }
4771 // ... snip
4772 }
4773 "#
4774 }
4775 }
4776 }
4777 }),
4778 )
4779 .await;
4780 }
4781
4782 fn rust_lang() -> Language {
4783 Language::new(
4784 LanguageConfig {
4785 name: "Rust".into(),
4786 matcher: LanguageMatcher {
4787 path_suffixes: vec!["rs".to_string()],
4788 ..Default::default()
4789 },
4790 ..Default::default()
4791 },
4792 Some(tree_sitter_rust::LANGUAGE.into()),
4793 )
4794 .with_highlights_query(
4795 r#"
4796 (field_identifier) @field
4797 (struct_expression) @struct
4798 "#,
4799 )
4800 .unwrap()
4801 .with_injection_query(
4802 r#"
4803 (macro_invocation
4804 (token_tree) @content
4805 (#set! "language" "rust"))
4806 "#,
4807 )
4808 .unwrap()
4809 }
4810}