1mod outline_panel_settings;
2
3use anyhow::Context as _;
4use collections::{BTreeSet, HashMap, HashSet, hash_map};
5use db::kvp::KEY_VALUE_STORE;
6use editor::{
7 AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange,
8 MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects,
9 display_map::ToDisplayPoint,
10 items::{entry_git_aware_label_color, entry_label_color},
11 scroll::{Autoscroll, ScrollAnchor},
12};
13use file_icons::FileIcons;
14use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
15use gpui::{
16 Action, AnyElement, App, AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context,
17 DismissEvent, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
18 InteractiveElement, IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
19 MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy,
20 SharedString, Stateful, StatefulInteractiveElement as _, Styled, Subscription, Task,
21 UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, point, px, size,
22 uniform_list,
23};
24use itertools::Itertools;
25use language::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
26use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};
27use std::{
28 cmp,
29 collections::BTreeMap,
30 hash::Hash,
31 ops::Range,
32 path::{Path, PathBuf},
33 sync::{
34 Arc, OnceLock,
35 atomic::{self, AtomicBool},
36 },
37 time::Duration,
38 u32,
39};
40
41use outline_panel_settings::{DockSide, OutlinePanelSettings, ShowIndentGuides};
42use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem};
43use search::{BufferSearchBar, ProjectSearchView};
44use serde::{Deserialize, Serialize};
45use settings::{Settings, SettingsStore};
46use smol::channel;
47use theme::{SyntaxTheme, ThemeSettings};
48use ui::{
49 ContextMenu, FluentBuilder, HighlightedLabel, IconButton, IconButtonShape, IndentGuideColors,
50 IndentGuideLayout, ListItem, ScrollAxes, Scrollbars, Tab, Tooltip, WithScrollbar, prelude::*,
51};
52use util::{RangeExt, ResultExt, TryFutureExt, debug_panic, rel_path::RelPath};
53use workspace::{
54 OpenInTerminal, WeakItemHandle, Workspace,
55 dock::{DockPosition, Panel, PanelEvent},
56 item::ItemHandle,
57 searchable::{SearchEvent, SearchableItem},
58};
59use worktree::{Entry, ProjectEntryId, WorktreeId};
60
61actions!(
62 outline_panel,
63 [
64 /// Collapses all entries in the outline tree.
65 CollapseAllEntries,
66 /// Collapses the currently selected entry.
67 CollapseSelectedEntry,
68 /// Expands all entries in the outline tree.
69 ExpandAllEntries,
70 /// Expands the currently selected entry.
71 ExpandSelectedEntry,
72 /// Folds the selected directory.
73 FoldDirectory,
74 /// Opens the selected entry in the editor.
75 OpenSelectedEntry,
76 /// Reveals the selected item in the system file manager.
77 RevealInFileManager,
78 /// Selects the parent of the current entry.
79 SelectParent,
80 /// Toggles the pin status of the active editor.
81 ToggleActiveEditorPin,
82 /// Unfolds the selected directory.
83 UnfoldDirectory,
84 /// Toggles focus on the outline panel.
85 ToggleFocus,
86 ]
87);
88
89const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
90const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
91
92type Outline = OutlineItem<language::Anchor>;
93type HighlightStyleData = Arc<OnceLock<Vec<(Range<usize>, HighlightStyle)>>>;
94
95pub struct OutlinePanel {
96 fs: Arc<dyn Fs>,
97 width: Option<Pixels>,
98 project: Entity<Project>,
99 workspace: WeakEntity<Workspace>,
100 active: bool,
101 pinned: bool,
102 scroll_handle: UniformListScrollHandle,
103 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
104 focus_handle: FocusHandle,
105 pending_serialization: Task<Option<()>>,
106 fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
107 fs_entries: Vec<FsEntry>,
108 fs_children_count: HashMap<WorktreeId, HashMap<Arc<RelPath>, FsChildren>>,
109 collapsed_entries: HashSet<CollapsedEntry>,
110 unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
111 selected_entry: SelectedEntry,
112 active_item: Option<ActiveItem>,
113 _subscriptions: Vec<Subscription>,
114 updating_fs_entries: bool,
115 updating_cached_entries: bool,
116 new_entries_for_fs_update: HashSet<ExcerptId>,
117 fs_entries_update_task: Task<()>,
118 cached_entries_update_task: Task<()>,
119 reveal_selection_task: Task<anyhow::Result<()>>,
120 outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
121 excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
122 cached_entries: Vec<CachedEntry>,
123 filter_editor: Entity<Editor>,
124 mode: ItemsDisplayMode,
125 max_width_item_index: Option<usize>,
126 preserve_selection_on_buffer_fold_toggles: HashSet<BufferId>,
127 pending_default_expansion_depth: Option<usize>,
128 outline_children_cache: HashMap<BufferId, HashMap<(Range<Anchor>, usize), bool>>,
129}
130
131#[derive(Debug)]
132enum ItemsDisplayMode {
133 Search(SearchState),
134 Outline,
135}
136
137#[derive(Debug)]
138struct SearchState {
139 kind: SearchKind,
140 query: String,
141 matches: Vec<(Range<editor::Anchor>, Arc<OnceLock<SearchData>>)>,
142 highlight_search_match_tx: channel::Sender<HighlightArguments>,
143 _search_match_highlighter: Task<()>,
144 _search_match_notify: Task<()>,
145}
146
147struct HighlightArguments {
148 multi_buffer_snapshot: MultiBufferSnapshot,
149 match_range: Range<editor::Anchor>,
150 search_data: Arc<OnceLock<SearchData>>,
151}
152
153impl SearchState {
154 fn new(
155 kind: SearchKind,
156 query: String,
157 previous_matches: HashMap<Range<editor::Anchor>, Arc<OnceLock<SearchData>>>,
158 new_matches: Vec<Range<editor::Anchor>>,
159 theme: Arc<SyntaxTheme>,
160 window: &mut Window,
161 cx: &mut Context<OutlinePanel>,
162 ) -> Self {
163 let (highlight_search_match_tx, highlight_search_match_rx) = channel::unbounded();
164 let (notify_tx, notify_rx) = channel::unbounded::<()>();
165 Self {
166 kind,
167 query,
168 matches: new_matches
169 .into_iter()
170 .map(|range| {
171 let search_data = previous_matches
172 .get(&range)
173 .map(Arc::clone)
174 .unwrap_or_default();
175 (range, search_data)
176 })
177 .collect(),
178 highlight_search_match_tx,
179 _search_match_highlighter: cx.background_spawn(async move {
180 while let Ok(highlight_arguments) = highlight_search_match_rx.recv().await {
181 let needs_init = highlight_arguments.search_data.get().is_none();
182 let search_data = highlight_arguments.search_data.get_or_init(|| {
183 SearchData::new(
184 &highlight_arguments.match_range,
185 &highlight_arguments.multi_buffer_snapshot,
186 )
187 });
188 if needs_init {
189 notify_tx.try_send(()).ok();
190 }
191
192 let highlight_data = &search_data.highlights_data;
193 if highlight_data.get().is_some() {
194 continue;
195 }
196 let mut left_whitespaces_count = 0;
197 let mut non_whitespace_symbol_occurred = false;
198 let context_offset_range = search_data
199 .context_range
200 .to_offset(&highlight_arguments.multi_buffer_snapshot);
201 let mut offset = context_offset_range.start;
202 let mut context_text = String::new();
203 let mut highlight_ranges = Vec::new();
204 for mut chunk in highlight_arguments
205 .multi_buffer_snapshot
206 .chunks(context_offset_range.start..context_offset_range.end, true)
207 {
208 if !non_whitespace_symbol_occurred {
209 for c in chunk.text.chars() {
210 if c.is_whitespace() {
211 left_whitespaces_count += c.len_utf8();
212 } else {
213 non_whitespace_symbol_occurred = true;
214 break;
215 }
216 }
217 }
218
219 if chunk.text.len() > context_offset_range.end - offset {
220 chunk.text = &chunk.text[0..(context_offset_range.end - offset)];
221 offset = context_offset_range.end;
222 } else {
223 offset += chunk.text.len();
224 }
225 let style = chunk
226 .syntax_highlight_id
227 .and_then(|highlight| highlight.style(&theme));
228 if let Some(style) = style {
229 let start = context_text.len();
230 let end = start + chunk.text.len();
231 highlight_ranges.push((start..end, style));
232 }
233 context_text.push_str(chunk.text);
234 if offset >= context_offset_range.end {
235 break;
236 }
237 }
238
239 highlight_ranges.iter_mut().for_each(|(range, _)| {
240 range.start = range.start.saturating_sub(left_whitespaces_count);
241 range.end = range.end.saturating_sub(left_whitespaces_count);
242 });
243 if highlight_data.set(highlight_ranges).ok().is_some() {
244 notify_tx.try_send(()).ok();
245 }
246
247 let trimmed_text = context_text[left_whitespaces_count..].to_owned();
248 debug_assert_eq!(
249 trimmed_text, search_data.context_text,
250 "Highlighted text that does not match the buffer text"
251 );
252 }
253 }),
254 _search_match_notify: cx.spawn_in(window, async move |outline_panel, cx| {
255 loop {
256 match notify_rx.recv().await {
257 Ok(()) => {}
258 Err(_) => break,
259 };
260 while let Ok(()) = notify_rx.try_recv() {
261 //
262 }
263 let update_result = outline_panel.update(cx, |_, cx| {
264 cx.notify();
265 });
266 if update_result.is_err() {
267 break;
268 }
269 }
270 }),
271 }
272 }
273}
274
275#[derive(Debug)]
276enum SelectedEntry {
277 Invalidated(Option<PanelEntry>),
278 Valid(PanelEntry, usize),
279 None,
280}
281
282impl SelectedEntry {
283 fn invalidate(&mut self) {
284 match std::mem::replace(self, SelectedEntry::None) {
285 Self::Valid(entry, _) => *self = Self::Invalidated(Some(entry)),
286 Self::None => *self = Self::Invalidated(None),
287 other => *self = other,
288 }
289 }
290
291 fn is_invalidated(&self) -> bool {
292 matches!(self, Self::Invalidated(_))
293 }
294}
295
296#[derive(Debug, Clone, Copy, Default)]
297struct FsChildren {
298 files: usize,
299 dirs: usize,
300}
301
302impl FsChildren {
303 fn may_be_fold_part(&self) -> bool {
304 self.dirs == 0 || (self.dirs == 1 && self.files == 0)
305 }
306}
307
308#[derive(Clone, Debug)]
309struct CachedEntry {
310 depth: usize,
311 string_match: Option<StringMatch>,
312 entry: PanelEntry,
313}
314
315#[derive(Clone, Debug, PartialEq, Eq, Hash)]
316enum CollapsedEntry {
317 Dir(WorktreeId, ProjectEntryId),
318 File(WorktreeId, BufferId),
319 ExternalFile(BufferId),
320 Excerpt(BufferId, ExcerptId),
321 Outline(BufferId, ExcerptId, Range<Anchor>),
322}
323
324#[derive(Debug)]
325struct Excerpt {
326 range: ExcerptRange<language::Anchor>,
327 outlines: ExcerptOutlines,
328}
329
330impl Excerpt {
331 fn invalidate_outlines(&mut self) {
332 if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines {
333 self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines));
334 }
335 }
336
337 fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {
338 match &self.outlines {
339 ExcerptOutlines::Outlines(outlines) => outlines.iter(),
340 ExcerptOutlines::Invalidated(outlines) => outlines.iter(),
341 ExcerptOutlines::NotFetched => [].iter(),
342 }
343 }
344
345 fn should_fetch_outlines(&self) -> bool {
346 match &self.outlines {
347 ExcerptOutlines::Outlines(_) => false,
348 ExcerptOutlines::Invalidated(_) => true,
349 ExcerptOutlines::NotFetched => true,
350 }
351 }
352}
353
354#[derive(Debug)]
355enum ExcerptOutlines {
356 Outlines(Vec<Outline>),
357 Invalidated(Vec<Outline>),
358 NotFetched,
359}
360
361#[derive(Clone, Debug, PartialEq, Eq)]
362struct FoldedDirsEntry {
363 worktree_id: WorktreeId,
364 entries: Vec<GitEntry>,
365}
366
367// TODO: collapse the inner enums into panel entry
368#[derive(Clone, Debug)]
369enum PanelEntry {
370 Fs(FsEntry),
371 FoldedDirs(FoldedDirsEntry),
372 Outline(OutlineEntry),
373 Search(SearchEntry),
374}
375
376#[derive(Clone, Debug)]
377struct SearchEntry {
378 match_range: Range<editor::Anchor>,
379 kind: SearchKind,
380 render_data: Arc<OnceLock<SearchData>>,
381}
382
383#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
384enum SearchKind {
385 Project,
386 Buffer,
387}
388
389#[derive(Clone, Debug)]
390struct SearchData {
391 context_range: Range<editor::Anchor>,
392 context_text: String,
393 truncated_left: bool,
394 truncated_right: bool,
395 search_match_indices: Vec<Range<usize>>,
396 highlights_data: HighlightStyleData,
397}
398
399impl PartialEq for PanelEntry {
400 fn eq(&self, other: &Self) -> bool {
401 match (self, other) {
402 (Self::Fs(a), Self::Fs(b)) => a == b,
403 (
404 Self::FoldedDirs(FoldedDirsEntry {
405 worktree_id: worktree_id_a,
406 entries: entries_a,
407 }),
408 Self::FoldedDirs(FoldedDirsEntry {
409 worktree_id: worktree_id_b,
410 entries: entries_b,
411 }),
412 ) => worktree_id_a == worktree_id_b && entries_a == entries_b,
413 (Self::Outline(a), Self::Outline(b)) => a == b,
414 (
415 Self::Search(SearchEntry {
416 match_range: match_range_a,
417 kind: kind_a,
418 ..
419 }),
420 Self::Search(SearchEntry {
421 match_range: match_range_b,
422 kind: kind_b,
423 ..
424 }),
425 ) => match_range_a == match_range_b && kind_a == kind_b,
426 _ => false,
427 }
428 }
429}
430
431impl Eq for PanelEntry {}
432
433const SEARCH_MATCH_CONTEXT_SIZE: u32 = 40;
434const TRUNCATED_CONTEXT_MARK: &str = "…";
435
436impl SearchData {
437 fn new(
438 match_range: &Range<editor::Anchor>,
439 multi_buffer_snapshot: &MultiBufferSnapshot,
440 ) -> Self {
441 let match_point_range = match_range.to_point(multi_buffer_snapshot);
442 let context_left_border = multi_buffer_snapshot.clip_point(
443 language::Point::new(
444 match_point_range.start.row,
445 match_point_range
446 .start
447 .column
448 .saturating_sub(SEARCH_MATCH_CONTEXT_SIZE),
449 ),
450 Bias::Left,
451 );
452 let context_right_border = multi_buffer_snapshot.clip_point(
453 language::Point::new(
454 match_point_range.end.row,
455 match_point_range.end.column + SEARCH_MATCH_CONTEXT_SIZE,
456 ),
457 Bias::Right,
458 );
459
460 let context_anchor_range =
461 (context_left_border..context_right_border).to_anchors(multi_buffer_snapshot);
462 let context_offset_range = context_anchor_range.to_offset(multi_buffer_snapshot);
463 let match_offset_range = match_range.to_offset(multi_buffer_snapshot);
464
465 let mut search_match_indices = vec![
466 match_offset_range.start - context_offset_range.start
467 ..match_offset_range.end - context_offset_range.start,
468 ];
469
470 let entire_context_text = multi_buffer_snapshot
471 .text_for_range(context_offset_range.clone())
472 .collect::<String>();
473 let left_whitespaces_offset = entire_context_text
474 .chars()
475 .take_while(|c| c.is_whitespace())
476 .map(|c| c.len_utf8())
477 .sum::<usize>();
478
479 let mut extended_context_left_border = context_left_border;
480 extended_context_left_border.column = extended_context_left_border.column.saturating_sub(1);
481 let extended_context_left_border =
482 multi_buffer_snapshot.clip_point(extended_context_left_border, Bias::Left);
483 let mut extended_context_right_border = context_right_border;
484 extended_context_right_border.column += 1;
485 let extended_context_right_border =
486 multi_buffer_snapshot.clip_point(extended_context_right_border, Bias::Right);
487
488 let truncated_left = left_whitespaces_offset == 0
489 && extended_context_left_border < context_left_border
490 && multi_buffer_snapshot
491 .chars_at(extended_context_left_border)
492 .last()
493 .is_some_and(|c| !c.is_whitespace());
494 let truncated_right = entire_context_text
495 .chars()
496 .last()
497 .is_none_or(|c| !c.is_whitespace())
498 && extended_context_right_border > context_right_border
499 && multi_buffer_snapshot
500 .chars_at(extended_context_right_border)
501 .next()
502 .is_some_and(|c| !c.is_whitespace());
503 search_match_indices.iter_mut().for_each(|range| {
504 range.start = range.start.saturating_sub(left_whitespaces_offset);
505 range.end = range.end.saturating_sub(left_whitespaces_offset);
506 });
507
508 let trimmed_row_offset_range =
509 context_offset_range.start + left_whitespaces_offset..context_offset_range.end;
510 let trimmed_text = entire_context_text[left_whitespaces_offset..].to_owned();
511 Self {
512 highlights_data: Arc::default(),
513 search_match_indices,
514 context_range: trimmed_row_offset_range.to_anchors(multi_buffer_snapshot),
515 context_text: trimmed_text,
516 truncated_left,
517 truncated_right,
518 }
519 }
520}
521
522#[derive(Clone, Debug, PartialEq, Eq, Hash)]
523struct OutlineEntryExcerpt {
524 id: ExcerptId,
525 buffer_id: BufferId,
526 range: ExcerptRange<language::Anchor>,
527}
528
529#[derive(Clone, Debug, Eq)]
530struct OutlineEntryOutline {
531 buffer_id: BufferId,
532 excerpt_id: ExcerptId,
533 outline: Outline,
534}
535
536impl PartialEq for OutlineEntryOutline {
537 fn eq(&self, other: &Self) -> bool {
538 self.buffer_id == other.buffer_id
539 && self.excerpt_id == other.excerpt_id
540 && self.outline.depth == other.outline.depth
541 && self.outline.range == other.outline.range
542 && self.outline.text == other.outline.text
543 }
544}
545
546impl Hash for OutlineEntryOutline {
547 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
548 (
549 self.buffer_id,
550 self.excerpt_id,
551 self.outline.depth,
552 &self.outline.range,
553 &self.outline.text,
554 )
555 .hash(state);
556 }
557}
558
559#[derive(Clone, Debug, PartialEq, Eq)]
560enum OutlineEntry {
561 Excerpt(OutlineEntryExcerpt),
562 Outline(OutlineEntryOutline),
563}
564
565impl OutlineEntry {
566 fn ids(&self) -> (BufferId, ExcerptId) {
567 match self {
568 OutlineEntry::Excerpt(excerpt) => (excerpt.buffer_id, excerpt.id),
569 OutlineEntry::Outline(outline) => (outline.buffer_id, outline.excerpt_id),
570 }
571 }
572}
573
574#[derive(Debug, Clone, Eq)]
575struct FsEntryFile {
576 worktree_id: WorktreeId,
577 entry: GitEntry,
578 buffer_id: BufferId,
579 excerpts: Vec<ExcerptId>,
580}
581
582impl PartialEq for FsEntryFile {
583 fn eq(&self, other: &Self) -> bool {
584 self.worktree_id == other.worktree_id
585 && self.entry.id == other.entry.id
586 && self.buffer_id == other.buffer_id
587 }
588}
589
590impl Hash for FsEntryFile {
591 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
592 (self.buffer_id, self.entry.id, self.worktree_id).hash(state);
593 }
594}
595
596#[derive(Debug, Clone, Eq)]
597struct FsEntryDirectory {
598 worktree_id: WorktreeId,
599 entry: GitEntry,
600}
601
602impl PartialEq for FsEntryDirectory {
603 fn eq(&self, other: &Self) -> bool {
604 self.worktree_id == other.worktree_id && self.entry.id == other.entry.id
605 }
606}
607
608impl Hash for FsEntryDirectory {
609 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
610 (self.worktree_id, self.entry.id).hash(state);
611 }
612}
613
614#[derive(Debug, Clone, Eq)]
615struct FsEntryExternalFile {
616 buffer_id: BufferId,
617 excerpts: Vec<ExcerptId>,
618}
619
620impl PartialEq for FsEntryExternalFile {
621 fn eq(&self, other: &Self) -> bool {
622 self.buffer_id == other.buffer_id
623 }
624}
625
626impl Hash for FsEntryExternalFile {
627 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
628 self.buffer_id.hash(state);
629 }
630}
631
632#[derive(Clone, Debug, Eq, PartialEq)]
633enum FsEntry {
634 ExternalFile(FsEntryExternalFile),
635 Directory(FsEntryDirectory),
636 File(FsEntryFile),
637}
638
639struct ActiveItem {
640 item_handle: Box<dyn WeakItemHandle>,
641 active_editor: WeakEntity<Editor>,
642 _buffer_search_subscription: Subscription,
643 _editor_subscription: Subscription,
644}
645
646#[derive(Debug)]
647pub enum Event {
648 Focus,
649}
650
651#[derive(Serialize, Deserialize)]
652struct SerializedOutlinePanel {
653 width: Option<Pixels>,
654 active: Option<bool>,
655}
656
657pub fn init(cx: &mut App) {
658 cx.observe_new(|workspace: &mut Workspace, _, _| {
659 workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
660 workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
661 });
662 })
663 .detach();
664}
665
666impl OutlinePanel {
667 pub async fn load(
668 workspace: WeakEntity<Workspace>,
669 mut cx: AsyncWindowContext,
670 ) -> anyhow::Result<Entity<Self>> {
671 let serialized_panel = match workspace
672 .read_with(&cx, |workspace, _| {
673 OutlinePanel::serialization_key(workspace)
674 })
675 .ok()
676 .flatten()
677 {
678 Some(serialization_key) => cx
679 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })
680 .await
681 .context("loading outline panel")
682 .log_err()
683 .flatten()
684 .map(|panel| serde_json::from_str::<SerializedOutlinePanel>(&panel))
685 .transpose()
686 .log_err()
687 .flatten(),
688 None => None,
689 };
690
691 workspace.update_in(&mut cx, |workspace, window, cx| {
692 let panel = Self::new(workspace, window, cx);
693 if let Some(serialized_panel) = serialized_panel {
694 panel.update(cx, |panel, cx| {
695 panel.width = serialized_panel.width.map(|px| px.round());
696 panel.active = serialized_panel.active.unwrap_or(false);
697 cx.notify();
698 });
699 }
700 panel
701 })
702 }
703
704 fn new(
705 workspace: &mut Workspace,
706 window: &mut Window,
707 cx: &mut Context<Workspace>,
708 ) -> Entity<Self> {
709 let project = workspace.project().clone();
710 let workspace_handle = cx.entity().downgrade();
711
712 cx.new(|cx| {
713 let filter_editor = cx.new(|cx| {
714 let mut editor = Editor::single_line(window, cx);
715 editor.set_placeholder_text("Search buffer symbols…", window, cx);
716 editor
717 });
718 let filter_update_subscription = cx.subscribe_in(
719 &filter_editor,
720 window,
721 |outline_panel: &mut Self, _, event, window, cx| {
722 if let editor::EditorEvent::BufferEdited = event {
723 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
724 }
725 },
726 );
727
728 let focus_handle = cx.focus_handle();
729 let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in);
730 let workspace_subscription = cx.subscribe_in(
731 &workspace
732 .weak_handle()
733 .upgrade()
734 .expect("have a &mut Workspace"),
735 window,
736 move |outline_panel, workspace, event, window, cx| {
737 if let workspace::Event::ActiveItemChanged = event {
738 if let Some((new_active_item, new_active_editor)) =
739 workspace_active_editor(workspace.read(cx), cx)
740 {
741 if outline_panel.should_replace_active_item(new_active_item.as_ref()) {
742 outline_panel.replace_active_editor(
743 new_active_item,
744 new_active_editor,
745 window,
746 cx,
747 );
748 }
749 } else {
750 outline_panel.clear_previous(window, cx);
751 cx.notify();
752 }
753 }
754 },
755 );
756
757 let icons_subscription = cx.observe_global::<FileIcons>(|_, cx| {
758 cx.notify();
759 });
760
761 let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx);
762 let mut current_theme = ThemeSettings::get_global(cx).clone();
763 let settings_subscription =
764 cx.observe_global_in::<SettingsStore>(window, move |outline_panel, window, cx| {
765 let new_settings = OutlinePanelSettings::get_global(cx);
766 let new_theme = ThemeSettings::get_global(cx);
767 if ¤t_theme != new_theme {
768 outline_panel_settings = *new_settings;
769 current_theme = new_theme.clone();
770 for excerpts in outline_panel.excerpts.values_mut() {
771 for excerpt in excerpts.values_mut() {
772 excerpt.invalidate_outlines();
773 }
774 }
775 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
776 if update_cached_items {
777 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
778 }
779 } else if &outline_panel_settings != new_settings {
780 let old_expansion_depth = outline_panel_settings.expand_outlines_with_depth;
781 outline_panel_settings = *new_settings;
782
783 if old_expansion_depth != new_settings.expand_outlines_with_depth {
784 let old_collapsed_entries = outline_panel.collapsed_entries.clone();
785 outline_panel
786 .collapsed_entries
787 .retain(|entry| !matches!(entry, CollapsedEntry::Outline(..)));
788
789 let new_depth = new_settings.expand_outlines_with_depth;
790
791 for (buffer_id, excerpts) in &outline_panel.excerpts {
792 for (excerpt_id, excerpt) in excerpts {
793 if let ExcerptOutlines::Outlines(outlines) = &excerpt.outlines {
794 for outline in outlines {
795 if outline_panel
796 .outline_children_cache
797 .get(buffer_id)
798 .and_then(|children_map| {
799 let key =
800 (outline.range.clone(), outline.depth);
801 children_map.get(&key)
802 })
803 .copied()
804 .unwrap_or(false)
805 && (new_depth == 0 || outline.depth >= new_depth)
806 {
807 outline_panel.collapsed_entries.insert(
808 CollapsedEntry::Outline(
809 *buffer_id,
810 *excerpt_id,
811 outline.range.clone(),
812 ),
813 );
814 }
815 }
816 }
817 }
818 }
819
820 if old_collapsed_entries != outline_panel.collapsed_entries {
821 outline_panel.update_cached_entries(
822 Some(UPDATE_DEBOUNCE),
823 window,
824 cx,
825 );
826 }
827 } else {
828 cx.notify();
829 }
830 }
831 });
832
833 let scroll_handle = UniformListScrollHandle::new();
834
835 let mut outline_panel = Self {
836 mode: ItemsDisplayMode::Outline,
837 active: false,
838 pinned: false,
839 workspace: workspace_handle,
840 project,
841 fs: workspace.app_state().fs.clone(),
842 max_width_item_index: None,
843 scroll_handle,
844 focus_handle,
845 filter_editor,
846 fs_entries: Vec::new(),
847 fs_entries_depth: HashMap::default(),
848 fs_children_count: HashMap::default(),
849 collapsed_entries: HashSet::default(),
850 unfolded_dirs: HashMap::default(),
851 selected_entry: SelectedEntry::None,
852 context_menu: None,
853 width: None,
854 active_item: None,
855 pending_serialization: Task::ready(None),
856 updating_fs_entries: false,
857 updating_cached_entries: false,
858 new_entries_for_fs_update: HashSet::default(),
859 preserve_selection_on_buffer_fold_toggles: HashSet::default(),
860 pending_default_expansion_depth: None,
861 fs_entries_update_task: Task::ready(()),
862 cached_entries_update_task: Task::ready(()),
863 reveal_selection_task: Task::ready(Ok(())),
864 outline_fetch_tasks: HashMap::default(),
865 excerpts: HashMap::default(),
866 cached_entries: Vec::new(),
867 _subscriptions: vec![
868 settings_subscription,
869 icons_subscription,
870 focus_subscription,
871 workspace_subscription,
872 filter_update_subscription,
873 ],
874 outline_children_cache: HashMap::default(),
875 };
876 if let Some((item, editor)) = workspace_active_editor(workspace, cx) {
877 outline_panel.replace_active_editor(item, editor, window, cx);
878 }
879 outline_panel
880 })
881 }
882
883 fn serialization_key(workspace: &Workspace) -> Option<String> {
884 workspace
885 .database_id()
886 .map(|id| i64::from(id).to_string())
887 .or(workspace.session_id())
888 .map(|id| format!("{}-{:?}", OUTLINE_PANEL_KEY, id))
889 }
890
891 fn serialize(&mut self, cx: &mut Context<Self>) {
892 let Some(serialization_key) = self
893 .workspace
894 .read_with(cx, |workspace, _| {
895 OutlinePanel::serialization_key(workspace)
896 })
897 .ok()
898 .flatten()
899 else {
900 return;
901 };
902 let width = self.width;
903 let active = Some(self.active);
904 self.pending_serialization = cx.background_spawn(
905 async move {
906 KEY_VALUE_STORE
907 .write_kvp(
908 serialization_key,
909 serde_json::to_string(&SerializedOutlinePanel { width, active })?,
910 )
911 .await?;
912 anyhow::Ok(())
913 }
914 .log_err(),
915 );
916 }
917
918 fn dispatch_context(&self, window: &mut Window, cx: &mut Context<Self>) -> KeyContext {
919 let mut dispatch_context = KeyContext::new_with_defaults();
920 dispatch_context.add("OutlinePanel");
921 dispatch_context.add("menu");
922 let identifier = if self.filter_editor.focus_handle(cx).is_focused(window) {
923 "editing"
924 } else {
925 "not_editing"
926 };
927 dispatch_context.add(identifier);
928 dispatch_context
929 }
930
931 fn unfold_directory(
932 &mut self,
933 _: &UnfoldDirectory,
934 window: &mut Window,
935 cx: &mut Context<Self>,
936 ) {
937 if let Some(PanelEntry::FoldedDirs(FoldedDirsEntry {
938 worktree_id,
939 entries,
940 ..
941 })) = self.selected_entry().cloned()
942 {
943 self.unfolded_dirs
944 .entry(worktree_id)
945 .or_default()
946 .extend(entries.iter().map(|entry| entry.id));
947 self.update_cached_entries(None, window, cx);
948 }
949 }
950
951 fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
952 let (worktree_id, entry) = match self.selected_entry().cloned() {
953 Some(PanelEntry::Fs(FsEntry::Directory(directory))) => {
954 (directory.worktree_id, Some(directory.entry))
955 }
956 Some(PanelEntry::FoldedDirs(folded_dirs)) => {
957 (folded_dirs.worktree_id, folded_dirs.entries.last().cloned())
958 }
959 _ => return,
960 };
961 let Some(entry) = entry else {
962 return;
963 };
964 let unfolded_dirs = self.unfolded_dirs.get_mut(&worktree_id);
965 let worktree = self
966 .project
967 .read(cx)
968 .worktree_for_id(worktree_id, cx)
969 .map(|w| w.read(cx).snapshot());
970 let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
971 return;
972 };
973
974 unfolded_dirs.remove(&entry.id);
975 self.update_cached_entries(None, window, cx);
976 }
977
978 fn open_selected_entry(
979 &mut self,
980 _: &OpenSelectedEntry,
981 window: &mut Window,
982 cx: &mut Context<Self>,
983 ) {
984 if self.filter_editor.focus_handle(cx).is_focused(window) {
985 cx.propagate()
986 } else if let Some(selected_entry) = self.selected_entry().cloned() {
987 self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
988 }
989 }
990
991 fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
992 if self.filter_editor.focus_handle(cx).is_focused(window) {
993 self.focus_handle.focus(window);
994 } else {
995 self.filter_editor.focus_handle(cx).focus(window);
996 }
997
998 if self.context_menu.is_some() {
999 self.context_menu.take();
1000 cx.notify();
1001 }
1002 }
1003
1004 fn open_excerpts(
1005 &mut self,
1006 action: &editor::actions::OpenExcerpts,
1007 window: &mut Window,
1008 cx: &mut Context<Self>,
1009 ) {
1010 if self.filter_editor.focus_handle(cx).is_focused(window) {
1011 cx.propagate()
1012 } else if let Some((active_editor, selected_entry)) =
1013 self.active_editor().zip(self.selected_entry().cloned())
1014 {
1015 self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
1016 active_editor.update(cx, |editor, cx| editor.open_excerpts(action, window, cx));
1017 }
1018 }
1019
1020 fn open_excerpts_split(
1021 &mut self,
1022 action: &editor::actions::OpenExcerptsSplit,
1023 window: &mut Window,
1024 cx: &mut Context<Self>,
1025 ) {
1026 if self.filter_editor.focus_handle(cx).is_focused(window) {
1027 cx.propagate()
1028 } else if let Some((active_editor, selected_entry)) =
1029 self.active_editor().zip(self.selected_entry().cloned())
1030 {
1031 self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
1032 active_editor.update(cx, |editor, cx| {
1033 editor.open_excerpts_in_split(action, window, cx)
1034 });
1035 }
1036 }
1037
1038 fn scroll_editor_to_entry(
1039 &mut self,
1040 entry: &PanelEntry,
1041 prefer_selection_change: bool,
1042 prefer_focus_change: bool,
1043 window: &mut Window,
1044 cx: &mut Context<OutlinePanel>,
1045 ) {
1046 let Some(active_editor) = self.active_editor() else {
1047 return;
1048 };
1049 let active_multi_buffer = active_editor.read(cx).buffer().clone();
1050 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
1051 let mut change_selection = prefer_selection_change;
1052 let mut change_focus = prefer_focus_change;
1053 let mut scroll_to_buffer = None;
1054 let scroll_target = match entry {
1055 PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => {
1056 change_focus = false;
1057 None
1058 }
1059 PanelEntry::Fs(FsEntry::ExternalFile(file)) => {
1060 change_selection = false;
1061 scroll_to_buffer = Some(file.buffer_id);
1062 multi_buffer_snapshot.excerpts().find_map(
1063 |(excerpt_id, buffer_snapshot, excerpt_range)| {
1064 if buffer_snapshot.remote_id() == file.buffer_id {
1065 multi_buffer_snapshot
1066 .anchor_in_excerpt(excerpt_id, excerpt_range.context.start)
1067 } else {
1068 None
1069 }
1070 },
1071 )
1072 }
1073
1074 PanelEntry::Fs(FsEntry::File(file)) => {
1075 change_selection = false;
1076 scroll_to_buffer = Some(file.buffer_id);
1077 self.project
1078 .update(cx, |project, cx| {
1079 project
1080 .path_for_entry(file.entry.id, cx)
1081 .and_then(|path| project.get_open_buffer(&path, cx))
1082 })
1083 .map(|buffer| {
1084 active_multi_buffer
1085 .read(cx)
1086 .excerpts_for_buffer(buffer.read(cx).remote_id(), cx)
1087 })
1088 .and_then(|excerpts| {
1089 let (excerpt_id, excerpt_range) = excerpts.first()?;
1090 multi_buffer_snapshot
1091 .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
1092 })
1093 }
1094 PanelEntry::Outline(OutlineEntry::Outline(outline)) => multi_buffer_snapshot
1095 .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.start)
1096 .or_else(|| {
1097 multi_buffer_snapshot
1098 .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.end)
1099 }),
1100 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1101 change_selection = false;
1102 change_focus = false;
1103 multi_buffer_snapshot.anchor_in_excerpt(excerpt.id, excerpt.range.context.start)
1104 }
1105 PanelEntry::Search(search_entry) => Some(search_entry.match_range.start),
1106 };
1107
1108 if let Some(anchor) = scroll_target {
1109 let activate = self
1110 .workspace
1111 .update(cx, |workspace, cx| match self.active_item() {
1112 Some(active_item) => workspace.activate_item(
1113 active_item.as_ref(),
1114 true,
1115 change_focus,
1116 window,
1117 cx,
1118 ),
1119 None => workspace.activate_item(&active_editor, true, change_focus, window, cx),
1120 });
1121
1122 if activate.is_ok() {
1123 self.select_entry(entry.clone(), true, window, cx);
1124 if change_selection {
1125 active_editor.update(cx, |editor, cx| {
1126 editor.change_selections(
1127 SelectionEffects::scroll(Autoscroll::center()),
1128 window,
1129 cx,
1130 |s| s.select_ranges(Some(anchor..anchor)),
1131 );
1132 });
1133 } else {
1134 let mut offset = Point::default();
1135 if let Some(buffer_id) = scroll_to_buffer
1136 && multi_buffer_snapshot.as_singleton().is_none()
1137 && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
1138 {
1139 offset.y = -(active_editor.read(cx).file_header_size() as f64);
1140 }
1141
1142 active_editor.update(cx, |editor, cx| {
1143 editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, window, cx);
1144 });
1145 }
1146
1147 if change_focus {
1148 active_editor.focus_handle(cx).focus(window);
1149 } else {
1150 self.focus_handle.focus(window);
1151 }
1152 }
1153 }
1154 }
1155
1156 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1157 if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1158 self.cached_entries
1159 .iter()
1160 .map(|cached_entry| &cached_entry.entry)
1161 .skip_while(|entry| entry != &selected_entry)
1162 .nth(1)
1163 .cloned()
1164 }) {
1165 self.select_entry(entry_to_select, true, window, cx);
1166 } else {
1167 self.select_first(&SelectFirst {}, window, cx)
1168 }
1169 if let Some(selected_entry) = self.selected_entry().cloned() {
1170 self.scroll_editor_to_entry(&selected_entry, true, false, window, cx);
1171 }
1172 }
1173
1174 fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
1175 if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1176 self.cached_entries
1177 .iter()
1178 .rev()
1179 .map(|cached_entry| &cached_entry.entry)
1180 .skip_while(|entry| entry != &selected_entry)
1181 .nth(1)
1182 .cloned()
1183 }) {
1184 self.select_entry(entry_to_select, true, window, cx);
1185 } else {
1186 self.select_last(&SelectLast, window, cx)
1187 }
1188 if let Some(selected_entry) = self.selected_entry().cloned() {
1189 self.scroll_editor_to_entry(&selected_entry, true, false, window, cx);
1190 }
1191 }
1192
1193 fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
1194 if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1195 let mut previous_entries = self
1196 .cached_entries
1197 .iter()
1198 .rev()
1199 .map(|cached_entry| &cached_entry.entry)
1200 .skip_while(|entry| entry != &selected_entry)
1201 .skip(1);
1202 match &selected_entry {
1203 PanelEntry::Fs(fs_entry) => match fs_entry {
1204 FsEntry::ExternalFile(..) => None,
1205 FsEntry::File(FsEntryFile {
1206 worktree_id, entry, ..
1207 })
1208 | FsEntry::Directory(FsEntryDirectory {
1209 worktree_id, entry, ..
1210 }) => entry.path.parent().and_then(|parent_path| {
1211 previous_entries.find(|entry| match entry {
1212 PanelEntry::Fs(FsEntry::Directory(directory)) => {
1213 directory.worktree_id == *worktree_id
1214 && directory.entry.path.as_ref() == parent_path
1215 }
1216 PanelEntry::FoldedDirs(FoldedDirsEntry {
1217 worktree_id: dirs_worktree_id,
1218 entries: dirs,
1219 ..
1220 }) => {
1221 dirs_worktree_id == worktree_id
1222 && dirs
1223 .last()
1224 .is_some_and(|dir| dir.path.as_ref() == parent_path)
1225 }
1226 _ => false,
1227 })
1228 }),
1229 },
1230 PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
1231 .entries
1232 .first()
1233 .and_then(|entry| entry.path.parent())
1234 .and_then(|parent_path| {
1235 previous_entries.find(|entry| {
1236 if let PanelEntry::Fs(FsEntry::Directory(directory)) = entry {
1237 directory.worktree_id == folded_dirs.worktree_id
1238 && directory.entry.path.as_ref() == parent_path
1239 } else {
1240 false
1241 }
1242 })
1243 }),
1244 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1245 previous_entries.find(|entry| match entry {
1246 PanelEntry::Fs(FsEntry::File(file)) => {
1247 file.buffer_id == excerpt.buffer_id
1248 && file.excerpts.contains(&excerpt.id)
1249 }
1250 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1251 external_file.buffer_id == excerpt.buffer_id
1252 && external_file.excerpts.contains(&excerpt.id)
1253 }
1254 _ => false,
1255 })
1256 }
1257 PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1258 previous_entries.find(|entry| {
1259 if let PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) = entry {
1260 outline.buffer_id == excerpt.buffer_id
1261 && outline.excerpt_id == excerpt.id
1262 } else {
1263 false
1264 }
1265 })
1266 }
1267 PanelEntry::Search(_) => {
1268 previous_entries.find(|entry| !matches!(entry, PanelEntry::Search(_)))
1269 }
1270 }
1271 }) {
1272 self.select_entry(entry_to_select.clone(), true, window, cx);
1273 } else {
1274 self.select_first(&SelectFirst {}, window, cx);
1275 }
1276 }
1277
1278 fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1279 if let Some(first_entry) = self.cached_entries.first() {
1280 self.select_entry(first_entry.entry.clone(), true, window, cx);
1281 }
1282 }
1283
1284 fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
1285 if let Some(new_selection) = self
1286 .cached_entries
1287 .iter()
1288 .rev()
1289 .map(|cached_entry| &cached_entry.entry)
1290 .next()
1291 {
1292 self.select_entry(new_selection.clone(), true, window, cx);
1293 }
1294 }
1295
1296 fn autoscroll(&mut self, cx: &mut Context<Self>) {
1297 if let Some(selected_entry) = self.selected_entry() {
1298 let index = self
1299 .cached_entries
1300 .iter()
1301 .position(|cached_entry| &cached_entry.entry == selected_entry);
1302 if let Some(index) = index {
1303 self.scroll_handle
1304 .scroll_to_item(index, ScrollStrategy::Center);
1305 cx.notify();
1306 }
1307 }
1308 }
1309
1310 fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1311 if !self.focus_handle.contains_focused(window, cx) {
1312 cx.emit(Event::Focus);
1313 }
1314 }
1315
1316 fn deploy_context_menu(
1317 &mut self,
1318 position: Point<Pixels>,
1319 entry: PanelEntry,
1320 window: &mut Window,
1321 cx: &mut Context<Self>,
1322 ) {
1323 self.select_entry(entry.clone(), true, window, cx);
1324 let is_root = match &entry {
1325 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1326 worktree_id, entry, ..
1327 }))
1328 | PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1329 worktree_id, entry, ..
1330 })) => self
1331 .project
1332 .read(cx)
1333 .worktree_for_id(*worktree_id, cx)
1334 .map(|worktree| {
1335 worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1336 })
1337 .unwrap_or(false),
1338 PanelEntry::FoldedDirs(FoldedDirsEntry {
1339 worktree_id,
1340 entries,
1341 ..
1342 }) => entries
1343 .first()
1344 .and_then(|entry| {
1345 self.project
1346 .read(cx)
1347 .worktree_for_id(*worktree_id, cx)
1348 .map(|worktree| {
1349 worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1350 })
1351 })
1352 .unwrap_or(false),
1353 PanelEntry::Fs(FsEntry::ExternalFile(..)) => false,
1354 PanelEntry::Outline(..) => {
1355 cx.notify();
1356 return;
1357 }
1358 PanelEntry::Search(_) => {
1359 cx.notify();
1360 return;
1361 }
1362 };
1363 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
1364 let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(&entry);
1365 let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(&entry);
1366
1367 let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
1368 menu.context(self.focus_handle.clone())
1369 .when(cfg!(target_os = "macos"), |menu| {
1370 menu.action("Reveal in Finder", Box::new(RevealInFileManager))
1371 })
1372 .when(cfg!(not(target_os = "macos")), |menu| {
1373 menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
1374 })
1375 .action("Open in Terminal", Box::new(OpenInTerminal))
1376 .when(is_unfoldable, |menu| {
1377 menu.action("Unfold Directory", Box::new(UnfoldDirectory))
1378 })
1379 .when(is_foldable, |menu| {
1380 menu.action("Fold Directory", Box::new(FoldDirectory))
1381 })
1382 .separator()
1383 .action("Copy Path", Box::new(zed_actions::workspace::CopyPath))
1384 .action(
1385 "Copy Relative Path",
1386 Box::new(zed_actions::workspace::CopyRelativePath),
1387 )
1388 });
1389 window.focus(&context_menu.focus_handle(cx));
1390 let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
1391 outline_panel.context_menu.take();
1392 cx.notify();
1393 });
1394 self.context_menu = Some((context_menu, position, subscription));
1395 cx.notify();
1396 }
1397
1398 fn is_unfoldable(&self, entry: &PanelEntry) -> bool {
1399 matches!(entry, PanelEntry::FoldedDirs(..))
1400 }
1401
1402 fn is_foldable(&self, entry: &PanelEntry) -> bool {
1403 let (directory_worktree, directory_entry) = match entry {
1404 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1405 worktree_id,
1406 entry: directory_entry,
1407 ..
1408 })) => (*worktree_id, Some(directory_entry)),
1409 _ => return false,
1410 };
1411 let Some(directory_entry) = directory_entry else {
1412 return false;
1413 };
1414
1415 if self
1416 .unfolded_dirs
1417 .get(&directory_worktree)
1418 .is_none_or(|unfolded_dirs| !unfolded_dirs.contains(&directory_entry.id))
1419 {
1420 return false;
1421 }
1422
1423 let children = self
1424 .fs_children_count
1425 .get(&directory_worktree)
1426 .and_then(|entries| entries.get(&directory_entry.path))
1427 .copied()
1428 .unwrap_or_default();
1429
1430 children.may_be_fold_part() && children.dirs > 0
1431 }
1432
1433 fn expand_selected_entry(
1434 &mut self,
1435 _: &ExpandSelectedEntry,
1436 window: &mut Window,
1437 cx: &mut Context<Self>,
1438 ) {
1439 let Some(active_editor) = self.active_editor() else {
1440 return;
1441 };
1442 let Some(selected_entry) = self.selected_entry().cloned() else {
1443 return;
1444 };
1445 let mut buffers_to_unfold = HashSet::default();
1446 let entry_to_expand = match &selected_entry {
1447 PanelEntry::FoldedDirs(FoldedDirsEntry {
1448 entries: dir_entries,
1449 worktree_id,
1450 ..
1451 }) => dir_entries.last().map(|entry| {
1452 buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry));
1453 CollapsedEntry::Dir(*worktree_id, entry.id)
1454 }),
1455 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1456 worktree_id, entry, ..
1457 })) => {
1458 buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry));
1459 Some(CollapsedEntry::Dir(*worktree_id, entry.id))
1460 }
1461 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1462 worktree_id,
1463 buffer_id,
1464 ..
1465 })) => {
1466 buffers_to_unfold.insert(*buffer_id);
1467 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1468 }
1469 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1470 buffers_to_unfold.insert(external_file.buffer_id);
1471 Some(CollapsedEntry::ExternalFile(external_file.buffer_id))
1472 }
1473 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1474 Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id))
1475 }
1476 PanelEntry::Outline(OutlineEntry::Outline(outline)) => Some(CollapsedEntry::Outline(
1477 outline.buffer_id,
1478 outline.excerpt_id,
1479 outline.outline.range.clone(),
1480 )),
1481 PanelEntry::Search(_) => return,
1482 };
1483 let Some(collapsed_entry) = entry_to_expand else {
1484 return;
1485 };
1486 let expanded = self.collapsed_entries.remove(&collapsed_entry);
1487 if expanded {
1488 if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
1489 let task = self.project.update(cx, |project, cx| {
1490 project.expand_entry(worktree_id, dir_entry_id, cx)
1491 });
1492 if let Some(task) = task {
1493 task.detach_and_log_err(cx);
1494 }
1495 };
1496
1497 active_editor.update(cx, |editor, cx| {
1498 buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
1499 });
1500 self.select_entry(selected_entry, true, window, cx);
1501 if buffers_to_unfold.is_empty() {
1502 self.update_cached_entries(None, window, cx);
1503 } else {
1504 self.toggle_buffers_fold(buffers_to_unfold, false, window, cx)
1505 .detach();
1506 }
1507 } else {
1508 self.select_next(&SelectNext, window, cx)
1509 }
1510 }
1511
1512 fn collapse_selected_entry(
1513 &mut self,
1514 _: &CollapseSelectedEntry,
1515 window: &mut Window,
1516 cx: &mut Context<Self>,
1517 ) {
1518 let Some(active_editor) = self.active_editor() else {
1519 return;
1520 };
1521 let Some(selected_entry) = self.selected_entry().cloned() else {
1522 return;
1523 };
1524
1525 let mut buffers_to_fold = HashSet::default();
1526 let collapsed = match &selected_entry {
1527 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1528 worktree_id, entry, ..
1529 })) => {
1530 if self
1531 .collapsed_entries
1532 .insert(CollapsedEntry::Dir(*worktree_id, entry.id))
1533 {
1534 buffers_to_fold.extend(self.buffers_inside_directory(*worktree_id, entry));
1535 true
1536 } else {
1537 false
1538 }
1539 }
1540 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1541 worktree_id,
1542 buffer_id,
1543 ..
1544 })) => {
1545 if self
1546 .collapsed_entries
1547 .insert(CollapsedEntry::File(*worktree_id, *buffer_id))
1548 {
1549 buffers_to_fold.insert(*buffer_id);
1550 true
1551 } else {
1552 false
1553 }
1554 }
1555 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1556 if self
1557 .collapsed_entries
1558 .insert(CollapsedEntry::ExternalFile(external_file.buffer_id))
1559 {
1560 buffers_to_fold.insert(external_file.buffer_id);
1561 true
1562 } else {
1563 false
1564 }
1565 }
1566 PanelEntry::FoldedDirs(folded_dirs) => {
1567 let mut folded = false;
1568 if let Some(dir_entry) = folded_dirs.entries.last()
1569 && self
1570 .collapsed_entries
1571 .insert(CollapsedEntry::Dir(folded_dirs.worktree_id, dir_entry.id))
1572 {
1573 folded = true;
1574 buffers_to_fold
1575 .extend(self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry));
1576 }
1577 folded
1578 }
1579 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
1580 .collapsed_entries
1581 .insert(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)),
1582 PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1583 self.collapsed_entries.insert(CollapsedEntry::Outline(
1584 outline.buffer_id,
1585 outline.excerpt_id,
1586 outline.outline.range.clone(),
1587 ))
1588 }
1589 PanelEntry::Search(_) => false,
1590 };
1591
1592 if collapsed {
1593 active_editor.update(cx, |editor, cx| {
1594 buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
1595 });
1596 self.select_entry(selected_entry, true, window, cx);
1597 if buffers_to_fold.is_empty() {
1598 self.update_cached_entries(None, window, cx);
1599 } else {
1600 self.toggle_buffers_fold(buffers_to_fold, true, window, cx)
1601 .detach();
1602 }
1603 } else {
1604 self.select_parent(&SelectParent, window, cx);
1605 }
1606 }
1607
1608 pub fn expand_all_entries(
1609 &mut self,
1610 _: &ExpandAllEntries,
1611 window: &mut Window,
1612 cx: &mut Context<Self>,
1613 ) {
1614 let Some(active_editor) = self.active_editor() else {
1615 return;
1616 };
1617
1618 let mut to_uncollapse: HashSet<CollapsedEntry> = HashSet::default();
1619 let mut buffers_to_unfold: HashSet<BufferId> = HashSet::default();
1620
1621 for fs_entry in &self.fs_entries {
1622 match fs_entry {
1623 FsEntry::File(FsEntryFile {
1624 worktree_id,
1625 buffer_id,
1626 ..
1627 }) => {
1628 to_uncollapse.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
1629 buffers_to_unfold.insert(*buffer_id);
1630 }
1631 FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
1632 to_uncollapse.insert(CollapsedEntry::ExternalFile(*buffer_id));
1633 buffers_to_unfold.insert(*buffer_id);
1634 }
1635 FsEntry::Directory(FsEntryDirectory {
1636 worktree_id, entry, ..
1637 }) => {
1638 to_uncollapse.insert(CollapsedEntry::Dir(*worktree_id, entry.id));
1639 }
1640 }
1641 }
1642
1643 for (&buffer_id, excerpts) in &self.excerpts {
1644 for (&excerpt_id, excerpt) in excerpts {
1645 match &excerpt.outlines {
1646 ExcerptOutlines::Outlines(outlines) => {
1647 for outline in outlines {
1648 to_uncollapse.insert(CollapsedEntry::Outline(
1649 buffer_id,
1650 excerpt_id,
1651 outline.range.clone(),
1652 ));
1653 }
1654 }
1655 ExcerptOutlines::Invalidated(outlines) => {
1656 for outline in outlines {
1657 to_uncollapse.insert(CollapsedEntry::Outline(
1658 buffer_id,
1659 excerpt_id,
1660 outline.range.clone(),
1661 ));
1662 }
1663 }
1664 ExcerptOutlines::NotFetched => {}
1665 }
1666 to_uncollapse.insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1667 }
1668 }
1669
1670 for cached in &self.cached_entries {
1671 if let PanelEntry::FoldedDirs(FoldedDirsEntry {
1672 worktree_id,
1673 entries,
1674 ..
1675 }) = &cached.entry
1676 {
1677 if let Some(last) = entries.last() {
1678 to_uncollapse.insert(CollapsedEntry::Dir(*worktree_id, last.id));
1679 }
1680 }
1681 }
1682
1683 self.collapsed_entries
1684 .retain(|entry| !to_uncollapse.contains(entry));
1685
1686 active_editor.update(cx, |editor, cx| {
1687 buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
1688 });
1689
1690 if buffers_to_unfold.is_empty() {
1691 self.update_cached_entries(None, window, cx);
1692 } else {
1693 self.toggle_buffers_fold(buffers_to_unfold, false, window, cx)
1694 .detach();
1695 }
1696 }
1697
1698 pub fn collapse_all_entries(
1699 &mut self,
1700 _: &CollapseAllEntries,
1701 window: &mut Window,
1702 cx: &mut Context<Self>,
1703 ) {
1704 let Some(active_editor) = self.active_editor() else {
1705 return;
1706 };
1707 let mut buffers_to_fold = HashSet::default();
1708 self.collapsed_entries
1709 .extend(self.cached_entries.iter().filter_map(
1710 |cached_entry| match &cached_entry.entry {
1711 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1712 worktree_id,
1713 entry,
1714 ..
1715 })) => Some(CollapsedEntry::Dir(*worktree_id, entry.id)),
1716 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1717 worktree_id,
1718 buffer_id,
1719 ..
1720 })) => {
1721 buffers_to_fold.insert(*buffer_id);
1722 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1723 }
1724 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1725 buffers_to_fold.insert(external_file.buffer_id);
1726 Some(CollapsedEntry::ExternalFile(external_file.buffer_id))
1727 }
1728 PanelEntry::FoldedDirs(FoldedDirsEntry {
1729 worktree_id,
1730 entries,
1731 ..
1732 }) => Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)),
1733 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1734 Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id))
1735 }
1736 PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1737 Some(CollapsedEntry::Outline(
1738 outline.buffer_id,
1739 outline.excerpt_id,
1740 outline.outline.range.clone(),
1741 ))
1742 }
1743 PanelEntry::Search(_) => None,
1744 },
1745 ));
1746
1747 active_editor.update(cx, |editor, cx| {
1748 buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
1749 });
1750 if buffers_to_fold.is_empty() {
1751 self.update_cached_entries(None, window, cx);
1752 } else {
1753 self.toggle_buffers_fold(buffers_to_fold, true, window, cx)
1754 .detach();
1755 }
1756 }
1757
1758 fn toggle_expanded(&mut self, entry: &PanelEntry, window: &mut Window, cx: &mut Context<Self>) {
1759 let Some(active_editor) = self.active_editor() else {
1760 return;
1761 };
1762 let mut fold = false;
1763 let mut buffers_to_toggle = HashSet::default();
1764 match entry {
1765 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1766 worktree_id,
1767 entry: dir_entry,
1768 ..
1769 })) => {
1770 let entry_id = dir_entry.id;
1771 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1772 buffers_to_toggle.extend(self.buffers_inside_directory(*worktree_id, dir_entry));
1773 if self.collapsed_entries.remove(&collapsed_entry) {
1774 self.project
1775 .update(cx, |project, cx| {
1776 project.expand_entry(*worktree_id, entry_id, cx)
1777 })
1778 .unwrap_or_else(|| Task::ready(Ok(())))
1779 .detach_and_log_err(cx);
1780 } else {
1781 self.collapsed_entries.insert(collapsed_entry);
1782 fold = true;
1783 }
1784 }
1785 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1786 worktree_id,
1787 buffer_id,
1788 ..
1789 })) => {
1790 let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1791 buffers_to_toggle.insert(*buffer_id);
1792 if !self.collapsed_entries.remove(&collapsed_entry) {
1793 self.collapsed_entries.insert(collapsed_entry);
1794 fold = true;
1795 }
1796 }
1797 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1798 let collapsed_entry = CollapsedEntry::ExternalFile(external_file.buffer_id);
1799 buffers_to_toggle.insert(external_file.buffer_id);
1800 if !self.collapsed_entries.remove(&collapsed_entry) {
1801 self.collapsed_entries.insert(collapsed_entry);
1802 fold = true;
1803 }
1804 }
1805 PanelEntry::FoldedDirs(FoldedDirsEntry {
1806 worktree_id,
1807 entries: dir_entries,
1808 ..
1809 }) => {
1810 if let Some(dir_entry) = dir_entries.first() {
1811 let entry_id = dir_entry.id;
1812 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1813 buffers_to_toggle
1814 .extend(self.buffers_inside_directory(*worktree_id, dir_entry));
1815 if self.collapsed_entries.remove(&collapsed_entry) {
1816 self.project
1817 .update(cx, |project, cx| {
1818 project.expand_entry(*worktree_id, entry_id, cx)
1819 })
1820 .unwrap_or_else(|| Task::ready(Ok(())))
1821 .detach_and_log_err(cx);
1822 } else {
1823 self.collapsed_entries.insert(collapsed_entry);
1824 fold = true;
1825 }
1826 }
1827 }
1828 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1829 let collapsed_entry = CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id);
1830 if !self.collapsed_entries.remove(&collapsed_entry) {
1831 self.collapsed_entries.insert(collapsed_entry);
1832 }
1833 }
1834 PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1835 let collapsed_entry = CollapsedEntry::Outline(
1836 outline.buffer_id,
1837 outline.excerpt_id,
1838 outline.outline.range.clone(),
1839 );
1840 if !self.collapsed_entries.remove(&collapsed_entry) {
1841 self.collapsed_entries.insert(collapsed_entry);
1842 }
1843 }
1844 _ => {}
1845 }
1846
1847 active_editor.update(cx, |editor, cx| {
1848 buffers_to_toggle.retain(|buffer_id| {
1849 let folded = editor.is_buffer_folded(*buffer_id, cx);
1850 if fold { !folded } else { folded }
1851 });
1852 });
1853
1854 self.select_entry(entry.clone(), true, window, cx);
1855 if buffers_to_toggle.is_empty() {
1856 self.update_cached_entries(None, window, cx);
1857 } else {
1858 self.toggle_buffers_fold(buffers_to_toggle, fold, window, cx)
1859 .detach();
1860 }
1861 }
1862
1863 fn toggle_buffers_fold(
1864 &self,
1865 buffers: HashSet<BufferId>,
1866 fold: bool,
1867 window: &mut Window,
1868 cx: &mut Context<Self>,
1869 ) -> Task<()> {
1870 let Some(active_editor) = self.active_editor() else {
1871 return Task::ready(());
1872 };
1873 cx.spawn_in(window, async move |outline_panel, cx| {
1874 outline_panel
1875 .update_in(cx, |outline_panel, window, cx| {
1876 active_editor.update(cx, |editor, cx| {
1877 for buffer_id in buffers {
1878 outline_panel
1879 .preserve_selection_on_buffer_fold_toggles
1880 .insert(buffer_id);
1881 if fold {
1882 editor.fold_buffer(buffer_id, cx);
1883 } else {
1884 editor.unfold_buffer(buffer_id, cx);
1885 }
1886 }
1887 });
1888 if let Some(selection) = outline_panel.selected_entry().cloned() {
1889 outline_panel.scroll_editor_to_entry(&selection, false, false, window, cx);
1890 }
1891 })
1892 .ok();
1893 })
1894 }
1895
1896 fn copy_path(
1897 &mut self,
1898 _: &zed_actions::workspace::CopyPath,
1899 _: &mut Window,
1900 cx: &mut Context<Self>,
1901 ) {
1902 if let Some(clipboard_text) = self
1903 .selected_entry()
1904 .and_then(|entry| self.abs_path(entry, cx))
1905 .map(|p| p.to_string_lossy().into_owned())
1906 {
1907 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1908 }
1909 }
1910
1911 fn copy_relative_path(
1912 &mut self,
1913 _: &zed_actions::workspace::CopyRelativePath,
1914 _: &mut Window,
1915 cx: &mut Context<Self>,
1916 ) {
1917 let path_style = self.project.read(cx).path_style(cx);
1918 if let Some(clipboard_text) = self
1919 .selected_entry()
1920 .and_then(|entry| match entry {
1921 PanelEntry::Fs(entry) => self.relative_path(entry, cx),
1922 PanelEntry::FoldedDirs(folded_dirs) => {
1923 folded_dirs.entries.last().map(|entry| entry.path.clone())
1924 }
1925 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1926 })
1927 .map(|p| p.display(path_style).to_string())
1928 {
1929 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1930 }
1931 }
1932
1933 fn reveal_in_finder(
1934 &mut self,
1935 _: &RevealInFileManager,
1936 _: &mut Window,
1937 cx: &mut Context<Self>,
1938 ) {
1939 if let Some(abs_path) = self
1940 .selected_entry()
1941 .and_then(|entry| self.abs_path(entry, cx))
1942 {
1943 cx.reveal_path(&abs_path);
1944 }
1945 }
1946
1947 fn open_in_terminal(
1948 &mut self,
1949 _: &OpenInTerminal,
1950 window: &mut Window,
1951 cx: &mut Context<Self>,
1952 ) {
1953 let selected_entry = self.selected_entry();
1954 let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx));
1955 let working_directory = if let (
1956 Some(abs_path),
1957 Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1958 ) = (&abs_path, selected_entry)
1959 {
1960 abs_path.parent().map(|p| p.to_owned())
1961 } else {
1962 abs_path
1963 };
1964
1965 if let Some(working_directory) = working_directory {
1966 window.dispatch_action(
1967 workspace::OpenTerminal { working_directory }.boxed_clone(),
1968 cx,
1969 )
1970 }
1971 }
1972
1973 fn reveal_entry_for_selection(
1974 &mut self,
1975 editor: Entity<Editor>,
1976 window: &mut Window,
1977 cx: &mut Context<Self>,
1978 ) {
1979 if !self.active
1980 || !OutlinePanelSettings::get_global(cx).auto_reveal_entries
1981 || self.focus_handle.contains_focused(window, cx)
1982 {
1983 return;
1984 }
1985 let project = self.project.clone();
1986 self.reveal_selection_task = cx.spawn_in(window, async move |outline_panel, cx| {
1987 cx.background_executor().timer(UPDATE_DEBOUNCE).await;
1988 let entry_with_selection =
1989 outline_panel.update_in(cx, |outline_panel, window, cx| {
1990 outline_panel.location_for_editor_selection(&editor, window, cx)
1991 })?;
1992 let Some(entry_with_selection) = entry_with_selection else {
1993 outline_panel.update(cx, |outline_panel, cx| {
1994 outline_panel.selected_entry = SelectedEntry::None;
1995 cx.notify();
1996 })?;
1997 return Ok(());
1998 };
1999 let related_buffer_entry = match &entry_with_selection {
2000 PanelEntry::Fs(FsEntry::File(FsEntryFile {
2001 worktree_id,
2002 buffer_id,
2003 ..
2004 })) => project.update(cx, |project, cx| {
2005 let entry_id = project
2006 .buffer_for_id(*buffer_id, cx)
2007 .and_then(|buffer| buffer.read(cx).entry_id(cx));
2008 project
2009 .worktree_for_id(*worktree_id, cx)
2010 .zip(entry_id)
2011 .and_then(|(worktree, entry_id)| {
2012 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
2013 Some((worktree, entry))
2014 })
2015 })?,
2016 PanelEntry::Outline(outline_entry) => {
2017 let (buffer_id, excerpt_id) = outline_entry.ids();
2018 outline_panel.update(cx, |outline_panel, cx| {
2019 outline_panel
2020 .collapsed_entries
2021 .remove(&CollapsedEntry::ExternalFile(buffer_id));
2022 outline_panel
2023 .collapsed_entries
2024 .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
2025 let project = outline_panel.project.read(cx);
2026 let entry_id = project
2027 .buffer_for_id(buffer_id, cx)
2028 .and_then(|buffer| buffer.read(cx).entry_id(cx));
2029
2030 entry_id.and_then(|entry_id| {
2031 project
2032 .worktree_for_entry(entry_id, cx)
2033 .and_then(|worktree| {
2034 let worktree_id = worktree.read(cx).id();
2035 outline_panel
2036 .collapsed_entries
2037 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
2038 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
2039 Some((worktree, entry))
2040 })
2041 })
2042 })?
2043 }
2044 PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
2045 PanelEntry::Search(SearchEntry { match_range, .. }) => match_range
2046 .start
2047 .text_anchor
2048 .buffer_id
2049 .or(match_range.end.text_anchor.buffer_id)
2050 .map(|buffer_id| {
2051 outline_panel.update(cx, |outline_panel, cx| {
2052 outline_panel
2053 .collapsed_entries
2054 .remove(&CollapsedEntry::ExternalFile(buffer_id));
2055 let project = project.read(cx);
2056 let entry_id = project
2057 .buffer_for_id(buffer_id, cx)
2058 .and_then(|buffer| buffer.read(cx).entry_id(cx));
2059
2060 entry_id.and_then(|entry_id| {
2061 project
2062 .worktree_for_entry(entry_id, cx)
2063 .and_then(|worktree| {
2064 let worktree_id = worktree.read(cx).id();
2065 outline_panel
2066 .collapsed_entries
2067 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
2068 let entry =
2069 worktree.read(cx).entry_for_id(entry_id)?.clone();
2070 Some((worktree, entry))
2071 })
2072 })
2073 })
2074 })
2075 .transpose()?
2076 .flatten(),
2077 _ => return anyhow::Ok(()),
2078 };
2079 if let Some((worktree, buffer_entry)) = related_buffer_entry {
2080 outline_panel.update(cx, |outline_panel, cx| {
2081 let worktree_id = worktree.read(cx).id();
2082 let mut dirs_to_expand = Vec::new();
2083 {
2084 let mut traversal = worktree.read(cx).traverse_from_path(
2085 true,
2086 true,
2087 true,
2088 buffer_entry.path.as_ref(),
2089 );
2090 let mut current_entry = buffer_entry;
2091 loop {
2092 if current_entry.is_dir()
2093 && outline_panel
2094 .collapsed_entries
2095 .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
2096 {
2097 dirs_to_expand.push(current_entry.id);
2098 }
2099
2100 if traversal.back_to_parent()
2101 && let Some(parent_entry) = traversal.entry()
2102 {
2103 current_entry = parent_entry.clone();
2104 continue;
2105 }
2106 break;
2107 }
2108 }
2109 for dir_to_expand in dirs_to_expand {
2110 project
2111 .update(cx, |project, cx| {
2112 project.expand_entry(worktree_id, dir_to_expand, cx)
2113 })
2114 .unwrap_or_else(|| Task::ready(Ok(())))
2115 .detach_and_log_err(cx)
2116 }
2117 })?
2118 }
2119
2120 outline_panel.update_in(cx, |outline_panel, window, cx| {
2121 outline_panel.select_entry(entry_with_selection, false, window, cx);
2122 outline_panel.update_cached_entries(None, window, cx);
2123 })?;
2124
2125 anyhow::Ok(())
2126 });
2127 }
2128
2129 fn render_excerpt(
2130 &self,
2131 excerpt: &OutlineEntryExcerpt,
2132 depth: usize,
2133 window: &mut Window,
2134 cx: &mut Context<OutlinePanel>,
2135 ) -> Option<Stateful<Div>> {
2136 let item_id = ElementId::from(excerpt.id.to_proto() as usize);
2137 let is_active = match self.selected_entry() {
2138 Some(PanelEntry::Outline(OutlineEntry::Excerpt(selected_excerpt))) => {
2139 selected_excerpt.buffer_id == excerpt.buffer_id && selected_excerpt.id == excerpt.id
2140 }
2141 _ => false,
2142 };
2143 let has_outlines = self
2144 .excerpts
2145 .get(&excerpt.buffer_id)
2146 .and_then(|excerpts| match &excerpts.get(&excerpt.id)?.outlines {
2147 ExcerptOutlines::Outlines(outlines) => Some(outlines),
2148 ExcerptOutlines::Invalidated(outlines) => Some(outlines),
2149 ExcerptOutlines::NotFetched => None,
2150 })
2151 .is_some_and(|outlines| !outlines.is_empty());
2152 let is_expanded = !self
2153 .collapsed_entries
2154 .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id));
2155 let color = entry_label_color(is_active);
2156 let icon = if has_outlines {
2157 FileIcons::get_chevron_icon(is_expanded, cx)
2158 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2159 } else {
2160 None
2161 }
2162 .unwrap_or_else(empty_icon);
2163
2164 let label = self.excerpt_label(excerpt.buffer_id, &excerpt.range, cx)?;
2165 let label_element = Label::new(label)
2166 .single_line()
2167 .color(color)
2168 .into_any_element();
2169
2170 Some(self.entry_element(
2171 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())),
2172 item_id,
2173 depth,
2174 icon,
2175 is_active,
2176 label_element,
2177 window,
2178 cx,
2179 ))
2180 }
2181
2182 fn excerpt_label(
2183 &self,
2184 buffer_id: BufferId,
2185 range: &ExcerptRange<language::Anchor>,
2186 cx: &App,
2187 ) -> Option<String> {
2188 let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
2189 let excerpt_range = range.context.to_point(&buffer_snapshot);
2190 Some(format!(
2191 "Lines {}- {}",
2192 excerpt_range.start.row + 1,
2193 excerpt_range.end.row + 1,
2194 ))
2195 }
2196
2197 fn render_outline(
2198 &self,
2199 outline: &OutlineEntryOutline,
2200 depth: usize,
2201 string_match: Option<&StringMatch>,
2202 window: &mut Window,
2203 cx: &mut Context<Self>,
2204 ) -> Stateful<Div> {
2205 let item_id = ElementId::from(SharedString::from(format!(
2206 "{:?}|{:?}{:?}|{:?}",
2207 outline.buffer_id, outline.excerpt_id, outline.outline.range, &outline.outline.text,
2208 )));
2209
2210 let label_element = outline::render_item(
2211 &outline.outline,
2212 string_match
2213 .map(|string_match| string_match.ranges().collect::<Vec<_>>())
2214 .unwrap_or_default(),
2215 cx,
2216 )
2217 .into_any_element();
2218
2219 let is_active = match self.selected_entry() {
2220 Some(PanelEntry::Outline(OutlineEntry::Outline(selected))) => {
2221 outline == selected && outline.outline == selected.outline
2222 }
2223 _ => false,
2224 };
2225
2226 let has_children = self
2227 .outline_children_cache
2228 .get(&outline.buffer_id)
2229 .and_then(|children_map| {
2230 let key = (outline.outline.range.clone(), outline.outline.depth);
2231 children_map.get(&key)
2232 })
2233 .copied()
2234 .unwrap_or(false);
2235 let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Outline(
2236 outline.buffer_id,
2237 outline.excerpt_id,
2238 outline.outline.range.clone(),
2239 ));
2240
2241 let icon = if has_children {
2242 FileIcons::get_chevron_icon(is_expanded, cx)
2243 .map(|icon_path| {
2244 Icon::from_path(icon_path)
2245 .color(entry_label_color(is_active))
2246 .into_any_element()
2247 })
2248 .unwrap_or_else(empty_icon)
2249 } else {
2250 empty_icon()
2251 };
2252
2253 self.entry_element(
2254 PanelEntry::Outline(OutlineEntry::Outline(outline.clone())),
2255 item_id,
2256 depth,
2257 icon,
2258 is_active,
2259 label_element,
2260 window,
2261 cx,
2262 )
2263 }
2264
2265 fn render_entry(
2266 &self,
2267 rendered_entry: &FsEntry,
2268 depth: usize,
2269 string_match: Option<&StringMatch>,
2270 window: &mut Window,
2271 cx: &mut Context<Self>,
2272 ) -> Stateful<Div> {
2273 let settings = OutlinePanelSettings::get_global(cx);
2274 let is_active = match self.selected_entry() {
2275 Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry,
2276 _ => false,
2277 };
2278 let (item_id, label_element, icon) = match rendered_entry {
2279 FsEntry::File(FsEntryFile {
2280 worktree_id, entry, ..
2281 }) => {
2282 let name = self.entry_name(worktree_id, entry, cx);
2283 let color =
2284 entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
2285 let icon = if settings.file_icons {
2286 FileIcons::get_icon(entry.path.as_std_path(), cx)
2287 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2288 } else {
2289 None
2290 };
2291 (
2292 ElementId::from(entry.id.to_proto() as usize),
2293 HighlightedLabel::new(
2294 name,
2295 string_match
2296 .map(|string_match| string_match.positions.clone())
2297 .unwrap_or_default(),
2298 )
2299 .color(color)
2300 .into_any_element(),
2301 icon.unwrap_or_else(empty_icon),
2302 )
2303 }
2304 FsEntry::Directory(directory) => {
2305 let name = self.entry_name(&directory.worktree_id, &directory.entry, cx);
2306
2307 let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Dir(
2308 directory.worktree_id,
2309 directory.entry.id,
2310 ));
2311 let color = entry_git_aware_label_color(
2312 directory.entry.git_summary,
2313 directory.entry.is_ignored,
2314 is_active,
2315 );
2316 let icon = if settings.folder_icons {
2317 FileIcons::get_folder_icon(is_expanded, directory.entry.path.as_std_path(), cx)
2318 } else {
2319 FileIcons::get_chevron_icon(is_expanded, cx)
2320 }
2321 .map(Icon::from_path)
2322 .map(|icon| icon.color(color).into_any_element());
2323 (
2324 ElementId::from(directory.entry.id.to_proto() as usize),
2325 HighlightedLabel::new(
2326 name,
2327 string_match
2328 .map(|string_match| string_match.positions.clone())
2329 .unwrap_or_default(),
2330 )
2331 .color(color)
2332 .into_any_element(),
2333 icon.unwrap_or_else(empty_icon),
2334 )
2335 }
2336 FsEntry::ExternalFile(external_file) => {
2337 let color = entry_label_color(is_active);
2338 let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) {
2339 Some(buffer_snapshot) => match buffer_snapshot.file() {
2340 Some(file) => {
2341 let path = file.path();
2342 let icon = if settings.file_icons {
2343 FileIcons::get_icon(path.as_std_path(), cx)
2344 } else {
2345 None
2346 }
2347 .map(Icon::from_path)
2348 .map(|icon| icon.color(color).into_any_element());
2349 (icon, file_name(path.as_std_path()))
2350 }
2351 None => (None, "Untitled".to_string()),
2352 },
2353 None => (None, "Unknown buffer".to_string()),
2354 };
2355 (
2356 ElementId::from(external_file.buffer_id.to_proto() as usize),
2357 HighlightedLabel::new(
2358 name,
2359 string_match
2360 .map(|string_match| string_match.positions.clone())
2361 .unwrap_or_default(),
2362 )
2363 .color(color)
2364 .into_any_element(),
2365 icon.unwrap_or_else(empty_icon),
2366 )
2367 }
2368 };
2369
2370 self.entry_element(
2371 PanelEntry::Fs(rendered_entry.clone()),
2372 item_id,
2373 depth,
2374 icon,
2375 is_active,
2376 label_element,
2377 window,
2378 cx,
2379 )
2380 }
2381
2382 fn render_folded_dirs(
2383 &self,
2384 folded_dir: &FoldedDirsEntry,
2385 depth: usize,
2386 string_match: Option<&StringMatch>,
2387 window: &mut Window,
2388 cx: &mut Context<OutlinePanel>,
2389 ) -> Stateful<Div> {
2390 let settings = OutlinePanelSettings::get_global(cx);
2391 let is_active = match self.selected_entry() {
2392 Some(PanelEntry::FoldedDirs(selected_dirs)) => {
2393 selected_dirs.worktree_id == folded_dir.worktree_id
2394 && selected_dirs.entries == folded_dir.entries
2395 }
2396 _ => false,
2397 };
2398 let (item_id, label_element, icon) = {
2399 let name = self.dir_names_string(&folded_dir.entries, folded_dir.worktree_id, cx);
2400
2401 let is_expanded = folded_dir.entries.iter().all(|dir| {
2402 !self
2403 .collapsed_entries
2404 .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id))
2405 });
2406 let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored);
2407 let git_status = folded_dir
2408 .entries
2409 .first()
2410 .map(|entry| entry.git_summary)
2411 .unwrap_or_default();
2412 let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
2413 let icon = if settings.folder_icons {
2414 FileIcons::get_folder_icon(is_expanded, &Path::new(&name), cx)
2415 } else {
2416 FileIcons::get_chevron_icon(is_expanded, cx)
2417 }
2418 .map(Icon::from_path)
2419 .map(|icon| icon.color(color).into_any_element());
2420 (
2421 ElementId::from(
2422 folded_dir
2423 .entries
2424 .last()
2425 .map(|entry| entry.id.to_proto())
2426 .unwrap_or_else(|| folded_dir.worktree_id.to_proto())
2427 as usize,
2428 ),
2429 HighlightedLabel::new(
2430 name,
2431 string_match
2432 .map(|string_match| string_match.positions.clone())
2433 .unwrap_or_default(),
2434 )
2435 .color(color)
2436 .into_any_element(),
2437 icon.unwrap_or_else(empty_icon),
2438 )
2439 };
2440
2441 self.entry_element(
2442 PanelEntry::FoldedDirs(folded_dir.clone()),
2443 item_id,
2444 depth,
2445 icon,
2446 is_active,
2447 label_element,
2448 window,
2449 cx,
2450 )
2451 }
2452
2453 fn render_search_match(
2454 &mut self,
2455 multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
2456 match_range: &Range<editor::Anchor>,
2457 render_data: &Arc<OnceLock<SearchData>>,
2458 kind: SearchKind,
2459 depth: usize,
2460 string_match: Option<&StringMatch>,
2461 window: &mut Window,
2462 cx: &mut Context<Self>,
2463 ) -> Option<Stateful<Div>> {
2464 let search_data = match render_data.get() {
2465 Some(search_data) => search_data,
2466 None => {
2467 if let ItemsDisplayMode::Search(search_state) = &mut self.mode
2468 && let Some(multi_buffer_snapshot) = multi_buffer_snapshot
2469 {
2470 search_state
2471 .highlight_search_match_tx
2472 .try_send(HighlightArguments {
2473 multi_buffer_snapshot: multi_buffer_snapshot.clone(),
2474 match_range: match_range.clone(),
2475 search_data: Arc::clone(render_data),
2476 })
2477 .ok();
2478 }
2479 return None;
2480 }
2481 };
2482 let search_matches = string_match
2483 .iter()
2484 .flat_map(|string_match| string_match.ranges())
2485 .collect::<Vec<_>>();
2486 let match_ranges = if search_matches.is_empty() {
2487 &search_data.search_match_indices
2488 } else {
2489 &search_matches
2490 };
2491 let label_element = outline::render_item(
2492 &OutlineItem {
2493 depth,
2494 annotation_range: None,
2495 range: search_data.context_range.clone(),
2496 text: search_data.context_text.clone(),
2497 source_range_for_text: search_data.context_range.clone(),
2498 highlight_ranges: search_data
2499 .highlights_data
2500 .get()
2501 .cloned()
2502 .unwrap_or_default(),
2503 name_ranges: search_data.search_match_indices.clone(),
2504 body_range: Some(search_data.context_range.clone()),
2505 },
2506 match_ranges.iter().cloned(),
2507 cx,
2508 );
2509 let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
2510 let entire_label = h_flex()
2511 .justify_center()
2512 .p_0()
2513 .when(search_data.truncated_left, |parent| {
2514 parent.child(truncated_contents_label())
2515 })
2516 .child(label_element)
2517 .when(search_data.truncated_right, |parent| {
2518 parent.child(truncated_contents_label())
2519 })
2520 .into_any_element();
2521
2522 let is_active = match self.selected_entry() {
2523 Some(PanelEntry::Search(SearchEntry {
2524 match_range: selected_match_range,
2525 ..
2526 })) => match_range == selected_match_range,
2527 _ => false,
2528 };
2529 Some(self.entry_element(
2530 PanelEntry::Search(SearchEntry {
2531 kind,
2532 match_range: match_range.clone(),
2533 render_data: render_data.clone(),
2534 }),
2535 ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
2536 depth,
2537 empty_icon(),
2538 is_active,
2539 entire_label,
2540 window,
2541 cx,
2542 ))
2543 }
2544
2545 fn entry_element(
2546 &self,
2547 rendered_entry: PanelEntry,
2548 item_id: ElementId,
2549 depth: usize,
2550 icon_element: AnyElement,
2551 is_active: bool,
2552 label_element: gpui::AnyElement,
2553 window: &mut Window,
2554 cx: &mut Context<OutlinePanel>,
2555 ) -> Stateful<Div> {
2556 let settings = OutlinePanelSettings::get_global(cx);
2557 div()
2558 .text_ui(cx)
2559 .id(item_id.clone())
2560 .on_click({
2561 let clicked_entry = rendered_entry.clone();
2562 cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| {
2563 if event.is_right_click() || event.first_focus() {
2564 return;
2565 }
2566
2567 let change_focus = event.click_count() > 1;
2568 outline_panel.toggle_expanded(&clicked_entry, window, cx);
2569
2570 outline_panel.scroll_editor_to_entry(
2571 &clicked_entry,
2572 true,
2573 change_focus,
2574 window,
2575 cx,
2576 );
2577 })
2578 })
2579 .cursor_pointer()
2580 .child(
2581 ListItem::new(item_id)
2582 .indent_level(depth)
2583 .indent_step_size(px(settings.indent_size))
2584 .toggle_state(is_active)
2585 .child(
2586 h_flex()
2587 .child(h_flex().w(px(16.)).justify_center().child(icon_element))
2588 .child(h_flex().h_6().child(label_element).ml_1()),
2589 )
2590 .on_secondary_mouse_down(cx.listener(
2591 move |outline_panel, event: &MouseDownEvent, window, cx| {
2592 // Stop propagation to prevent the catch-all context menu for the project
2593 // panel from being deployed.
2594 cx.stop_propagation();
2595 outline_panel.deploy_context_menu(
2596 event.position,
2597 rendered_entry.clone(),
2598 window,
2599 cx,
2600 )
2601 },
2602 )),
2603 )
2604 .border_1()
2605 .border_r_2()
2606 .rounded_none()
2607 .hover(|style| {
2608 if is_active {
2609 style
2610 } else {
2611 let hover_color = cx.theme().colors().ghost_element_hover;
2612 style.bg(hover_color).border_color(hover_color)
2613 }
2614 })
2615 .when(
2616 is_active && self.focus_handle.contains_focused(window, cx),
2617 |div| div.border_color(Color::Selected.color(cx)),
2618 )
2619 }
2620
2621 fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String {
2622 match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2623 Some(worktree) => {
2624 let worktree = worktree.read(cx);
2625 match worktree.snapshot().root_entry() {
2626 Some(root_entry) => {
2627 if root_entry.id == entry.id {
2628 file_name(worktree.abs_path().as_ref())
2629 } else {
2630 let path = worktree.absolutize(entry.path.as_ref());
2631 file_name(&path)
2632 }
2633 }
2634 None => {
2635 let path = worktree.absolutize(entry.path.as_ref());
2636 file_name(&path)
2637 }
2638 }
2639 }
2640 None => file_name(entry.path.as_std_path()),
2641 }
2642 }
2643
2644 fn update_fs_entries(
2645 &mut self,
2646 active_editor: Entity<Editor>,
2647 debounce: Option<Duration>,
2648 window: &mut Window,
2649 cx: &mut Context<Self>,
2650 ) {
2651 if !self.active {
2652 return;
2653 }
2654
2655 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2656 let active_multi_buffer = active_editor.read(cx).buffer().clone();
2657 let new_entries = self.new_entries_for_fs_update.clone();
2658 let repo_snapshots = self.project.update(cx, |project, cx| {
2659 project.git_store().read(cx).repo_snapshots(cx)
2660 });
2661 self.updating_fs_entries = true;
2662 self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
2663 if let Some(debounce) = debounce {
2664 cx.background_executor().timer(debounce).await;
2665 }
2666
2667 let mut new_collapsed_entries = HashSet::default();
2668 let mut new_unfolded_dirs = HashMap::default();
2669 let mut root_entries = HashSet::default();
2670 let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
2671 let Ok(buffer_excerpts) = outline_panel.update(cx, |outline_panel, cx| {
2672 let git_store = outline_panel.project.read(cx).git_store().clone();
2673 new_collapsed_entries = outline_panel.collapsed_entries.clone();
2674 new_unfolded_dirs = outline_panel.unfolded_dirs.clone();
2675 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
2676
2677 multi_buffer_snapshot.excerpts().fold(
2678 HashMap::default(),
2679 |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
2680 let buffer_id = buffer_snapshot.remote_id();
2681 let file = File::from_dyn(buffer_snapshot.file());
2682 let entry_id = file.and_then(|file| file.project_entry_id());
2683 let worktree = file.map(|file| file.worktree.read(cx).snapshot());
2684 let is_new = new_entries.contains(&excerpt_id)
2685 || !outline_panel.excerpts.contains_key(&buffer_id);
2686 let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
2687 let status = git_store
2688 .read(cx)
2689 .repository_and_path_for_buffer_id(buffer_id, cx)
2690 .and_then(|(repo, path)| {
2691 Some(repo.read(cx).status_for_path(&path)?.status)
2692 });
2693 buffer_excerpts
2694 .entry(buffer_id)
2695 .or_insert_with(|| {
2696 (is_new, is_folded, Vec::new(), entry_id, worktree, status)
2697 })
2698 .2
2699 .push(excerpt_id);
2700
2701 let outlines = match outline_panel
2702 .excerpts
2703 .get(&buffer_id)
2704 .and_then(|excerpts| excerpts.get(&excerpt_id))
2705 {
2706 Some(old_excerpt) => match &old_excerpt.outlines {
2707 ExcerptOutlines::Outlines(outlines) => {
2708 ExcerptOutlines::Outlines(outlines.clone())
2709 }
2710 ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
2711 ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
2712 },
2713 None => ExcerptOutlines::NotFetched,
2714 };
2715 new_excerpts.entry(buffer_id).or_default().insert(
2716 excerpt_id,
2717 Excerpt {
2718 range: excerpt_range,
2719 outlines,
2720 },
2721 );
2722 buffer_excerpts
2723 },
2724 )
2725 }) else {
2726 return;
2727 };
2728
2729 let Some((
2730 new_collapsed_entries,
2731 new_unfolded_dirs,
2732 new_fs_entries,
2733 new_depth_map,
2734 new_children_count,
2735 )) = cx
2736 .background_spawn(async move {
2737 let mut processed_external_buffers = HashSet::default();
2738 let mut new_worktree_entries =
2739 BTreeMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
2740 let mut worktree_excerpts = HashMap::<
2741 WorktreeId,
2742 HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
2743 >::default();
2744 let mut external_excerpts = HashMap::default();
2745
2746 for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree, status)) in
2747 buffer_excerpts
2748 {
2749 if is_folded {
2750 match &worktree {
2751 Some(worktree) => {
2752 new_collapsed_entries
2753 .insert(CollapsedEntry::File(worktree.id(), buffer_id));
2754 }
2755 None => {
2756 new_collapsed_entries
2757 .insert(CollapsedEntry::ExternalFile(buffer_id));
2758 }
2759 }
2760 } else if is_new {
2761 match &worktree {
2762 Some(worktree) => {
2763 new_collapsed_entries
2764 .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
2765 }
2766 None => {
2767 new_collapsed_entries
2768 .remove(&CollapsedEntry::ExternalFile(buffer_id));
2769 }
2770 }
2771 }
2772
2773 if let Some(worktree) = worktree {
2774 let worktree_id = worktree.id();
2775 let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
2776
2777 match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
2778 Some(entry) => {
2779 let entry = GitEntry {
2780 git_summary: status
2781 .map(|status| status.summary())
2782 .unwrap_or_default(),
2783 entry,
2784 };
2785 let mut traversal = GitTraversal::new(
2786 &repo_snapshots,
2787 worktree.traverse_from_path(
2788 true,
2789 true,
2790 true,
2791 entry.path.as_ref(),
2792 ),
2793 );
2794
2795 let mut entries_to_add = HashMap::default();
2796 worktree_excerpts
2797 .entry(worktree_id)
2798 .or_default()
2799 .insert(entry.id, (buffer_id, excerpts));
2800 let mut current_entry = entry;
2801 loop {
2802 if current_entry.is_dir() {
2803 let is_root =
2804 worktree.root_entry().map(|entry| entry.id)
2805 == Some(current_entry.id);
2806 if is_root {
2807 root_entries.insert(current_entry.id);
2808 if auto_fold_dirs {
2809 unfolded_dirs.insert(current_entry.id);
2810 }
2811 }
2812 if is_new {
2813 new_collapsed_entries.remove(&CollapsedEntry::Dir(
2814 worktree_id,
2815 current_entry.id,
2816 ));
2817 }
2818 }
2819
2820 let new_entry_added = entries_to_add
2821 .insert(current_entry.id, current_entry)
2822 .is_none();
2823 if new_entry_added
2824 && traversal.back_to_parent()
2825 && let Some(parent_entry) = traversal.entry()
2826 {
2827 current_entry = parent_entry.to_owned();
2828 continue;
2829 }
2830 break;
2831 }
2832 new_worktree_entries
2833 .entry(worktree_id)
2834 .or_insert_with(HashMap::default)
2835 .extend(entries_to_add);
2836 }
2837 None => {
2838 if processed_external_buffers.insert(buffer_id) {
2839 external_excerpts
2840 .entry(buffer_id)
2841 .or_insert_with(Vec::new)
2842 .extend(excerpts);
2843 }
2844 }
2845 }
2846 } else if processed_external_buffers.insert(buffer_id) {
2847 external_excerpts
2848 .entry(buffer_id)
2849 .or_insert_with(Vec::new)
2850 .extend(excerpts);
2851 }
2852 }
2853
2854 let mut new_children_count =
2855 HashMap::<WorktreeId, HashMap<Arc<RelPath>, FsChildren>>::default();
2856
2857 let worktree_entries = new_worktree_entries
2858 .into_iter()
2859 .map(|(worktree_id, entries)| {
2860 let mut entries = entries.into_values().collect::<Vec<_>>();
2861 entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
2862 (worktree_id, entries)
2863 })
2864 .flat_map(|(worktree_id, entries)| {
2865 {
2866 entries
2867 .into_iter()
2868 .filter_map(|entry| {
2869 if auto_fold_dirs && let Some(parent) = entry.path.parent()
2870 {
2871 let children = new_children_count
2872 .entry(worktree_id)
2873 .or_default()
2874 .entry(Arc::from(parent))
2875 .or_default();
2876 if entry.is_dir() {
2877 children.dirs += 1;
2878 } else {
2879 children.files += 1;
2880 }
2881 }
2882
2883 if entry.is_dir() {
2884 Some(FsEntry::Directory(FsEntryDirectory {
2885 worktree_id,
2886 entry,
2887 }))
2888 } else {
2889 let (buffer_id, excerpts) = worktree_excerpts
2890 .get_mut(&worktree_id)
2891 .and_then(|worktree_excerpts| {
2892 worktree_excerpts.remove(&entry.id)
2893 })?;
2894 Some(FsEntry::File(FsEntryFile {
2895 worktree_id,
2896 buffer_id,
2897 entry,
2898 excerpts,
2899 }))
2900 }
2901 })
2902 .collect::<Vec<_>>()
2903 }
2904 })
2905 .collect::<Vec<_>>();
2906
2907 let mut visited_dirs = Vec::new();
2908 let mut new_depth_map = HashMap::default();
2909 let new_visible_entries = external_excerpts
2910 .into_iter()
2911 .sorted_by_key(|(id, _)| *id)
2912 .map(|(buffer_id, excerpts)| {
2913 FsEntry::ExternalFile(FsEntryExternalFile {
2914 buffer_id,
2915 excerpts,
2916 })
2917 })
2918 .chain(worktree_entries)
2919 .filter(|visible_item| {
2920 match visible_item {
2921 FsEntry::Directory(directory) => {
2922 let parent_id = back_to_common_visited_parent(
2923 &mut visited_dirs,
2924 &directory.worktree_id,
2925 &directory.entry,
2926 );
2927
2928 let mut depth = 0;
2929 if !root_entries.contains(&directory.entry.id) {
2930 if auto_fold_dirs {
2931 let children = new_children_count
2932 .get(&directory.worktree_id)
2933 .and_then(|children_count| {
2934 children_count.get(&directory.entry.path)
2935 })
2936 .copied()
2937 .unwrap_or_default();
2938
2939 if !children.may_be_fold_part()
2940 || (children.dirs == 0
2941 && visited_dirs
2942 .last()
2943 .map(|(parent_dir_id, _)| {
2944 new_unfolded_dirs
2945 .get(&directory.worktree_id)
2946 .is_none_or(|unfolded_dirs| {
2947 unfolded_dirs
2948 .contains(parent_dir_id)
2949 })
2950 })
2951 .unwrap_or(true))
2952 {
2953 new_unfolded_dirs
2954 .entry(directory.worktree_id)
2955 .or_default()
2956 .insert(directory.entry.id);
2957 }
2958 }
2959
2960 depth = parent_id
2961 .and_then(|(worktree_id, id)| {
2962 new_depth_map.get(&(worktree_id, id)).copied()
2963 })
2964 .unwrap_or(0)
2965 + 1;
2966 };
2967 visited_dirs
2968 .push((directory.entry.id, directory.entry.path.clone()));
2969 new_depth_map
2970 .insert((directory.worktree_id, directory.entry.id), depth);
2971 }
2972 FsEntry::File(FsEntryFile {
2973 worktree_id,
2974 entry: file_entry,
2975 ..
2976 }) => {
2977 let parent_id = back_to_common_visited_parent(
2978 &mut visited_dirs,
2979 worktree_id,
2980 file_entry,
2981 );
2982 let depth = if root_entries.contains(&file_entry.id) {
2983 0
2984 } else {
2985 parent_id
2986 .and_then(|(worktree_id, id)| {
2987 new_depth_map.get(&(worktree_id, id)).copied()
2988 })
2989 .unwrap_or(0)
2990 + 1
2991 };
2992 new_depth_map.insert((*worktree_id, file_entry.id), depth);
2993 }
2994 FsEntry::ExternalFile(..) => {
2995 visited_dirs.clear();
2996 }
2997 }
2998
2999 true
3000 })
3001 .collect::<Vec<_>>();
3002
3003 anyhow::Ok((
3004 new_collapsed_entries,
3005 new_unfolded_dirs,
3006 new_visible_entries,
3007 new_depth_map,
3008 new_children_count,
3009 ))
3010 })
3011 .await
3012 .log_err()
3013 else {
3014 return;
3015 };
3016
3017 outline_panel
3018 .update_in(cx, |outline_panel, window, cx| {
3019 outline_panel.updating_fs_entries = false;
3020 outline_panel.new_entries_for_fs_update.clear();
3021 outline_panel.excerpts = new_excerpts;
3022 outline_panel.collapsed_entries = new_collapsed_entries;
3023 outline_panel.unfolded_dirs = new_unfolded_dirs;
3024 outline_panel.fs_entries = new_fs_entries;
3025 outline_panel.fs_entries_depth = new_depth_map;
3026 outline_panel.fs_children_count = new_children_count;
3027 outline_panel.update_non_fs_items(window, cx);
3028
3029 // Only update cached entries if we don't have outlines to fetch
3030 // If we do have outlines to fetch, let fetch_outdated_outlines handle the update
3031 if outline_panel.excerpt_fetch_ranges(cx).is_empty() {
3032 outline_panel.update_cached_entries(debounce, window, cx);
3033 }
3034
3035 cx.notify();
3036 })
3037 .ok();
3038 });
3039 }
3040
3041 fn replace_active_editor(
3042 &mut self,
3043 new_active_item: Box<dyn ItemHandle>,
3044 new_active_editor: Entity<Editor>,
3045 window: &mut Window,
3046 cx: &mut Context<Self>,
3047 ) {
3048 self.clear_previous(window, cx);
3049
3050 let default_expansion_depth =
3051 OutlinePanelSettings::get_global(cx).expand_outlines_with_depth;
3052 // We'll apply the expansion depth after outlines are loaded
3053 self.pending_default_expansion_depth = Some(default_expansion_depth);
3054
3055 let buffer_search_subscription = cx.subscribe_in(
3056 &new_active_editor,
3057 window,
3058 |outline_panel: &mut Self,
3059 _,
3060 e: &SearchEvent,
3061 window: &mut Window,
3062 cx: &mut Context<Self>| {
3063 if matches!(e, SearchEvent::MatchesInvalidated) {
3064 let update_cached_items = outline_panel.update_search_matches(window, cx);
3065 if update_cached_items {
3066 outline_panel.selected_entry.invalidate();
3067 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
3068 }
3069 };
3070 outline_panel.autoscroll(cx);
3071 },
3072 );
3073 self.active_item = Some(ActiveItem {
3074 _buffer_search_subscription: buffer_search_subscription,
3075 _editor_subscription: subscribe_for_editor_events(&new_active_editor, window, cx),
3076 item_handle: new_active_item.downgrade_item(),
3077 active_editor: new_active_editor.downgrade(),
3078 });
3079 self.new_entries_for_fs_update
3080 .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
3081 self.selected_entry.invalidate();
3082 self.update_fs_entries(new_active_editor, None, window, cx);
3083 }
3084
3085 fn clear_previous(&mut self, window: &mut Window, cx: &mut App) {
3086 self.fs_entries_update_task = Task::ready(());
3087 self.outline_fetch_tasks.clear();
3088 self.cached_entries_update_task = Task::ready(());
3089 self.reveal_selection_task = Task::ready(Ok(()));
3090 self.filter_editor
3091 .update(cx, |editor, cx| editor.clear(window, cx));
3092 self.collapsed_entries.clear();
3093 self.unfolded_dirs.clear();
3094 self.active_item = None;
3095 self.fs_entries.clear();
3096 self.fs_entries_depth.clear();
3097 self.fs_children_count.clear();
3098 self.excerpts.clear();
3099 self.cached_entries = Vec::new();
3100 self.selected_entry = SelectedEntry::None;
3101 self.pinned = false;
3102 self.mode = ItemsDisplayMode::Outline;
3103 self.pending_default_expansion_depth = None;
3104 }
3105
3106 fn location_for_editor_selection(
3107 &self,
3108 editor: &Entity<Editor>,
3109 window: &mut Window,
3110 cx: &mut Context<Self>,
3111 ) -> Option<PanelEntry> {
3112 let selection = editor.update(cx, |editor, cx| {
3113 editor
3114 .selections
3115 .newest::<language::Point>(&editor.display_snapshot(cx))
3116 .head()
3117 });
3118 let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
3119 let multi_buffer = editor.read(cx).buffer();
3120 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
3121 let (excerpt_id, buffer, _) = editor
3122 .read(cx)
3123 .buffer()
3124 .read(cx)
3125 .excerpt_containing(selection, cx)?;
3126 let buffer_id = buffer.read(cx).remote_id();
3127
3128 if editor.read(cx).is_buffer_folded(buffer_id, cx) {
3129 return self
3130 .fs_entries
3131 .iter()
3132 .find(|fs_entry| match fs_entry {
3133 FsEntry::Directory(..) => false,
3134 FsEntry::File(FsEntryFile {
3135 buffer_id: other_buffer_id,
3136 ..
3137 })
3138 | FsEntry::ExternalFile(FsEntryExternalFile {
3139 buffer_id: other_buffer_id,
3140 ..
3141 }) => buffer_id == *other_buffer_id,
3142 })
3143 .cloned()
3144 .map(PanelEntry::Fs);
3145 }
3146
3147 let selection_display_point = selection.to_display_point(&editor_snapshot);
3148
3149 match &self.mode {
3150 ItemsDisplayMode::Search(search_state) => search_state
3151 .matches
3152 .iter()
3153 .rev()
3154 .min_by_key(|&(match_range, _)| {
3155 let match_display_range =
3156 match_range.clone().to_display_points(&editor_snapshot);
3157 let start_distance = if selection_display_point < match_display_range.start {
3158 match_display_range.start - selection_display_point
3159 } else {
3160 selection_display_point - match_display_range.start
3161 };
3162 let end_distance = if selection_display_point < match_display_range.end {
3163 match_display_range.end - selection_display_point
3164 } else {
3165 selection_display_point - match_display_range.end
3166 };
3167 start_distance + end_distance
3168 })
3169 .and_then(|(closest_range, _)| {
3170 self.cached_entries.iter().find_map(|cached_entry| {
3171 if let PanelEntry::Search(SearchEntry { match_range, .. }) =
3172 &cached_entry.entry
3173 {
3174 if match_range == closest_range {
3175 Some(cached_entry.entry.clone())
3176 } else {
3177 None
3178 }
3179 } else {
3180 None
3181 }
3182 })
3183 }),
3184 ItemsDisplayMode::Outline => self.outline_location(
3185 buffer_id,
3186 excerpt_id,
3187 multi_buffer_snapshot,
3188 editor_snapshot,
3189 selection_display_point,
3190 ),
3191 }
3192 }
3193
3194 fn outline_location(
3195 &self,
3196 buffer_id: BufferId,
3197 excerpt_id: ExcerptId,
3198 multi_buffer_snapshot: editor::MultiBufferSnapshot,
3199 editor_snapshot: editor::EditorSnapshot,
3200 selection_display_point: DisplayPoint,
3201 ) -> Option<PanelEntry> {
3202 let excerpt_outlines = self
3203 .excerpts
3204 .get(&buffer_id)
3205 .and_then(|excerpts| excerpts.get(&excerpt_id))
3206 .into_iter()
3207 .flat_map(|excerpt| excerpt.iter_outlines())
3208 .flat_map(|outline| {
3209 let range = multi_buffer_snapshot
3210 .anchor_range_in_excerpt(excerpt_id, outline.range.clone())?;
3211 Some((
3212 range.start.to_display_point(&editor_snapshot)
3213 ..range.end.to_display_point(&editor_snapshot),
3214 outline,
3215 ))
3216 })
3217 .collect::<Vec<_>>();
3218
3219 let mut matching_outline_indices = Vec::new();
3220 let mut children = HashMap::default();
3221 let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
3222
3223 for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
3224 if outline_range
3225 .to_inclusive()
3226 .contains(&selection_display_point)
3227 {
3228 matching_outline_indices.push(i);
3229 } else if (outline_range.start.row()..outline_range.end.row())
3230 .to_inclusive()
3231 .contains(&selection_display_point.row())
3232 {
3233 matching_outline_indices.push(i);
3234 }
3235
3236 while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
3237 if parent_outline.depth >= outline.depth
3238 || !parent_range.contains(&outline_range.start)
3239 {
3240 parents_stack.pop();
3241 } else {
3242 break;
3243 }
3244 }
3245 if let Some((_, _, parent_index)) = parents_stack.last_mut() {
3246 children
3247 .entry(*parent_index)
3248 .or_insert_with(Vec::new)
3249 .push(i);
3250 }
3251 parents_stack.push((outline_range, outline, i));
3252 }
3253
3254 let outline_item = matching_outline_indices
3255 .into_iter()
3256 .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
3257 .filter(|(i, _)| {
3258 children
3259 .get(i)
3260 .map(|children| {
3261 children.iter().all(|child_index| {
3262 excerpt_outlines
3263 .get(*child_index)
3264 .map(|(child_range, _)| child_range.start > selection_display_point)
3265 .unwrap_or(false)
3266 })
3267 })
3268 .unwrap_or(true)
3269 })
3270 .min_by_key(|(_, (outline_range, outline))| {
3271 let distance_from_start = if outline_range.start > selection_display_point {
3272 outline_range.start - selection_display_point
3273 } else {
3274 selection_display_point - outline_range.start
3275 };
3276 let distance_from_end = if outline_range.end > selection_display_point {
3277 outline_range.end - selection_display_point
3278 } else {
3279 selection_display_point - outline_range.end
3280 };
3281
3282 (
3283 cmp::Reverse(outline.depth),
3284 distance_from_start + distance_from_end,
3285 )
3286 })
3287 .map(|(_, (_, outline))| *outline)
3288 .cloned();
3289
3290 let closest_container = match outline_item {
3291 Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
3292 buffer_id,
3293 excerpt_id,
3294 outline,
3295 })),
3296 None => {
3297 self.cached_entries.iter().rev().find_map(|cached_entry| {
3298 match &cached_entry.entry {
3299 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
3300 if excerpt.buffer_id == buffer_id && excerpt.id == excerpt_id {
3301 Some(cached_entry.entry.clone())
3302 } else {
3303 None
3304 }
3305 }
3306 PanelEntry::Fs(
3307 FsEntry::ExternalFile(FsEntryExternalFile {
3308 buffer_id: file_buffer_id,
3309 excerpts: file_excerpts,
3310 })
3311 | FsEntry::File(FsEntryFile {
3312 buffer_id: file_buffer_id,
3313 excerpts: file_excerpts,
3314 ..
3315 }),
3316 ) => {
3317 if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
3318 Some(cached_entry.entry.clone())
3319 } else {
3320 None
3321 }
3322 }
3323 _ => None,
3324 }
3325 })?
3326 }
3327 };
3328 Some(closest_container)
3329 }
3330
3331 fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3332 let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
3333 if excerpt_fetch_ranges.is_empty() {
3334 return;
3335 }
3336
3337 let syntax_theme = cx.theme().syntax().clone();
3338 let first_update = Arc::new(AtomicBool::new(true));
3339 for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
3340 for (excerpt_id, excerpt_range) in excerpt_ranges {
3341 let syntax_theme = syntax_theme.clone();
3342 let buffer_snapshot = buffer_snapshot.clone();
3343 let first_update = first_update.clone();
3344 self.outline_fetch_tasks.insert(
3345 (buffer_id, excerpt_id),
3346 cx.spawn_in(window, async move |outline_panel, cx| {
3347 let buffer_language = buffer_snapshot.language().cloned();
3348 let fetched_outlines = cx
3349 .background_spawn(async move {
3350 let mut outlines = buffer_snapshot.outline_items_containing(
3351 excerpt_range.context,
3352 false,
3353 Some(&syntax_theme),
3354 );
3355 outlines.retain(|outline| {
3356 buffer_language.is_none()
3357 || buffer_language.as_ref()
3358 == buffer_snapshot.language_at(outline.range.start)
3359 });
3360
3361 let outlines_with_children = outlines
3362 .windows(2)
3363 .filter_map(|window| {
3364 let current = &window[0];
3365 let next = &window[1];
3366 if next.depth > current.depth {
3367 Some((current.range.clone(), current.depth))
3368 } else {
3369 None
3370 }
3371 })
3372 .collect::<HashSet<_>>();
3373
3374 (outlines, outlines_with_children)
3375 })
3376 .await;
3377
3378 let (fetched_outlines, outlines_with_children) = fetched_outlines;
3379
3380 outline_panel
3381 .update_in(cx, |outline_panel, window, cx| {
3382 let pending_default_depth =
3383 outline_panel.pending_default_expansion_depth.take();
3384
3385 let debounce =
3386 if first_update.fetch_and(false, atomic::Ordering::AcqRel) {
3387 None
3388 } else {
3389 Some(UPDATE_DEBOUNCE)
3390 };
3391
3392 if let Some(excerpt) = outline_panel
3393 .excerpts
3394 .entry(buffer_id)
3395 .or_default()
3396 .get_mut(&excerpt_id)
3397 {
3398 excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
3399
3400 if let Some(default_depth) = pending_default_depth
3401 && let ExcerptOutlines::Outlines(outlines) =
3402 &excerpt.outlines
3403 {
3404 outlines
3405 .iter()
3406 .filter(|outline| {
3407 (default_depth == 0
3408 || outline.depth >= default_depth)
3409 && outlines_with_children.contains(&(
3410 outline.range.clone(),
3411 outline.depth,
3412 ))
3413 })
3414 .for_each(|outline| {
3415 outline_panel.collapsed_entries.insert(
3416 CollapsedEntry::Outline(
3417 buffer_id,
3418 excerpt_id,
3419 outline.range.clone(),
3420 ),
3421 );
3422 });
3423 }
3424
3425 // Even if no outlines to check, we still need to update cached entries
3426 // to show the outline entries that were just fetched
3427 outline_panel.update_cached_entries(debounce, window, cx);
3428 }
3429 })
3430 .ok();
3431 }),
3432 );
3433 }
3434 }
3435 }
3436
3437 fn is_singleton_active(&self, cx: &App) -> bool {
3438 self.active_editor()
3439 .is_some_and(|active_editor| active_editor.read(cx).buffer().read(cx).is_singleton())
3440 }
3441
3442 fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
3443 self.outline_fetch_tasks.clear();
3444 let mut ids = ids.iter().collect::<HashSet<_>>();
3445 for excerpts in self.excerpts.values_mut() {
3446 ids.retain(|id| {
3447 if let Some(excerpt) = excerpts.get_mut(id) {
3448 excerpt.invalidate_outlines();
3449 false
3450 } else {
3451 true
3452 }
3453 });
3454 if ids.is_empty() {
3455 break;
3456 }
3457 }
3458 }
3459
3460 fn excerpt_fetch_ranges(
3461 &self,
3462 cx: &App,
3463 ) -> HashMap<
3464 BufferId,
3465 (
3466 BufferSnapshot,
3467 HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
3468 ),
3469 > {
3470 self.fs_entries
3471 .iter()
3472 .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
3473 match fs_entry {
3474 FsEntry::File(FsEntryFile {
3475 buffer_id,
3476 excerpts: file_excerpts,
3477 ..
3478 })
3479 | FsEntry::ExternalFile(FsEntryExternalFile {
3480 buffer_id,
3481 excerpts: file_excerpts,
3482 }) => {
3483 let excerpts = self.excerpts.get(buffer_id);
3484 for &file_excerpt in file_excerpts {
3485 if let Some(excerpt) = excerpts
3486 .and_then(|excerpts| excerpts.get(&file_excerpt))
3487 .filter(|excerpt| excerpt.should_fetch_outlines())
3488 {
3489 match excerpts_to_fetch.entry(*buffer_id) {
3490 hash_map::Entry::Occupied(mut o) => {
3491 o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
3492 }
3493 hash_map::Entry::Vacant(v) => {
3494 if let Some(buffer_snapshot) =
3495 self.buffer_snapshot_for_id(*buffer_id, cx)
3496 {
3497 v.insert((buffer_snapshot, HashMap::default()))
3498 .1
3499 .insert(file_excerpt, excerpt.range.clone());
3500 }
3501 }
3502 }
3503 }
3504 }
3505 }
3506 FsEntry::Directory(..) => {}
3507 }
3508 excerpts_to_fetch
3509 })
3510 }
3511
3512 fn buffer_snapshot_for_id(&self, buffer_id: BufferId, cx: &App) -> Option<BufferSnapshot> {
3513 let editor = self.active_editor()?;
3514 Some(
3515 editor
3516 .read(cx)
3517 .buffer()
3518 .read(cx)
3519 .buffer(buffer_id)?
3520 .read(cx)
3521 .snapshot(),
3522 )
3523 }
3524
3525 fn abs_path(&self, entry: &PanelEntry, cx: &App) -> Option<PathBuf> {
3526 match entry {
3527 PanelEntry::Fs(
3528 FsEntry::File(FsEntryFile { buffer_id, .. })
3529 | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }),
3530 ) => self
3531 .buffer_snapshot_for_id(*buffer_id, cx)
3532 .and_then(|buffer_snapshot| {
3533 let file = File::from_dyn(buffer_snapshot.file())?;
3534 Some(file.worktree.read(cx).absolutize(&file.path))
3535 }),
3536 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3537 worktree_id, entry, ..
3538 })) => Some(
3539 self.project
3540 .read(cx)
3541 .worktree_for_id(*worktree_id, cx)?
3542 .read(cx)
3543 .absolutize(&entry.path),
3544 ),
3545 PanelEntry::FoldedDirs(FoldedDirsEntry {
3546 worktree_id,
3547 entries: dirs,
3548 ..
3549 }) => dirs.last().and_then(|entry| {
3550 self.project
3551 .read(cx)
3552 .worktree_for_id(*worktree_id, cx)
3553 .map(|worktree| worktree.read(cx).absolutize(&entry.path))
3554 }),
3555 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
3556 }
3557 }
3558
3559 fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<RelPath>> {
3560 match entry {
3561 FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3562 let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
3563 Some(buffer_snapshot.file()?.path().clone())
3564 }
3565 FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()),
3566 FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()),
3567 }
3568 }
3569
3570 fn update_cached_entries(
3571 &mut self,
3572 debounce: Option<Duration>,
3573 window: &mut Window,
3574 cx: &mut Context<OutlinePanel>,
3575 ) {
3576 if !self.active {
3577 return;
3578 }
3579
3580 let is_singleton = self.is_singleton_active(cx);
3581 let query = self.query(cx);
3582 self.updating_cached_entries = true;
3583 self.cached_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
3584 if let Some(debounce) = debounce {
3585 cx.background_executor().timer(debounce).await;
3586 }
3587 let Some(new_cached_entries) = outline_panel
3588 .update_in(cx, |outline_panel, window, cx| {
3589 outline_panel.generate_cached_entries(is_singleton, query, window, cx)
3590 })
3591 .ok()
3592 else {
3593 return;
3594 };
3595 let (new_cached_entries, max_width_item_index) = new_cached_entries.await;
3596 outline_panel
3597 .update_in(cx, |outline_panel, window, cx| {
3598 outline_panel.cached_entries = new_cached_entries;
3599 outline_panel.max_width_item_index = max_width_item_index;
3600 if (outline_panel.selected_entry.is_invalidated()
3601 || matches!(outline_panel.selected_entry, SelectedEntry::None))
3602 && let Some(new_selected_entry) =
3603 outline_panel.active_editor().and_then(|active_editor| {
3604 outline_panel.location_for_editor_selection(
3605 &active_editor,
3606 window,
3607 cx,
3608 )
3609 })
3610 {
3611 outline_panel.select_entry(new_selected_entry, false, window, cx);
3612 }
3613
3614 outline_panel.autoscroll(cx);
3615 outline_panel.updating_cached_entries = false;
3616 cx.notify();
3617 })
3618 .ok();
3619 });
3620 }
3621
3622 fn generate_cached_entries(
3623 &self,
3624 is_singleton: bool,
3625 query: Option<String>,
3626 window: &mut Window,
3627 cx: &mut Context<Self>,
3628 ) -> Task<(Vec<CachedEntry>, Option<usize>)> {
3629 let project = self.project.clone();
3630 let Some(active_editor) = self.active_editor() else {
3631 return Task::ready((Vec::new(), None));
3632 };
3633 cx.spawn_in(window, async move |outline_panel, cx| {
3634 let mut generation_state = GenerationState::default();
3635
3636 let Ok(()) = outline_panel.update(cx, |outline_panel, cx| {
3637 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
3638 let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>;
3639 let track_matches = query.is_some();
3640
3641 #[derive(Debug)]
3642 struct ParentStats {
3643 path: Arc<RelPath>,
3644 folded: bool,
3645 expanded: bool,
3646 depth: usize,
3647 }
3648 let mut parent_dirs = Vec::<ParentStats>::new();
3649 for entry in outline_panel.fs_entries.clone() {
3650 let is_expanded = outline_panel.is_expanded(&entry);
3651 let (depth, should_add) = match &entry {
3652 FsEntry::Directory(directory_entry) => {
3653 let mut should_add = true;
3654 let is_root = project
3655 .read(cx)
3656 .worktree_for_id(directory_entry.worktree_id, cx)
3657 .is_some_and(|worktree| {
3658 worktree.read(cx).root_entry() == Some(&directory_entry.entry)
3659 });
3660 let folded = auto_fold_dirs
3661 && !is_root
3662 && outline_panel
3663 .unfolded_dirs
3664 .get(&directory_entry.worktree_id)
3665 .is_none_or(|unfolded_dirs| {
3666 !unfolded_dirs.contains(&directory_entry.entry.id)
3667 });
3668 let fs_depth = outline_panel
3669 .fs_entries_depth
3670 .get(&(directory_entry.worktree_id, directory_entry.entry.id))
3671 .copied()
3672 .unwrap_or(0);
3673 while let Some(parent) = parent_dirs.last() {
3674 if !is_root && directory_entry.entry.path.starts_with(&parent.path)
3675 {
3676 break;
3677 }
3678 parent_dirs.pop();
3679 }
3680 let auto_fold = match parent_dirs.last() {
3681 Some(parent) => {
3682 parent.folded
3683 && Some(parent.path.as_ref())
3684 == directory_entry.entry.path.parent()
3685 && outline_panel
3686 .fs_children_count
3687 .get(&directory_entry.worktree_id)
3688 .and_then(|entries| {
3689 entries.get(&directory_entry.entry.path)
3690 })
3691 .copied()
3692 .unwrap_or_default()
3693 .may_be_fold_part()
3694 }
3695 None => false,
3696 };
3697 let folded = folded || auto_fold;
3698 let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
3699 Some(parent) => {
3700 let parent_folded = parent.folded;
3701 let parent_expanded = parent.expanded;
3702 let new_depth = if parent_folded {
3703 parent.depth
3704 } else {
3705 parent.depth + 1
3706 };
3707 parent_dirs.push(ParentStats {
3708 path: directory_entry.entry.path.clone(),
3709 folded,
3710 expanded: parent_expanded && is_expanded,
3711 depth: new_depth,
3712 });
3713 (new_depth, parent_expanded, parent_folded)
3714 }
3715 None => {
3716 parent_dirs.push(ParentStats {
3717 path: directory_entry.entry.path.clone(),
3718 folded,
3719 expanded: is_expanded,
3720 depth: fs_depth,
3721 });
3722 (fs_depth, true, false)
3723 }
3724 };
3725
3726 if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take()
3727 {
3728 if folded
3729 && directory_entry.worktree_id == folded_dirs.worktree_id
3730 && directory_entry.entry.path.parent()
3731 == folded_dirs
3732 .entries
3733 .last()
3734 .map(|entry| entry.path.as_ref())
3735 {
3736 folded_dirs.entries.push(directory_entry.entry.clone());
3737 folded_dirs_entry = Some((folded_depth, folded_dirs))
3738 } else {
3739 if !is_singleton {
3740 let start_of_collapsed_dir_sequence = !parent_expanded
3741 && parent_dirs
3742 .iter()
3743 .rev()
3744 .nth(folded_dirs.entries.len() + 1)
3745 .is_none_or(|parent| parent.expanded);
3746 if start_of_collapsed_dir_sequence
3747 || parent_expanded
3748 || query.is_some()
3749 {
3750 if parent_folded {
3751 folded_dirs
3752 .entries
3753 .push(directory_entry.entry.clone());
3754 should_add = false;
3755 }
3756 let new_folded_dirs =
3757 PanelEntry::FoldedDirs(folded_dirs.clone());
3758 outline_panel.push_entry(
3759 &mut generation_state,
3760 track_matches,
3761 new_folded_dirs,
3762 folded_depth,
3763 cx,
3764 );
3765 }
3766 }
3767
3768 folded_dirs_entry = if parent_folded {
3769 None
3770 } else {
3771 Some((
3772 depth,
3773 FoldedDirsEntry {
3774 worktree_id: directory_entry.worktree_id,
3775 entries: vec![directory_entry.entry.clone()],
3776 },
3777 ))
3778 };
3779 }
3780 } else if folded {
3781 folded_dirs_entry = Some((
3782 depth,
3783 FoldedDirsEntry {
3784 worktree_id: directory_entry.worktree_id,
3785 entries: vec![directory_entry.entry.clone()],
3786 },
3787 ));
3788 }
3789
3790 let should_add =
3791 should_add && parent_expanded && folded_dirs_entry.is_none();
3792 (depth, should_add)
3793 }
3794 FsEntry::ExternalFile(..) => {
3795 if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() {
3796 let parent_expanded = parent_dirs
3797 .iter()
3798 .rev()
3799 .find(|parent| {
3800 folded_dir
3801 .entries
3802 .iter()
3803 .all(|entry| entry.path != parent.path)
3804 })
3805 .is_none_or(|parent| parent.expanded);
3806 if !is_singleton && (parent_expanded || query.is_some()) {
3807 outline_panel.push_entry(
3808 &mut generation_state,
3809 track_matches,
3810 PanelEntry::FoldedDirs(folded_dir),
3811 folded_depth,
3812 cx,
3813 );
3814 }
3815 }
3816 parent_dirs.clear();
3817 (0, true)
3818 }
3819 FsEntry::File(file) => {
3820 if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3821 let parent_expanded = parent_dirs
3822 .iter()
3823 .rev()
3824 .find(|parent| {
3825 folded_dirs
3826 .entries
3827 .iter()
3828 .all(|entry| entry.path != parent.path)
3829 })
3830 .is_none_or(|parent| parent.expanded);
3831 if !is_singleton && (parent_expanded || query.is_some()) {
3832 outline_panel.push_entry(
3833 &mut generation_state,
3834 track_matches,
3835 PanelEntry::FoldedDirs(folded_dirs),
3836 folded_depth,
3837 cx,
3838 );
3839 }
3840 }
3841
3842 let fs_depth = outline_panel
3843 .fs_entries_depth
3844 .get(&(file.worktree_id, file.entry.id))
3845 .copied()
3846 .unwrap_or(0);
3847 while let Some(parent) = parent_dirs.last() {
3848 if file.entry.path.starts_with(&parent.path) {
3849 break;
3850 }
3851 parent_dirs.pop();
3852 }
3853 match parent_dirs.last() {
3854 Some(parent) => {
3855 let new_depth = parent.depth + 1;
3856 (new_depth, parent.expanded)
3857 }
3858 None => (fs_depth, true),
3859 }
3860 }
3861 };
3862
3863 if !is_singleton
3864 && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3865 {
3866 outline_panel.push_entry(
3867 &mut generation_state,
3868 track_matches,
3869 PanelEntry::Fs(entry.clone()),
3870 depth,
3871 cx,
3872 );
3873 }
3874
3875 match outline_panel.mode {
3876 ItemsDisplayMode::Search(_) => {
3877 if is_singleton || query.is_some() || (should_add && is_expanded) {
3878 outline_panel.add_search_entries(
3879 &mut generation_state,
3880 &active_editor,
3881 entry.clone(),
3882 depth,
3883 query.clone(),
3884 is_singleton,
3885 cx,
3886 );
3887 }
3888 }
3889 ItemsDisplayMode::Outline => {
3890 let excerpts_to_consider =
3891 if is_singleton || query.is_some() || (should_add && is_expanded) {
3892 match &entry {
3893 FsEntry::File(FsEntryFile {
3894 buffer_id,
3895 excerpts,
3896 ..
3897 })
3898 | FsEntry::ExternalFile(FsEntryExternalFile {
3899 buffer_id,
3900 excerpts,
3901 ..
3902 }) => Some((*buffer_id, excerpts)),
3903 _ => None,
3904 }
3905 } else {
3906 None
3907 };
3908 if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider
3909 && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
3910 {
3911 outline_panel.add_excerpt_entries(
3912 &mut generation_state,
3913 buffer_id,
3914 entry_excerpts,
3915 depth,
3916 track_matches,
3917 is_singleton,
3918 query.as_deref(),
3919 cx,
3920 );
3921 }
3922 }
3923 }
3924
3925 if is_singleton
3926 && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
3927 && !generation_state.entries.iter().any(|item| {
3928 matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
3929 })
3930 {
3931 outline_panel.push_entry(
3932 &mut generation_state,
3933 track_matches,
3934 PanelEntry::Fs(entry.clone()),
3935 0,
3936 cx,
3937 );
3938 }
3939 }
3940
3941 if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3942 let parent_expanded = parent_dirs
3943 .iter()
3944 .rev()
3945 .find(|parent| {
3946 folded_dirs
3947 .entries
3948 .iter()
3949 .all(|entry| entry.path != parent.path)
3950 })
3951 .is_none_or(|parent| parent.expanded);
3952 if parent_expanded || query.is_some() {
3953 outline_panel.push_entry(
3954 &mut generation_state,
3955 track_matches,
3956 PanelEntry::FoldedDirs(folded_dirs),
3957 folded_depth,
3958 cx,
3959 );
3960 }
3961 }
3962 }) else {
3963 return (Vec::new(), None);
3964 };
3965
3966 let Some(query) = query else {
3967 return (
3968 generation_state.entries,
3969 generation_state
3970 .max_width_estimate_and_index
3971 .map(|(_, index)| index),
3972 );
3973 };
3974
3975 let mut matched_ids = match_strings(
3976 &generation_state.match_candidates,
3977 &query,
3978 true,
3979 true,
3980 usize::MAX,
3981 &AtomicBool::default(),
3982 cx.background_executor().clone(),
3983 )
3984 .await
3985 .into_iter()
3986 .map(|string_match| (string_match.candidate_id, string_match))
3987 .collect::<HashMap<_, _>>();
3988
3989 let mut id = 0;
3990 generation_state.entries.retain_mut(|cached_entry| {
3991 let retain = match matched_ids.remove(&id) {
3992 Some(string_match) => {
3993 cached_entry.string_match = Some(string_match);
3994 true
3995 }
3996 None => false,
3997 };
3998 id += 1;
3999 retain
4000 });
4001
4002 (
4003 generation_state.entries,
4004 generation_state
4005 .max_width_estimate_and_index
4006 .map(|(_, index)| index),
4007 )
4008 })
4009 }
4010
4011 fn push_entry(
4012 &self,
4013 state: &mut GenerationState,
4014 track_matches: bool,
4015 entry: PanelEntry,
4016 depth: usize,
4017 cx: &mut App,
4018 ) {
4019 let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry {
4020 match folded_dirs_entry.entries.len() {
4021 0 => {
4022 debug_panic!("Empty folded dirs receiver");
4023 return;
4024 }
4025 1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
4026 worktree_id: folded_dirs_entry.worktree_id,
4027 entry: folded_dirs_entry.entries[0].clone(),
4028 })),
4029 _ => entry,
4030 }
4031 } else {
4032 entry
4033 };
4034
4035 if track_matches {
4036 let id = state.entries.len();
4037 match &entry {
4038 PanelEntry::Fs(fs_entry) => {
4039 if let Some(file_name) = self
4040 .relative_path(fs_entry, cx)
4041 .and_then(|path| Some(path.file_name()?.to_string()))
4042 {
4043 state
4044 .match_candidates
4045 .push(StringMatchCandidate::new(id, &file_name));
4046 }
4047 }
4048 PanelEntry::FoldedDirs(folded_dir_entry) => {
4049 let dir_names = self.dir_names_string(
4050 &folded_dir_entry.entries,
4051 folded_dir_entry.worktree_id,
4052 cx,
4053 );
4054 {
4055 state
4056 .match_candidates
4057 .push(StringMatchCandidate::new(id, &dir_names));
4058 }
4059 }
4060 PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state
4061 .match_candidates
4062 .push(StringMatchCandidate::new(id, &outline_entry.outline.text)),
4063 PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {}
4064 PanelEntry::Search(new_search_entry) => {
4065 if let Some(search_data) = new_search_entry.render_data.get() {
4066 state
4067 .match_candidates
4068 .push(StringMatchCandidate::new(id, &search_data.context_text));
4069 }
4070 }
4071 }
4072 }
4073
4074 let width_estimate = self.width_estimate(depth, &entry, cx);
4075 if Some(width_estimate)
4076 > state
4077 .max_width_estimate_and_index
4078 .map(|(estimate, _)| estimate)
4079 {
4080 state.max_width_estimate_and_index = Some((width_estimate, state.entries.len()));
4081 }
4082 state.entries.push(CachedEntry {
4083 depth,
4084 entry,
4085 string_match: None,
4086 });
4087 }
4088
4089 fn dir_names_string(&self, entries: &[GitEntry], worktree_id: WorktreeId, cx: &App) -> String {
4090 let dir_names_segment = entries
4091 .iter()
4092 .map(|entry| self.entry_name(&worktree_id, entry, cx))
4093 .collect::<PathBuf>();
4094 dir_names_segment.to_string_lossy().into_owned()
4095 }
4096
4097 fn query(&self, cx: &App) -> Option<String> {
4098 let query = self.filter_editor.read(cx).text(cx);
4099 if query.trim().is_empty() {
4100 None
4101 } else {
4102 Some(query)
4103 }
4104 }
4105
4106 fn is_expanded(&self, entry: &FsEntry) -> bool {
4107 let entry_to_check = match entry {
4108 FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
4109 CollapsedEntry::ExternalFile(*buffer_id)
4110 }
4111 FsEntry::File(FsEntryFile {
4112 worktree_id,
4113 buffer_id,
4114 ..
4115 }) => CollapsedEntry::File(*worktree_id, *buffer_id),
4116 FsEntry::Directory(FsEntryDirectory {
4117 worktree_id, entry, ..
4118 }) => CollapsedEntry::Dir(*worktree_id, entry.id),
4119 };
4120 !self.collapsed_entries.contains(&entry_to_check)
4121 }
4122
4123 fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) -> bool {
4124 if !self.active {
4125 return false;
4126 }
4127
4128 let mut update_cached_items = false;
4129 update_cached_items |= self.update_search_matches(window, cx);
4130 self.fetch_outdated_outlines(window, cx);
4131 if update_cached_items {
4132 self.selected_entry.invalidate();
4133 }
4134 update_cached_items
4135 }
4136
4137 fn update_search_matches(
4138 &mut self,
4139 window: &mut Window,
4140 cx: &mut Context<OutlinePanel>,
4141 ) -> bool {
4142 if !self.active {
4143 return false;
4144 }
4145
4146 let project_search = self
4147 .active_item()
4148 .and_then(|item| item.downcast::<ProjectSearchView>());
4149 let project_search_matches = project_search
4150 .as_ref()
4151 .map(|project_search| project_search.read(cx).get_matches(cx))
4152 .unwrap_or_default();
4153
4154 let buffer_search = self
4155 .active_item()
4156 .as_deref()
4157 .and_then(|active_item| {
4158 self.workspace
4159 .upgrade()
4160 .and_then(|workspace| workspace.read(cx).pane_for(active_item))
4161 })
4162 .and_then(|pane| {
4163 pane.read(cx)
4164 .toolbar()
4165 .read(cx)
4166 .item_of_type::<BufferSearchBar>()
4167 });
4168 let buffer_search_matches = self
4169 .active_editor()
4170 .map(|active_editor| {
4171 active_editor.update(cx, |editor, cx| editor.get_matches(window, cx))
4172 })
4173 .unwrap_or_default();
4174
4175 let mut update_cached_entries = false;
4176 if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
4177 if matches!(self.mode, ItemsDisplayMode::Search(_)) {
4178 self.mode = ItemsDisplayMode::Outline;
4179 update_cached_entries = true;
4180 }
4181 } else {
4182 let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
4183 (
4184 SearchKind::Project,
4185 project_search_matches,
4186 project_search
4187 .map(|project_search| project_search.read(cx).search_query_text(cx))
4188 .unwrap_or_default(),
4189 )
4190 } else {
4191 (
4192 SearchKind::Buffer,
4193 buffer_search_matches,
4194 buffer_search
4195 .map(|buffer_search| buffer_search.read(cx).query(cx))
4196 .unwrap_or_default(),
4197 )
4198 };
4199
4200 let mut previous_matches = HashMap::default();
4201 update_cached_entries = match &mut self.mode {
4202 ItemsDisplayMode::Search(current_search_state) => {
4203 let update = current_search_state.query != new_search_query
4204 || current_search_state.kind != kind
4205 || current_search_state.matches.is_empty()
4206 || current_search_state.matches.iter().enumerate().any(
4207 |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
4208 );
4209 if current_search_state.kind == kind {
4210 previous_matches.extend(current_search_state.matches.drain(..));
4211 }
4212 update
4213 }
4214 ItemsDisplayMode::Outline => true,
4215 };
4216 self.mode = ItemsDisplayMode::Search(SearchState::new(
4217 kind,
4218 new_search_query,
4219 previous_matches,
4220 new_search_matches,
4221 cx.theme().syntax().clone(),
4222 window,
4223 cx,
4224 ));
4225 }
4226 update_cached_entries
4227 }
4228
4229 fn add_excerpt_entries(
4230 &mut self,
4231 state: &mut GenerationState,
4232 buffer_id: BufferId,
4233 entries_to_add: &[ExcerptId],
4234 parent_depth: usize,
4235 track_matches: bool,
4236 is_singleton: bool,
4237 query: Option<&str>,
4238 cx: &mut Context<Self>,
4239 ) {
4240 if let Some(excerpts) = self.excerpts.get(&buffer_id) {
4241 let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx);
4242
4243 for &excerpt_id in entries_to_add {
4244 let Some(excerpt) = excerpts.get(&excerpt_id) else {
4245 continue;
4246 };
4247 let excerpt_depth = parent_depth + 1;
4248 self.push_entry(
4249 state,
4250 track_matches,
4251 PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
4252 buffer_id,
4253 id: excerpt_id,
4254 range: excerpt.range.clone(),
4255 })),
4256 excerpt_depth,
4257 cx,
4258 );
4259
4260 let mut outline_base_depth = excerpt_depth + 1;
4261 if is_singleton {
4262 outline_base_depth = 0;
4263 state.clear();
4264 } else if query.is_none()
4265 && self
4266 .collapsed_entries
4267 .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
4268 {
4269 continue;
4270 }
4271
4272 let mut last_depth_at_level: Vec<Option<Range<Anchor>>> = vec![None; 10];
4273
4274 let all_outlines: Vec<_> = excerpt.iter_outlines().collect();
4275
4276 let mut outline_has_children = HashMap::default();
4277 let mut visible_outlines = Vec::new();
4278 let mut collapsed_state: Option<(usize, Range<Anchor>)> = None;
4279
4280 for (i, &outline) in all_outlines.iter().enumerate() {
4281 let has_children = all_outlines
4282 .get(i + 1)
4283 .map(|next| next.depth > outline.depth)
4284 .unwrap_or(false);
4285
4286 outline_has_children
4287 .insert((outline.range.clone(), outline.depth), has_children);
4288
4289 let mut should_include = true;
4290
4291 if let Some((collapsed_depth, collapsed_range)) = &collapsed_state {
4292 if outline.depth <= *collapsed_depth {
4293 collapsed_state = None;
4294 } else if let Some(buffer_snapshot) = buffer_snapshot.as_ref() {
4295 let outline_start = outline.range.start;
4296 if outline_start
4297 .cmp(&collapsed_range.start, buffer_snapshot)
4298 .is_ge()
4299 && outline_start
4300 .cmp(&collapsed_range.end, buffer_snapshot)
4301 .is_lt()
4302 {
4303 should_include = false; // Skip - inside collapsed range
4304 } else {
4305 collapsed_state = None;
4306 }
4307 }
4308 }
4309
4310 // Check if this outline itself is collapsed
4311 if should_include
4312 && self.collapsed_entries.contains(&CollapsedEntry::Outline(
4313 buffer_id,
4314 excerpt_id,
4315 outline.range.clone(),
4316 ))
4317 {
4318 collapsed_state = Some((outline.depth, outline.range.clone()));
4319 }
4320
4321 if should_include {
4322 visible_outlines.push(outline);
4323 }
4324 }
4325
4326 self.outline_children_cache
4327 .entry(buffer_id)
4328 .or_default()
4329 .extend(outline_has_children);
4330
4331 for outline in visible_outlines {
4332 let outline_entry = OutlineEntryOutline {
4333 buffer_id,
4334 excerpt_id,
4335 outline: outline.clone(),
4336 };
4337
4338 if outline.depth < last_depth_at_level.len() {
4339 last_depth_at_level[outline.depth] = Some(outline.range.clone());
4340 // Clear deeper levels when we go back to a shallower depth
4341 for d in (outline.depth + 1)..last_depth_at_level.len() {
4342 last_depth_at_level[d] = None;
4343 }
4344 }
4345
4346 self.push_entry(
4347 state,
4348 track_matches,
4349 PanelEntry::Outline(OutlineEntry::Outline(outline_entry)),
4350 outline_base_depth + outline.depth,
4351 cx,
4352 );
4353 }
4354 }
4355 }
4356 }
4357
4358 fn add_search_entries(
4359 &mut self,
4360 state: &mut GenerationState,
4361 active_editor: &Entity<Editor>,
4362 parent_entry: FsEntry,
4363 parent_depth: usize,
4364 filter_query: Option<String>,
4365 is_singleton: bool,
4366 cx: &mut Context<Self>,
4367 ) {
4368 let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
4369 return;
4370 };
4371
4372 let kind = search_state.kind;
4373 let related_excerpts = match &parent_entry {
4374 FsEntry::Directory(_) => return,
4375 FsEntry::ExternalFile(external) => &external.excerpts,
4376 FsEntry::File(file) => &file.excerpts,
4377 }
4378 .iter()
4379 .copied()
4380 .collect::<HashSet<_>>();
4381
4382 let depth = if is_singleton { 0 } else { parent_depth + 1 };
4383 let new_search_matches = search_state
4384 .matches
4385 .iter()
4386 .filter(|(match_range, _)| {
4387 related_excerpts.contains(&match_range.start.excerpt_id)
4388 || related_excerpts.contains(&match_range.end.excerpt_id)
4389 })
4390 .filter(|(match_range, _)| {
4391 let editor = active_editor.read(cx);
4392 let snapshot = editor.buffer().read(cx).snapshot(cx);
4393 if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.start)
4394 && editor.is_buffer_folded(buffer_id, cx)
4395 {
4396 return false;
4397 }
4398 if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.end)
4399 && editor.is_buffer_folded(buffer_id, cx)
4400 {
4401 return false;
4402 }
4403 true
4404 });
4405
4406 let new_search_entries = new_search_matches
4407 .map(|(match_range, search_data)| SearchEntry {
4408 match_range: match_range.clone(),
4409 kind,
4410 render_data: Arc::clone(search_data),
4411 })
4412 .collect::<Vec<_>>();
4413 for new_search_entry in new_search_entries {
4414 self.push_entry(
4415 state,
4416 filter_query.is_some(),
4417 PanelEntry::Search(new_search_entry),
4418 depth,
4419 cx,
4420 );
4421 }
4422 }
4423
4424 fn active_editor(&self) -> Option<Entity<Editor>> {
4425 self.active_item.as_ref()?.active_editor.upgrade()
4426 }
4427
4428 fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
4429 self.active_item.as_ref()?.item_handle.upgrade()
4430 }
4431
4432 fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
4433 self.active_item().is_none_or(|active_item| {
4434 !self.pinned && active_item.item_id() != new_active_item.item_id()
4435 })
4436 }
4437
4438 pub fn toggle_active_editor_pin(
4439 &mut self,
4440 _: &ToggleActiveEditorPin,
4441 window: &mut Window,
4442 cx: &mut Context<Self>,
4443 ) {
4444 self.pinned = !self.pinned;
4445 if !self.pinned
4446 && let Some((active_item, active_editor)) = self
4447 .workspace
4448 .upgrade()
4449 .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
4450 && self.should_replace_active_item(active_item.as_ref())
4451 {
4452 self.replace_active_editor(active_item, active_editor, window, cx);
4453 }
4454
4455 cx.notify();
4456 }
4457
4458 fn selected_entry(&self) -> Option<&PanelEntry> {
4459 match &self.selected_entry {
4460 SelectedEntry::Invalidated(entry) => entry.as_ref(),
4461 SelectedEntry::Valid(entry, _) => Some(entry),
4462 SelectedEntry::None => None,
4463 }
4464 }
4465
4466 fn select_entry(
4467 &mut self,
4468 entry: PanelEntry,
4469 focus: bool,
4470 window: &mut Window,
4471 cx: &mut Context<Self>,
4472 ) {
4473 if focus {
4474 self.focus_handle.focus(window);
4475 }
4476 let ix = self
4477 .cached_entries
4478 .iter()
4479 .enumerate()
4480 .find(|(_, cached_entry)| &cached_entry.entry == &entry)
4481 .map(|(i, _)| i)
4482 .unwrap_or_default();
4483
4484 self.selected_entry = SelectedEntry::Valid(entry, ix);
4485
4486 self.autoscroll(cx);
4487 cx.notify();
4488 }
4489
4490 fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
4491 let item_text_chars = match entry {
4492 PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
4493 .buffer_snapshot_for_id(external.buffer_id, cx)
4494 .and_then(|snapshot| Some(snapshot.file()?.path().file_name()?.len()))
4495 .unwrap_or_default(),
4496 PanelEntry::Fs(FsEntry::Directory(directory)) => directory
4497 .entry
4498 .path
4499 .file_name()
4500 .map(|name| name.len())
4501 .unwrap_or_default(),
4502 PanelEntry::Fs(FsEntry::File(file)) => file
4503 .entry
4504 .path
4505 .file_name()
4506 .map(|name| name.len())
4507 .unwrap_or_default(),
4508 PanelEntry::FoldedDirs(folded_dirs) => {
4509 folded_dirs
4510 .entries
4511 .iter()
4512 .map(|dir| {
4513 dir.path
4514 .file_name()
4515 .map(|name| name.len())
4516 .unwrap_or_default()
4517 })
4518 .sum::<usize>()
4519 + folded_dirs.entries.len().saturating_sub(1) * "/".len()
4520 }
4521 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
4522 .excerpt_label(excerpt.buffer_id, &excerpt.range, cx)
4523 .map(|label| label.len())
4524 .unwrap_or_default(),
4525 PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(),
4526 PanelEntry::Search(search) => search
4527 .render_data
4528 .get()
4529 .map(|data| data.context_text.len())
4530 .unwrap_or_default(),
4531 };
4532
4533 (item_text_chars + depth) as u64
4534 }
4535
4536 fn render_main_contents(
4537 &mut self,
4538 query: Option<String>,
4539 show_indent_guides: bool,
4540 indent_size: f32,
4541 window: &mut Window,
4542 cx: &mut Context<Self>,
4543 ) -> impl IntoElement {
4544 let contents = if self.cached_entries.is_empty() {
4545 let header = if self.updating_fs_entries || self.updating_cached_entries {
4546 None
4547 } else if query.is_some() {
4548 Some("No matches for query")
4549 } else {
4550 Some("No outlines available")
4551 };
4552
4553 v_flex()
4554 .id("empty-outline-state")
4555 .gap_0p5()
4556 .flex_1()
4557 .justify_center()
4558 .size_full()
4559 .when_some(header, |panel, header| {
4560 panel
4561 .child(h_flex().justify_center().child(Label::new(header)))
4562 .when_some(query.clone(), |panel, query| {
4563 panel.child(
4564 h_flex()
4565 .px_0p5()
4566 .justify_center()
4567 .bg(cx.theme().colors().element_selected.opacity(0.2))
4568 .child(Label::new(query)),
4569 )
4570 })
4571 .child(h_flex().justify_center().child({
4572 let keystroke = match self.position(window, cx) {
4573 DockPosition::Left => {
4574 window.keystroke_text_for(&workspace::ToggleLeftDock)
4575 }
4576 DockPosition::Bottom => {
4577 window.keystroke_text_for(&workspace::ToggleBottomDock)
4578 }
4579 DockPosition::Right => {
4580 window.keystroke_text_for(&workspace::ToggleRightDock)
4581 }
4582 };
4583 Label::new(format!("Toggle Panel With {keystroke}")).color(Color::Muted)
4584 }))
4585 })
4586 } else {
4587 let list_contents = {
4588 let items_len = self.cached_entries.len();
4589 let multi_buffer_snapshot = self
4590 .active_editor()
4591 .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
4592 uniform_list(
4593 "entries",
4594 items_len,
4595 cx.processor(move |outline_panel, range: Range<usize>, window, cx| {
4596 let entries = outline_panel.cached_entries.get(range);
4597 entries
4598 .map(|entries| entries.to_vec())
4599 .unwrap_or_default()
4600 .into_iter()
4601 .filter_map(|cached_entry| match cached_entry.entry {
4602 PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4603 &entry,
4604 cached_entry.depth,
4605 cached_entry.string_match.as_ref(),
4606 window,
4607 cx,
4608 )),
4609 PanelEntry::FoldedDirs(folded_dirs_entry) => {
4610 Some(outline_panel.render_folded_dirs(
4611 &folded_dirs_entry,
4612 cached_entry.depth,
4613 cached_entry.string_match.as_ref(),
4614 window,
4615 cx,
4616 ))
4617 }
4618 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4619 outline_panel.render_excerpt(
4620 &excerpt,
4621 cached_entry.depth,
4622 window,
4623 cx,
4624 )
4625 }
4626 PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4627 Some(outline_panel.render_outline(
4628 &entry,
4629 cached_entry.depth,
4630 cached_entry.string_match.as_ref(),
4631 window,
4632 cx,
4633 ))
4634 }
4635 PanelEntry::Search(SearchEntry {
4636 match_range,
4637 render_data,
4638 kind,
4639 ..
4640 }) => outline_panel.render_search_match(
4641 multi_buffer_snapshot.as_ref(),
4642 &match_range,
4643 &render_data,
4644 kind,
4645 cached_entry.depth,
4646 cached_entry.string_match.as_ref(),
4647 window,
4648 cx,
4649 ),
4650 })
4651 .collect()
4652 }),
4653 )
4654 .with_sizing_behavior(ListSizingBehavior::Infer)
4655 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4656 .with_width_from_item(self.max_width_item_index)
4657 .track_scroll(self.scroll_handle.clone())
4658 .when(show_indent_guides, |list| {
4659 list.with_decoration(
4660 ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
4661 .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| {
4662 let entries = outline_panel.cached_entries.get(range);
4663 if let Some(entries) = entries {
4664 entries.iter().map(|item| item.depth).collect()
4665 } else {
4666 smallvec::SmallVec::new()
4667 }
4668 })
4669 .with_render_fn(cx.entity(), move |outline_panel, params, _, _| {
4670 const LEFT_OFFSET: Pixels = px(14.);
4671
4672 let indent_size = params.indent_size;
4673 let item_height = params.item_height;
4674 let active_indent_guide_ix = find_active_indent_guide_ix(
4675 outline_panel,
4676 ¶ms.indent_guides,
4677 );
4678
4679 params
4680 .indent_guides
4681 .into_iter()
4682 .enumerate()
4683 .map(|(ix, layout)| {
4684 let bounds = Bounds::new(
4685 point(
4686 layout.offset.x * indent_size + LEFT_OFFSET,
4687 layout.offset.y * item_height,
4688 ),
4689 size(px(1.), layout.length * item_height),
4690 );
4691 ui::RenderedIndentGuide {
4692 bounds,
4693 layout,
4694 is_active: active_indent_guide_ix == Some(ix),
4695 hitbox: None,
4696 }
4697 })
4698 .collect()
4699 }),
4700 )
4701 })
4702 };
4703
4704 v_flex()
4705 .flex_shrink()
4706 .size_full()
4707 .child(list_contents.size_full().flex_shrink())
4708 .custom_scrollbars(
4709 Scrollbars::for_settings::<OutlinePanelSettings>()
4710 .tracked_scroll_handle(self.scroll_handle.clone())
4711 .with_track_along(
4712 ScrollAxes::Horizontal,
4713 cx.theme().colors().panel_background,
4714 )
4715 .tracked_entity(cx.entity_id()),
4716 window,
4717 cx,
4718 )
4719 }
4720 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4721 deferred(
4722 anchored()
4723 .position(*position)
4724 .anchor(gpui::Corner::TopLeft)
4725 .child(menu.clone()),
4726 )
4727 .with_priority(1)
4728 }));
4729
4730 v_flex().w_full().flex_1().overflow_hidden().child(contents)
4731 }
4732
4733 fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4734 let (icon, icon_tooltip) = if pinned {
4735 (IconName::Unpin, "Unpin Outline")
4736 } else {
4737 (IconName::Pin, "Pin Active Outline")
4738 };
4739
4740 h_flex()
4741 .p_2()
4742 .h(Tab::container_height(cx))
4743 .justify_between()
4744 .border_b_1()
4745 .border_color(cx.theme().colors().border)
4746 .child(
4747 h_flex()
4748 .w_full()
4749 .gap_1p5()
4750 .child(
4751 Icon::new(IconName::MagnifyingGlass)
4752 .size(IconSize::Small)
4753 .color(Color::Muted),
4754 )
4755 .child(self.filter_editor.clone()),
4756 )
4757 .child(
4758 IconButton::new("pin_button", icon)
4759 .tooltip(Tooltip::text(icon_tooltip))
4760 .shape(IconButtonShape::Square)
4761 .on_click(cx.listener(|outline_panel, _, window, cx| {
4762 outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, window, cx);
4763 })),
4764 )
4765 }
4766
4767 fn buffers_inside_directory(
4768 &self,
4769 dir_worktree: WorktreeId,
4770 dir_entry: &GitEntry,
4771 ) -> HashSet<BufferId> {
4772 if !dir_entry.is_dir() {
4773 debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4774 return HashSet::default();
4775 }
4776
4777 self.fs_entries
4778 .iter()
4779 .skip_while(|fs_entry| match fs_entry {
4780 FsEntry::Directory(directory) => {
4781 directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4782 }
4783 _ => true,
4784 })
4785 .skip(1)
4786 .take_while(|fs_entry| match fs_entry {
4787 FsEntry::ExternalFile(..) => false,
4788 FsEntry::Directory(directory) => {
4789 directory.worktree_id == dir_worktree
4790 && directory.entry.path.starts_with(&dir_entry.path)
4791 }
4792 FsEntry::File(file) => {
4793 file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4794 }
4795 })
4796 .filter_map(|fs_entry| match fs_entry {
4797 FsEntry::File(file) => Some(file.buffer_id),
4798 _ => None,
4799 })
4800 .collect()
4801 }
4802}
4803
4804fn workspace_active_editor(
4805 workspace: &Workspace,
4806 cx: &App,
4807) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4808 let active_item = workspace.active_item(cx)?;
4809 let active_editor = active_item
4810 .act_as::<Editor>(cx)
4811 .filter(|editor| editor.read(cx).mode().is_full())?;
4812 Some((active_item, active_editor))
4813}
4814
4815fn back_to_common_visited_parent(
4816 visited_dirs: &mut Vec<(ProjectEntryId, Arc<RelPath>)>,
4817 worktree_id: &WorktreeId,
4818 new_entry: &Entry,
4819) -> Option<(WorktreeId, ProjectEntryId)> {
4820 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4821 match new_entry.path.parent() {
4822 Some(parent_path) => {
4823 if parent_path == visited_path.as_ref() {
4824 return Some((*worktree_id, *visited_dir_id));
4825 }
4826 }
4827 None => {
4828 break;
4829 }
4830 }
4831 visited_dirs.pop();
4832 }
4833 None
4834}
4835
4836fn file_name(path: &Path) -> String {
4837 let mut current_path = path;
4838 loop {
4839 if let Some(file_name) = current_path.file_name() {
4840 return file_name.to_string_lossy().into_owned();
4841 }
4842 match current_path.parent() {
4843 Some(parent) => current_path = parent,
4844 None => return path.to_string_lossy().into_owned(),
4845 }
4846 }
4847}
4848
4849impl Panel for OutlinePanel {
4850 fn persistent_name() -> &'static str {
4851 "Outline Panel"
4852 }
4853
4854 fn panel_key() -> &'static str {
4855 OUTLINE_PANEL_KEY
4856 }
4857
4858 fn position(&self, _: &Window, cx: &App) -> DockPosition {
4859 match OutlinePanelSettings::get_global(cx).dock {
4860 DockSide::Left => DockPosition::Left,
4861 DockSide::Right => DockPosition::Right,
4862 }
4863 }
4864
4865 fn position_is_valid(&self, position: DockPosition) -> bool {
4866 matches!(position, DockPosition::Left | DockPosition::Right)
4867 }
4868
4869 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4870 settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
4871 let dock = match position {
4872 DockPosition::Left | DockPosition::Bottom => DockSide::Left,
4873 DockPosition::Right => DockSide::Right,
4874 };
4875 settings.outline_panel.get_or_insert_default().dock = Some(dock);
4876 });
4877 }
4878
4879 fn size(&self, _: &Window, cx: &App) -> Pixels {
4880 self.width
4881 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
4882 }
4883
4884 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
4885 self.width = size;
4886 cx.notify();
4887 cx.defer_in(window, |this, _, cx| {
4888 this.serialize(cx);
4889 });
4890 }
4891
4892 fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4893 OutlinePanelSettings::get_global(cx)
4894 .button
4895 .then_some(IconName::ListTree)
4896 }
4897
4898 fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
4899 Some("Outline Panel")
4900 }
4901
4902 fn toggle_action(&self) -> Box<dyn Action> {
4903 Box::new(ToggleFocus)
4904 }
4905
4906 fn starts_open(&self, _window: &Window, _: &App) -> bool {
4907 self.active
4908 }
4909
4910 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
4911 cx.spawn_in(window, async move |outline_panel, cx| {
4912 outline_panel
4913 .update_in(cx, |outline_panel, window, cx| {
4914 let old_active = outline_panel.active;
4915 outline_panel.active = active;
4916 if old_active != active {
4917 if active
4918 && let Some((active_item, active_editor)) =
4919 outline_panel.workspace.upgrade().and_then(|workspace| {
4920 workspace_active_editor(workspace.read(cx), cx)
4921 })
4922 {
4923 if outline_panel.should_replace_active_item(active_item.as_ref()) {
4924 outline_panel.replace_active_editor(
4925 active_item,
4926 active_editor,
4927 window,
4928 cx,
4929 );
4930 } else {
4931 outline_panel.update_fs_entries(active_editor, None, window, cx)
4932 }
4933 return;
4934 }
4935
4936 if !outline_panel.pinned {
4937 outline_panel.clear_previous(window, cx);
4938 }
4939 }
4940 outline_panel.serialize(cx);
4941 })
4942 .ok();
4943 })
4944 .detach()
4945 }
4946
4947 fn activation_priority(&self) -> u32 {
4948 5
4949 }
4950}
4951
4952impl Focusable for OutlinePanel {
4953 fn focus_handle(&self, cx: &App) -> FocusHandle {
4954 self.filter_editor.focus_handle(cx)
4955 }
4956}
4957
4958impl EventEmitter<Event> for OutlinePanel {}
4959
4960impl EventEmitter<PanelEvent> for OutlinePanel {}
4961
4962impl Render for OutlinePanel {
4963 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4964 let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| {
4965 (project.is_local(), project.is_via_remote_server())
4966 });
4967 let query = self.query(cx);
4968 let pinned = self.pinned;
4969 let settings = OutlinePanelSettings::get_global(cx);
4970 let indent_size = settings.indent_size;
4971 let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
4972
4973 let search_query = match &self.mode {
4974 ItemsDisplayMode::Search(search_query) => Some(search_query),
4975 _ => None,
4976 };
4977
4978 let search_query_text = search_query.map(|sq| sq.query.to_string());
4979
4980 v_flex()
4981 .id("outline-panel")
4982 .size_full()
4983 .overflow_hidden()
4984 .relative()
4985 .key_context(self.dispatch_context(window, cx))
4986 .on_action(cx.listener(Self::open_selected_entry))
4987 .on_action(cx.listener(Self::cancel))
4988 .on_action(cx.listener(Self::select_next))
4989 .on_action(cx.listener(Self::select_previous))
4990 .on_action(cx.listener(Self::select_first))
4991 .on_action(cx.listener(Self::select_last))
4992 .on_action(cx.listener(Self::select_parent))
4993 .on_action(cx.listener(Self::expand_selected_entry))
4994 .on_action(cx.listener(Self::collapse_selected_entry))
4995 .on_action(cx.listener(Self::expand_all_entries))
4996 .on_action(cx.listener(Self::collapse_all_entries))
4997 .on_action(cx.listener(Self::copy_path))
4998 .on_action(cx.listener(Self::copy_relative_path))
4999 .on_action(cx.listener(Self::toggle_active_editor_pin))
5000 .on_action(cx.listener(Self::unfold_directory))
5001 .on_action(cx.listener(Self::fold_directory))
5002 .on_action(cx.listener(Self::open_excerpts))
5003 .on_action(cx.listener(Self::open_excerpts_split))
5004 .when(is_local, |el| {
5005 el.on_action(cx.listener(Self::reveal_in_finder))
5006 })
5007 .when(is_local || is_via_ssh, |el| {
5008 el.on_action(cx.listener(Self::open_in_terminal))
5009 })
5010 .on_mouse_down(
5011 MouseButton::Right,
5012 cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
5013 if let Some(entry) = outline_panel.selected_entry().cloned() {
5014 outline_panel.deploy_context_menu(event.position, entry, window, cx)
5015 } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
5016 outline_panel.deploy_context_menu(
5017 event.position,
5018 PanelEntry::Fs(entry),
5019 window,
5020 cx,
5021 )
5022 }
5023 }),
5024 )
5025 .track_focus(&self.focus_handle)
5026 .child(self.render_filter_footer(pinned, cx))
5027 .when_some(search_query_text, |outline_panel, query_text| {
5028 outline_panel.child(
5029 h_flex()
5030 .py_1p5()
5031 .px_2()
5032 .h(Tab::container_height(cx))
5033 .gap_0p5()
5034 .border_b_1()
5035 .border_color(cx.theme().colors().border_variant)
5036 .child(Label::new("Searching:").color(Color::Muted))
5037 .child(Label::new(query_text)),
5038 )
5039 })
5040 .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
5041 }
5042}
5043
5044fn find_active_indent_guide_ix(
5045 outline_panel: &OutlinePanel,
5046 candidates: &[IndentGuideLayout],
5047) -> Option<usize> {
5048 let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
5049 return None;
5050 };
5051 let target_depth = outline_panel
5052 .cached_entries
5053 .get(*target_ix)
5054 .map(|cached_entry| cached_entry.depth)?;
5055
5056 let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
5057 .cached_entries
5058 .get(target_ix + 1)
5059 .filter(|cached_entry| cached_entry.depth > target_depth)
5060 .map(|entry| entry.depth)
5061 {
5062 (target_ix + 1, target_depth.saturating_sub(1))
5063 } else {
5064 (*target_ix, target_depth.saturating_sub(1))
5065 };
5066
5067 candidates
5068 .iter()
5069 .enumerate()
5070 .find(|(_, guide)| {
5071 guide.offset.y <= target_ix
5072 && target_ix < guide.offset.y + guide.length
5073 && guide.offset.x == target_depth
5074 })
5075 .map(|(ix, _)| ix)
5076}
5077
5078fn subscribe_for_editor_events(
5079 editor: &Entity<Editor>,
5080 window: &mut Window,
5081 cx: &mut Context<OutlinePanel>,
5082) -> Subscription {
5083 let debounce = Some(UPDATE_DEBOUNCE);
5084 cx.subscribe_in(
5085 editor,
5086 window,
5087 move |outline_panel, editor, e: &EditorEvent, window, cx| {
5088 if !outline_panel.active {
5089 return;
5090 }
5091 match e {
5092 EditorEvent::SelectionsChanged { local: true } => {
5093 outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5094 cx.notify();
5095 }
5096 EditorEvent::ExcerptsAdded { excerpts, .. } => {
5097 outline_panel
5098 .new_entries_for_fs_update
5099 .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
5100 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5101 }
5102 EditorEvent::ExcerptsRemoved { ids, .. } => {
5103 let mut ids = ids.iter().collect::<HashSet<_>>();
5104 for excerpts in outline_panel.excerpts.values_mut() {
5105 excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
5106 if ids.is_empty() {
5107 break;
5108 }
5109 }
5110 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5111 }
5112 EditorEvent::ExcerptsExpanded { ids } => {
5113 outline_panel.invalidate_outlines(ids);
5114 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5115 if update_cached_items {
5116 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5117 }
5118 }
5119 EditorEvent::ExcerptsEdited { ids } => {
5120 outline_panel.invalidate_outlines(ids);
5121 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5122 if update_cached_items {
5123 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5124 }
5125 }
5126 EditorEvent::BufferFoldToggled { ids, .. } => {
5127 outline_panel.invalidate_outlines(ids);
5128 let mut latest_unfolded_buffer_id = None;
5129 let mut latest_folded_buffer_id = None;
5130 let mut ignore_selections_change = false;
5131 outline_panel.new_entries_for_fs_update.extend(
5132 ids.iter()
5133 .filter(|id| {
5134 outline_panel
5135 .excerpts
5136 .iter()
5137 .find_map(|(buffer_id, excerpts)| {
5138 if excerpts.contains_key(id) {
5139 ignore_selections_change |= outline_panel
5140 .preserve_selection_on_buffer_fold_toggles
5141 .remove(buffer_id);
5142 Some(buffer_id)
5143 } else {
5144 None
5145 }
5146 })
5147 .map(|buffer_id| {
5148 if editor.read(cx).is_buffer_folded(*buffer_id, cx) {
5149 latest_folded_buffer_id = Some(*buffer_id);
5150 false
5151 } else {
5152 latest_unfolded_buffer_id = Some(*buffer_id);
5153 true
5154 }
5155 })
5156 .unwrap_or(true)
5157 })
5158 .copied(),
5159 );
5160 if !ignore_selections_change
5161 && let Some(entry_to_select) = latest_unfolded_buffer_id
5162 .or(latest_folded_buffer_id)
5163 .and_then(|toggled_buffer_id| {
5164 outline_panel.fs_entries.iter().find_map(
5165 |fs_entry| match fs_entry {
5166 FsEntry::ExternalFile(external) => {
5167 if external.buffer_id == toggled_buffer_id {
5168 Some(fs_entry.clone())
5169 } else {
5170 None
5171 }
5172 }
5173 FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5174 if *buffer_id == toggled_buffer_id {
5175 Some(fs_entry.clone())
5176 } else {
5177 None
5178 }
5179 }
5180 FsEntry::Directory(..) => None,
5181 },
5182 )
5183 })
5184 .map(PanelEntry::Fs)
5185 {
5186 outline_panel.select_entry(entry_to_select, true, window, cx);
5187 }
5188
5189 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5190 }
5191 EditorEvent::Reparsed(buffer_id) => {
5192 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
5193 for excerpt in excerpts.values_mut() {
5194 excerpt.invalidate_outlines();
5195 }
5196 }
5197 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5198 if update_cached_items {
5199 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5200 }
5201 }
5202 EditorEvent::TitleChanged => {
5203 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5204 }
5205 _ => {}
5206 }
5207 },
5208 )
5209}
5210
5211fn empty_icon() -> AnyElement {
5212 h_flex()
5213 .size(IconSize::default().rems())
5214 .invisible()
5215 .flex_none()
5216 .into_any_element()
5217}
5218
5219#[derive(Debug, Default)]
5220struct GenerationState {
5221 entries: Vec<CachedEntry>,
5222 match_candidates: Vec<StringMatchCandidate>,
5223 max_width_estimate_and_index: Option<(u64, usize)>,
5224}
5225
5226impl GenerationState {
5227 fn clear(&mut self) {
5228 self.entries.clear();
5229 self.match_candidates.clear();
5230 self.max_width_estimate_and_index = None;
5231 }
5232}
5233
5234#[cfg(test)]
5235mod tests {
5236 use db::indoc;
5237 use gpui::{TestAppContext, VisualTestContext, WindowHandle};
5238 use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
5239 use pretty_assertions::assert_eq;
5240 use project::FakeFs;
5241 use search::{
5242 buffer_search,
5243 project_search::{self, perform_project_search},
5244 };
5245 use serde_json::json;
5246 use util::path;
5247 use workspace::{OpenOptions, OpenVisible, ToolbarItemView};
5248
5249 use super::*;
5250
5251 const SELECTED_MARKER: &str = " <==== selected";
5252
5253 #[gpui::test(iterations = 10)]
5254 async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5255 init_test(cx);
5256
5257 let fs = FakeFs::new(cx.background_executor.clone());
5258 let root = path!("/rust-analyzer");
5259 populate_with_test_ra_project(&fs, root).await;
5260 let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5261 project.read_with(cx, |project, _| {
5262 project.languages().add(Arc::new(rust_lang()))
5263 });
5264 let workspace = add_outline_panel(&project, cx).await;
5265 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5266 let outline_panel = outline_panel(&workspace, cx);
5267 outline_panel.update_in(cx, |outline_panel, window, cx| {
5268 outline_panel.set_active(true, window, cx)
5269 });
5270
5271 workspace
5272 .update(cx, |workspace, window, cx| {
5273 ProjectSearchView::deploy_search(
5274 workspace,
5275 &workspace::DeploySearch::default(),
5276 window,
5277 cx,
5278 )
5279 })
5280 .unwrap();
5281 let search_view = workspace
5282 .update(cx, |workspace, _, cx| {
5283 workspace
5284 .active_pane()
5285 .read(cx)
5286 .items()
5287 .find_map(|item| item.downcast::<ProjectSearchView>())
5288 .expect("Project search view expected to appear after new search event trigger")
5289 })
5290 .unwrap();
5291
5292 let query = "param_names_for_lifetime_elision_hints";
5293 perform_project_search(&search_view, query, cx);
5294 search_view.update(cx, |search_view, cx| {
5295 search_view
5296 .results_editor()
5297 .update(cx, |results_editor, cx| {
5298 assert_eq!(
5299 results_editor.display_text(cx).match_indices(query).count(),
5300 9
5301 );
5302 });
5303 });
5304
5305 let all_matches = r#"rust-analyzer/
5306 crates/
5307 ide/src/
5308 inlay_hints/
5309 fn_lifetime_fn.rs
5310 search: match config.«param_names_for_lifetime_elision_hints» {
5311 search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5312 search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5313 search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5314 inlay_hints.rs
5315 search: pub «param_names_for_lifetime_elision_hints»: bool,
5316 search: «param_names_for_lifetime_elision_hints»: self
5317 static_index.rs
5318 search: «param_names_for_lifetime_elision_hints»: false,
5319 rust-analyzer/src/
5320 cli/
5321 analysis_stats.rs
5322 search: «param_names_for_lifetime_elision_hints»: true,
5323 config.rs
5324 search: «param_names_for_lifetime_elision_hints»: self"#
5325 .to_string();
5326
5327 let select_first_in_all_matches = |line_to_select: &str| {
5328 assert!(
5329 all_matches.contains(line_to_select),
5330 "`{line_to_select}` was not found in all matches `{all_matches}`"
5331 );
5332 all_matches.replacen(
5333 line_to_select,
5334 &format!("{line_to_select}{SELECTED_MARKER}"),
5335 1,
5336 )
5337 };
5338
5339 cx.executor()
5340 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5341 cx.run_until_parked();
5342 outline_panel.update(cx, |outline_panel, cx| {
5343 assert_eq!(
5344 display_entries(
5345 &project,
5346 &snapshot(outline_panel, cx),
5347 &outline_panel.cached_entries,
5348 outline_panel.selected_entry(),
5349 cx,
5350 ),
5351 select_first_in_all_matches(
5352 "search: match config.«param_names_for_lifetime_elision_hints» {"
5353 )
5354 );
5355 });
5356
5357 outline_panel.update_in(cx, |outline_panel, window, cx| {
5358 outline_panel.select_parent(&SelectParent, window, cx);
5359 assert_eq!(
5360 display_entries(
5361 &project,
5362 &snapshot(outline_panel, cx),
5363 &outline_panel.cached_entries,
5364 outline_panel.selected_entry(),
5365 cx,
5366 ),
5367 select_first_in_all_matches("fn_lifetime_fn.rs")
5368 );
5369 });
5370 outline_panel.update_in(cx, |outline_panel, window, cx| {
5371 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5372 });
5373 cx.executor()
5374 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5375 cx.run_until_parked();
5376 outline_panel.update(cx, |outline_panel, cx| {
5377 assert_eq!(
5378 display_entries(
5379 &project,
5380 &snapshot(outline_panel, cx),
5381 &outline_panel.cached_entries,
5382 outline_panel.selected_entry(),
5383 cx,
5384 ),
5385 format!(
5386 r#"rust-analyzer/
5387 crates/
5388 ide/src/
5389 inlay_hints/
5390 fn_lifetime_fn.rs{SELECTED_MARKER}
5391 inlay_hints.rs
5392 search: pub «param_names_for_lifetime_elision_hints»: bool,
5393 search: «param_names_for_lifetime_elision_hints»: self
5394 static_index.rs
5395 search: «param_names_for_lifetime_elision_hints»: false,
5396 rust-analyzer/src/
5397 cli/
5398 analysis_stats.rs
5399 search: «param_names_for_lifetime_elision_hints»: true,
5400 config.rs
5401 search: «param_names_for_lifetime_elision_hints»: self"#,
5402 )
5403 );
5404 });
5405
5406 outline_panel.update_in(cx, |outline_panel, window, cx| {
5407 outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5408 });
5409 cx.executor()
5410 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5411 cx.run_until_parked();
5412 outline_panel.update_in(cx, |outline_panel, window, cx| {
5413 outline_panel.select_parent(&SelectParent, window, cx);
5414 assert_eq!(
5415 display_entries(
5416 &project,
5417 &snapshot(outline_panel, cx),
5418 &outline_panel.cached_entries,
5419 outline_panel.selected_entry(),
5420 cx,
5421 ),
5422 select_first_in_all_matches("inlay_hints/")
5423 );
5424 });
5425
5426 outline_panel.update_in(cx, |outline_panel, window, cx| {
5427 outline_panel.select_parent(&SelectParent, window, cx);
5428 assert_eq!(
5429 display_entries(
5430 &project,
5431 &snapshot(outline_panel, cx),
5432 &outline_panel.cached_entries,
5433 outline_panel.selected_entry(),
5434 cx,
5435 ),
5436 select_first_in_all_matches("ide/src/")
5437 );
5438 });
5439
5440 outline_panel.update_in(cx, |outline_panel, window, cx| {
5441 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5442 });
5443 cx.executor()
5444 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5445 cx.run_until_parked();
5446 outline_panel.update(cx, |outline_panel, cx| {
5447 assert_eq!(
5448 display_entries(
5449 &project,
5450 &snapshot(outline_panel, cx),
5451 &outline_panel.cached_entries,
5452 outline_panel.selected_entry(),
5453 cx,
5454 ),
5455 format!(
5456 r#"rust-analyzer/
5457 crates/
5458 ide/src/{SELECTED_MARKER}
5459 rust-analyzer/src/
5460 cli/
5461 analysis_stats.rs
5462 search: «param_names_for_lifetime_elision_hints»: true,
5463 config.rs
5464 search: «param_names_for_lifetime_elision_hints»: self"#,
5465 )
5466 );
5467 });
5468 outline_panel.update_in(cx, |outline_panel, window, cx| {
5469 outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5470 });
5471 cx.executor()
5472 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5473 cx.run_until_parked();
5474 outline_panel.update(cx, |outline_panel, cx| {
5475 assert_eq!(
5476 display_entries(
5477 &project,
5478 &snapshot(outline_panel, cx),
5479 &outline_panel.cached_entries,
5480 outline_panel.selected_entry(),
5481 cx,
5482 ),
5483 select_first_in_all_matches("ide/src/")
5484 );
5485 });
5486 }
5487
5488 #[gpui::test(iterations = 10)]
5489 async fn test_item_filtering(cx: &mut TestAppContext) {
5490 init_test(cx);
5491
5492 let fs = FakeFs::new(cx.background_executor.clone());
5493 let root = path!("/rust-analyzer");
5494 populate_with_test_ra_project(&fs, root).await;
5495 let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5496 project.read_with(cx, |project, _| {
5497 project.languages().add(Arc::new(rust_lang()))
5498 });
5499 let workspace = add_outline_panel(&project, cx).await;
5500 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5501 let outline_panel = outline_panel(&workspace, cx);
5502 outline_panel.update_in(cx, |outline_panel, window, cx| {
5503 outline_panel.set_active(true, window, cx)
5504 });
5505
5506 workspace
5507 .update(cx, |workspace, window, cx| {
5508 ProjectSearchView::deploy_search(
5509 workspace,
5510 &workspace::DeploySearch::default(),
5511 window,
5512 cx,
5513 )
5514 })
5515 .unwrap();
5516 let search_view = workspace
5517 .update(cx, |workspace, _, cx| {
5518 workspace
5519 .active_pane()
5520 .read(cx)
5521 .items()
5522 .find_map(|item| item.downcast::<ProjectSearchView>())
5523 .expect("Project search view expected to appear after new search event trigger")
5524 })
5525 .unwrap();
5526
5527 let query = "param_names_for_lifetime_elision_hints";
5528 perform_project_search(&search_view, query, cx);
5529 search_view.update(cx, |search_view, cx| {
5530 search_view
5531 .results_editor()
5532 .update(cx, |results_editor, cx| {
5533 assert_eq!(
5534 results_editor.display_text(cx).match_indices(query).count(),
5535 9
5536 );
5537 });
5538 });
5539 let all_matches = r#"rust-analyzer/
5540 crates/
5541 ide/src/
5542 inlay_hints/
5543 fn_lifetime_fn.rs
5544 search: match config.«param_names_for_lifetime_elision_hints» {
5545 search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5546 search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5547 search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5548 inlay_hints.rs
5549 search: pub «param_names_for_lifetime_elision_hints»: bool,
5550 search: «param_names_for_lifetime_elision_hints»: self
5551 static_index.rs
5552 search: «param_names_for_lifetime_elision_hints»: false,
5553 rust-analyzer/src/
5554 cli/
5555 analysis_stats.rs
5556 search: «param_names_for_lifetime_elision_hints»: true,
5557 config.rs
5558 search: «param_names_for_lifetime_elision_hints»: self"#
5559 .to_string();
5560
5561 cx.executor()
5562 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5563 cx.run_until_parked();
5564 outline_panel.update(cx, |outline_panel, cx| {
5565 assert_eq!(
5566 display_entries(
5567 &project,
5568 &snapshot(outline_panel, cx),
5569 &outline_panel.cached_entries,
5570 None,
5571 cx,
5572 ),
5573 all_matches,
5574 );
5575 });
5576
5577 let filter_text = "a";
5578 outline_panel.update_in(cx, |outline_panel, window, cx| {
5579 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5580 filter_editor.set_text(filter_text, window, cx);
5581 });
5582 });
5583 cx.executor()
5584 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5585 cx.run_until_parked();
5586
5587 outline_panel.update(cx, |outline_panel, cx| {
5588 assert_eq!(
5589 display_entries(
5590 &project,
5591 &snapshot(outline_panel, cx),
5592 &outline_panel.cached_entries,
5593 None,
5594 cx,
5595 ),
5596 all_matches
5597 .lines()
5598 .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5599 .filter(|item| item.contains(filter_text))
5600 .collect::<Vec<_>>()
5601 .join("\n"),
5602 );
5603 });
5604
5605 outline_panel.update_in(cx, |outline_panel, window, cx| {
5606 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5607 filter_editor.set_text("", window, cx);
5608 });
5609 });
5610 cx.executor()
5611 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5612 cx.run_until_parked();
5613 outline_panel.update(cx, |outline_panel, cx| {
5614 assert_eq!(
5615 display_entries(
5616 &project,
5617 &snapshot(outline_panel, cx),
5618 &outline_panel.cached_entries,
5619 None,
5620 cx,
5621 ),
5622 all_matches,
5623 );
5624 });
5625 }
5626
5627 #[gpui::test(iterations = 10)]
5628 async fn test_item_opening(cx: &mut TestAppContext) {
5629 init_test(cx);
5630
5631 let fs = FakeFs::new(cx.background_executor.clone());
5632 let root = path!("/rust-analyzer");
5633 populate_with_test_ra_project(&fs, root).await;
5634 let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5635 project.read_with(cx, |project, _| {
5636 project.languages().add(Arc::new(rust_lang()))
5637 });
5638 let workspace = add_outline_panel(&project, cx).await;
5639 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5640 let outline_panel = outline_panel(&workspace, cx);
5641 outline_panel.update_in(cx, |outline_panel, window, cx| {
5642 outline_panel.set_active(true, window, cx)
5643 });
5644
5645 workspace
5646 .update(cx, |workspace, window, cx| {
5647 ProjectSearchView::deploy_search(
5648 workspace,
5649 &workspace::DeploySearch::default(),
5650 window,
5651 cx,
5652 )
5653 })
5654 .unwrap();
5655 let search_view = workspace
5656 .update(cx, |workspace, _, cx| {
5657 workspace
5658 .active_pane()
5659 .read(cx)
5660 .items()
5661 .find_map(|item| item.downcast::<ProjectSearchView>())
5662 .expect("Project search view expected to appear after new search event trigger")
5663 })
5664 .unwrap();
5665
5666 let query = "param_names_for_lifetime_elision_hints";
5667 perform_project_search(&search_view, query, cx);
5668 search_view.update(cx, |search_view, cx| {
5669 search_view
5670 .results_editor()
5671 .update(cx, |results_editor, cx| {
5672 assert_eq!(
5673 results_editor.display_text(cx).match_indices(query).count(),
5674 9
5675 );
5676 });
5677 });
5678 let all_matches = r#"rust-analyzer/
5679 crates/
5680 ide/src/
5681 inlay_hints/
5682 fn_lifetime_fn.rs
5683 search: match config.«param_names_for_lifetime_elision_hints» {
5684 search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5685 search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5686 search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5687 inlay_hints.rs
5688 search: pub «param_names_for_lifetime_elision_hints»: bool,
5689 search: «param_names_for_lifetime_elision_hints»: self
5690 static_index.rs
5691 search: «param_names_for_lifetime_elision_hints»: false,
5692 rust-analyzer/src/
5693 cli/
5694 analysis_stats.rs
5695 search: «param_names_for_lifetime_elision_hints»: true,
5696 config.rs
5697 search: «param_names_for_lifetime_elision_hints»: self"#
5698 .to_string();
5699 let select_first_in_all_matches = |line_to_select: &str| {
5700 assert!(
5701 all_matches.contains(line_to_select),
5702 "`{line_to_select}` was not found in all matches `{all_matches}`"
5703 );
5704 all_matches.replacen(
5705 line_to_select,
5706 &format!("{line_to_select}{SELECTED_MARKER}"),
5707 1,
5708 )
5709 };
5710 let clear_outline_metadata = |input: &str| {
5711 input
5712 .replace("search: ", "")
5713 .replace("«", "")
5714 .replace("»", "")
5715 };
5716
5717 cx.executor()
5718 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5719 cx.run_until_parked();
5720
5721 let active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5722 outline_panel
5723 .active_editor()
5724 .expect("should have an active editor open")
5725 });
5726 let initial_outline_selection =
5727 "search: match config.«param_names_for_lifetime_elision_hints» {";
5728 outline_panel.update_in(cx, |outline_panel, window, cx| {
5729 assert_eq!(
5730 display_entries(
5731 &project,
5732 &snapshot(outline_panel, cx),
5733 &outline_panel.cached_entries,
5734 outline_panel.selected_entry(),
5735 cx,
5736 ),
5737 select_first_in_all_matches(initial_outline_selection)
5738 );
5739 assert_eq!(
5740 selected_row_text(&active_editor, cx),
5741 clear_outline_metadata(initial_outline_selection),
5742 "Should place the initial editor selection on the corresponding search result"
5743 );
5744
5745 outline_panel.select_next(&SelectNext, window, cx);
5746 outline_panel.select_next(&SelectNext, window, cx);
5747 });
5748
5749 let navigated_outline_selection =
5750 "search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {";
5751 outline_panel.update(cx, |outline_panel, cx| {
5752 assert_eq!(
5753 display_entries(
5754 &project,
5755 &snapshot(outline_panel, cx),
5756 &outline_panel.cached_entries,
5757 outline_panel.selected_entry(),
5758 cx,
5759 ),
5760 select_first_in_all_matches(navigated_outline_selection)
5761 );
5762 });
5763 cx.executor()
5764 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5765 outline_panel.update(cx, |_, cx| {
5766 assert_eq!(
5767 selected_row_text(&active_editor, cx),
5768 clear_outline_metadata(navigated_outline_selection),
5769 "Should still have the initial caret position after SelectNext calls"
5770 );
5771 });
5772
5773 outline_panel.update_in(cx, |outline_panel, window, cx| {
5774 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5775 });
5776 outline_panel.update(cx, |_outline_panel, cx| {
5777 assert_eq!(
5778 selected_row_text(&active_editor, cx),
5779 clear_outline_metadata(navigated_outline_selection),
5780 "After opening, should move the caret to the opened outline entry's position"
5781 );
5782 });
5783
5784 outline_panel.update_in(cx, |outline_panel, window, cx| {
5785 outline_panel.select_next(&SelectNext, window, cx);
5786 });
5787 let next_navigated_outline_selection = "search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },";
5788 outline_panel.update(cx, |outline_panel, cx| {
5789 assert_eq!(
5790 display_entries(
5791 &project,
5792 &snapshot(outline_panel, cx),
5793 &outline_panel.cached_entries,
5794 outline_panel.selected_entry(),
5795 cx,
5796 ),
5797 select_first_in_all_matches(next_navigated_outline_selection)
5798 );
5799 });
5800 cx.executor()
5801 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5802 outline_panel.update(cx, |_outline_panel, cx| {
5803 assert_eq!(
5804 selected_row_text(&active_editor, cx),
5805 clear_outline_metadata(next_navigated_outline_selection),
5806 "Should again preserve the selection after another SelectNext call"
5807 );
5808 });
5809
5810 outline_panel.update_in(cx, |outline_panel, window, cx| {
5811 outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx);
5812 });
5813 cx.executor()
5814 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5815 cx.run_until_parked();
5816 let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5817 outline_panel
5818 .active_editor()
5819 .expect("should have an active editor open")
5820 });
5821 outline_panel.update(cx, |outline_panel, cx| {
5822 assert_ne!(
5823 active_editor, new_active_editor,
5824 "After opening an excerpt, new editor should be open"
5825 );
5826 assert_eq!(
5827 display_entries(
5828 &project,
5829 &snapshot(outline_panel, cx),
5830 &outline_panel.cached_entries,
5831 outline_panel.selected_entry(),
5832 cx,
5833 ),
5834 "fn_lifetime_fn.rs <==== selected"
5835 );
5836 assert_eq!(
5837 selected_row_text(&new_active_editor, cx),
5838 clear_outline_metadata(next_navigated_outline_selection),
5839 "When opening the excerpt, should navigate to the place corresponding the outline entry"
5840 );
5841 });
5842 }
5843
5844 #[gpui::test]
5845 async fn test_multiple_worktrees(cx: &mut TestAppContext) {
5846 init_test(cx);
5847
5848 let fs = FakeFs::new(cx.background_executor.clone());
5849 fs.insert_tree(
5850 path!("/root"),
5851 json!({
5852 "one": {
5853 "a.txt": "aaa aaa"
5854 },
5855 "two": {
5856 "b.txt": "a aaa"
5857 }
5858
5859 }),
5860 )
5861 .await;
5862 let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await;
5863 let workspace = add_outline_panel(&project, cx).await;
5864 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5865 let outline_panel = outline_panel(&workspace, cx);
5866 outline_panel.update_in(cx, |outline_panel, window, cx| {
5867 outline_panel.set_active(true, window, cx)
5868 });
5869
5870 let items = workspace
5871 .update(cx, |workspace, window, cx| {
5872 workspace.open_paths(
5873 vec![PathBuf::from(path!("/root/two"))],
5874 OpenOptions {
5875 visible: Some(OpenVisible::OnlyDirectories),
5876 ..Default::default()
5877 },
5878 None,
5879 window,
5880 cx,
5881 )
5882 })
5883 .unwrap()
5884 .await;
5885 assert_eq!(items.len(), 1, "Were opening another worktree directory");
5886 assert!(
5887 items[0].is_none(),
5888 "Directory should be opened successfully"
5889 );
5890
5891 workspace
5892 .update(cx, |workspace, window, cx| {
5893 ProjectSearchView::deploy_search(
5894 workspace,
5895 &workspace::DeploySearch::default(),
5896 window,
5897 cx,
5898 )
5899 })
5900 .unwrap();
5901 let search_view = workspace
5902 .update(cx, |workspace, _, cx| {
5903 workspace
5904 .active_pane()
5905 .read(cx)
5906 .items()
5907 .find_map(|item| item.downcast::<ProjectSearchView>())
5908 .expect("Project search view expected to appear after new search event trigger")
5909 })
5910 .unwrap();
5911
5912 let query = "aaa";
5913 perform_project_search(&search_view, query, cx);
5914 search_view.update(cx, |search_view, cx| {
5915 search_view
5916 .results_editor()
5917 .update(cx, |results_editor, cx| {
5918 assert_eq!(
5919 results_editor.display_text(cx).match_indices(query).count(),
5920 3
5921 );
5922 });
5923 });
5924
5925 cx.executor()
5926 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5927 cx.run_until_parked();
5928 outline_panel.update(cx, |outline_panel, cx| {
5929 assert_eq!(
5930 display_entries(
5931 &project,
5932 &snapshot(outline_panel, cx),
5933 &outline_panel.cached_entries,
5934 outline_panel.selected_entry(),
5935 cx,
5936 ),
5937 format!(
5938 r#"one/
5939 a.txt
5940 search: «aaa» aaa <==== selected
5941 search: aaa «aaa»
5942two/
5943 b.txt
5944 search: a «aaa»"#,
5945 ),
5946 );
5947 });
5948
5949 outline_panel.update_in(cx, |outline_panel, window, cx| {
5950 outline_panel.select_previous(&SelectPrevious, window, cx);
5951 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5952 });
5953 cx.executor()
5954 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5955 cx.run_until_parked();
5956 outline_panel.update(cx, |outline_panel, cx| {
5957 assert_eq!(
5958 display_entries(
5959 &project,
5960 &snapshot(outline_panel, cx),
5961 &outline_panel.cached_entries,
5962 outline_panel.selected_entry(),
5963 cx,
5964 ),
5965 format!(
5966 r#"one/
5967 a.txt <==== selected
5968two/
5969 b.txt
5970 search: a «aaa»"#,
5971 ),
5972 );
5973 });
5974
5975 outline_panel.update_in(cx, |outline_panel, window, cx| {
5976 outline_panel.select_next(&SelectNext, window, cx);
5977 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5978 });
5979 cx.executor()
5980 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5981 cx.run_until_parked();
5982 outline_panel.update(cx, |outline_panel, cx| {
5983 assert_eq!(
5984 display_entries(
5985 &project,
5986 &snapshot(outline_panel, cx),
5987 &outline_panel.cached_entries,
5988 outline_panel.selected_entry(),
5989 cx,
5990 ),
5991 format!(
5992 r#"one/
5993 a.txt
5994two/ <==== selected"#,
5995 ),
5996 );
5997 });
5998
5999 outline_panel.update_in(cx, |outline_panel, window, cx| {
6000 outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
6001 });
6002 cx.executor()
6003 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6004 cx.run_until_parked();
6005 outline_panel.update(cx, |outline_panel, cx| {
6006 assert_eq!(
6007 display_entries(
6008 &project,
6009 &snapshot(outline_panel, cx),
6010 &outline_panel.cached_entries,
6011 outline_panel.selected_entry(),
6012 cx,
6013 ),
6014 format!(
6015 r#"one/
6016 a.txt
6017two/ <==== selected
6018 b.txt
6019 search: a «aaa»"#,
6020 )
6021 );
6022 });
6023 }
6024
6025 #[gpui::test]
6026 async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
6027 init_test(cx);
6028
6029 let root = path!("/root");
6030 let fs = FakeFs::new(cx.background_executor.clone());
6031 fs.insert_tree(
6032 root,
6033 json!({
6034 "src": {
6035 "lib.rs": indoc!("
6036#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6037struct OutlineEntryExcerpt {
6038 id: ExcerptId,
6039 buffer_id: BufferId,
6040 range: ExcerptRange<language::Anchor>,
6041}"),
6042 }
6043 }),
6044 )
6045 .await;
6046 let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6047 project.read_with(cx, |project, _| {
6048 project.languages().add(Arc::new(
6049 rust_lang()
6050 .with_outline_query(
6051 r#"
6052 (struct_item
6053 (visibility_modifier)? @context
6054 "struct" @context
6055 name: (_) @name) @item
6056
6057 (field_declaration
6058 (visibility_modifier)? @context
6059 name: (_) @name) @item
6060"#,
6061 )
6062 .unwrap(),
6063 ))
6064 });
6065 let workspace = add_outline_panel(&project, cx).await;
6066 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6067 let outline_panel = outline_panel(&workspace, cx);
6068 cx.update(|window, cx| {
6069 outline_panel.update(cx, |outline_panel, cx| {
6070 outline_panel.set_active(true, window, cx)
6071 });
6072 });
6073
6074 let _editor = workspace
6075 .update(cx, |workspace, window, cx| {
6076 workspace.open_abs_path(
6077 PathBuf::from(path!("/root/src/lib.rs")),
6078 OpenOptions {
6079 visible: Some(OpenVisible::All),
6080 ..Default::default()
6081 },
6082 window,
6083 cx,
6084 )
6085 })
6086 .unwrap()
6087 .await
6088 .expect("Failed to open Rust source file")
6089 .downcast::<Editor>()
6090 .expect("Should open an editor for Rust source file");
6091
6092 cx.executor()
6093 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6094 cx.run_until_parked();
6095 outline_panel.update(cx, |outline_panel, cx| {
6096 assert_eq!(
6097 display_entries(
6098 &project,
6099 &snapshot(outline_panel, cx),
6100 &outline_panel.cached_entries,
6101 outline_panel.selected_entry(),
6102 cx,
6103 ),
6104 indoc!(
6105 "
6106outline: struct OutlineEntryExcerpt
6107 outline: id
6108 outline: buffer_id
6109 outline: range"
6110 )
6111 );
6112 });
6113
6114 cx.update(|window, cx| {
6115 outline_panel.update(cx, |outline_panel, cx| {
6116 outline_panel.select_next(&SelectNext, window, cx);
6117 });
6118 });
6119 cx.executor()
6120 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6121 cx.run_until_parked();
6122 outline_panel.update(cx, |outline_panel, cx| {
6123 assert_eq!(
6124 display_entries(
6125 &project,
6126 &snapshot(outline_panel, cx),
6127 &outline_panel.cached_entries,
6128 outline_panel.selected_entry(),
6129 cx,
6130 ),
6131 indoc!(
6132 "
6133outline: struct OutlineEntryExcerpt <==== selected
6134 outline: id
6135 outline: buffer_id
6136 outline: range"
6137 )
6138 );
6139 });
6140
6141 cx.update(|window, cx| {
6142 outline_panel.update(cx, |outline_panel, cx| {
6143 outline_panel.select_next(&SelectNext, window, cx);
6144 });
6145 });
6146 cx.executor()
6147 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6148 cx.run_until_parked();
6149 outline_panel.update(cx, |outline_panel, cx| {
6150 assert_eq!(
6151 display_entries(
6152 &project,
6153 &snapshot(outline_panel, cx),
6154 &outline_panel.cached_entries,
6155 outline_panel.selected_entry(),
6156 cx,
6157 ),
6158 indoc!(
6159 "
6160outline: struct OutlineEntryExcerpt
6161 outline: id <==== selected
6162 outline: buffer_id
6163 outline: range"
6164 )
6165 );
6166 });
6167
6168 cx.update(|window, cx| {
6169 outline_panel.update(cx, |outline_panel, cx| {
6170 outline_panel.select_next(&SelectNext, window, cx);
6171 });
6172 });
6173 cx.executor()
6174 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6175 cx.run_until_parked();
6176 outline_panel.update(cx, |outline_panel, cx| {
6177 assert_eq!(
6178 display_entries(
6179 &project,
6180 &snapshot(outline_panel, cx),
6181 &outline_panel.cached_entries,
6182 outline_panel.selected_entry(),
6183 cx,
6184 ),
6185 indoc!(
6186 "
6187outline: struct OutlineEntryExcerpt
6188 outline: id
6189 outline: buffer_id <==== selected
6190 outline: range"
6191 )
6192 );
6193 });
6194
6195 cx.update(|window, cx| {
6196 outline_panel.update(cx, |outline_panel, cx| {
6197 outline_panel.select_next(&SelectNext, window, cx);
6198 });
6199 });
6200 cx.executor()
6201 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6202 cx.run_until_parked();
6203 outline_panel.update(cx, |outline_panel, cx| {
6204 assert_eq!(
6205 display_entries(
6206 &project,
6207 &snapshot(outline_panel, cx),
6208 &outline_panel.cached_entries,
6209 outline_panel.selected_entry(),
6210 cx,
6211 ),
6212 indoc!(
6213 "
6214outline: struct OutlineEntryExcerpt
6215 outline: id
6216 outline: buffer_id
6217 outline: range <==== selected"
6218 )
6219 );
6220 });
6221
6222 cx.update(|window, cx| {
6223 outline_panel.update(cx, |outline_panel, cx| {
6224 outline_panel.select_next(&SelectNext, window, cx);
6225 });
6226 });
6227 cx.executor()
6228 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6229 cx.run_until_parked();
6230 outline_panel.update(cx, |outline_panel, cx| {
6231 assert_eq!(
6232 display_entries(
6233 &project,
6234 &snapshot(outline_panel, cx),
6235 &outline_panel.cached_entries,
6236 outline_panel.selected_entry(),
6237 cx,
6238 ),
6239 indoc!(
6240 "
6241outline: struct OutlineEntryExcerpt <==== selected
6242 outline: id
6243 outline: buffer_id
6244 outline: range"
6245 )
6246 );
6247 });
6248
6249 cx.update(|window, cx| {
6250 outline_panel.update(cx, |outline_panel, cx| {
6251 outline_panel.select_previous(&SelectPrevious, window, cx);
6252 });
6253 });
6254 cx.executor()
6255 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6256 cx.run_until_parked();
6257 outline_panel.update(cx, |outline_panel, cx| {
6258 assert_eq!(
6259 display_entries(
6260 &project,
6261 &snapshot(outline_panel, cx),
6262 &outline_panel.cached_entries,
6263 outline_panel.selected_entry(),
6264 cx,
6265 ),
6266 indoc!(
6267 "
6268outline: struct OutlineEntryExcerpt
6269 outline: id
6270 outline: buffer_id
6271 outline: range <==== selected"
6272 )
6273 );
6274 });
6275
6276 cx.update(|window, cx| {
6277 outline_panel.update(cx, |outline_panel, cx| {
6278 outline_panel.select_previous(&SelectPrevious, window, cx);
6279 });
6280 });
6281 cx.executor()
6282 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6283 cx.run_until_parked();
6284 outline_panel.update(cx, |outline_panel, cx| {
6285 assert_eq!(
6286 display_entries(
6287 &project,
6288 &snapshot(outline_panel, cx),
6289 &outline_panel.cached_entries,
6290 outline_panel.selected_entry(),
6291 cx,
6292 ),
6293 indoc!(
6294 "
6295outline: struct OutlineEntryExcerpt
6296 outline: id
6297 outline: buffer_id <==== selected
6298 outline: range"
6299 )
6300 );
6301 });
6302
6303 cx.update(|window, cx| {
6304 outline_panel.update(cx, |outline_panel, cx| {
6305 outline_panel.select_previous(&SelectPrevious, window, cx);
6306 });
6307 });
6308 cx.executor()
6309 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6310 cx.run_until_parked();
6311 outline_panel.update(cx, |outline_panel, cx| {
6312 assert_eq!(
6313 display_entries(
6314 &project,
6315 &snapshot(outline_panel, cx),
6316 &outline_panel.cached_entries,
6317 outline_panel.selected_entry(),
6318 cx,
6319 ),
6320 indoc!(
6321 "
6322outline: struct OutlineEntryExcerpt
6323 outline: id <==== selected
6324 outline: buffer_id
6325 outline: range"
6326 )
6327 );
6328 });
6329
6330 cx.update(|window, cx| {
6331 outline_panel.update(cx, |outline_panel, cx| {
6332 outline_panel.select_previous(&SelectPrevious, window, cx);
6333 });
6334 });
6335 cx.executor()
6336 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6337 cx.run_until_parked();
6338 outline_panel.update(cx, |outline_panel, cx| {
6339 assert_eq!(
6340 display_entries(
6341 &project,
6342 &snapshot(outline_panel, cx),
6343 &outline_panel.cached_entries,
6344 outline_panel.selected_entry(),
6345 cx,
6346 ),
6347 indoc!(
6348 "
6349outline: struct OutlineEntryExcerpt <==== selected
6350 outline: id
6351 outline: buffer_id
6352 outline: range"
6353 )
6354 );
6355 });
6356
6357 cx.update(|window, cx| {
6358 outline_panel.update(cx, |outline_panel, cx| {
6359 outline_panel.select_previous(&SelectPrevious, window, cx);
6360 });
6361 });
6362 cx.executor()
6363 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6364 cx.run_until_parked();
6365 outline_panel.update(cx, |outline_panel, cx| {
6366 assert_eq!(
6367 display_entries(
6368 &project,
6369 &snapshot(outline_panel, cx),
6370 &outline_panel.cached_entries,
6371 outline_panel.selected_entry(),
6372 cx,
6373 ),
6374 indoc!(
6375 "
6376outline: struct OutlineEntryExcerpt
6377 outline: id
6378 outline: buffer_id
6379 outline: range <==== selected"
6380 )
6381 );
6382 });
6383 }
6384
6385 #[gpui::test(iterations = 10)]
6386 async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6387 init_test(cx);
6388
6389 let root = path!("/frontend-project");
6390 let fs = FakeFs::new(cx.background_executor.clone());
6391 fs.insert_tree(
6392 root,
6393 json!({
6394 "public": {
6395 "lottie": {
6396 "syntax-tree.json": r#"{ "something": "static" }"#
6397 }
6398 },
6399 "src": {
6400 "app": {
6401 "(site)": {
6402 "(about)": {
6403 "jobs": {
6404 "[slug]": {
6405 "page.tsx": r#"static"#
6406 }
6407 }
6408 },
6409 "(blog)": {
6410 "post": {
6411 "[slug]": {
6412 "page.tsx": r#"static"#
6413 }
6414 }
6415 },
6416 }
6417 },
6418 "components": {
6419 "ErrorBoundary.tsx": r#"static"#,
6420 }
6421 }
6422
6423 }),
6424 )
6425 .await;
6426 let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6427 let workspace = add_outline_panel(&project, cx).await;
6428 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6429 let outline_panel = outline_panel(&workspace, cx);
6430 outline_panel.update_in(cx, |outline_panel, window, cx| {
6431 outline_panel.set_active(true, window, cx)
6432 });
6433
6434 workspace
6435 .update(cx, |workspace, window, cx| {
6436 ProjectSearchView::deploy_search(
6437 workspace,
6438 &workspace::DeploySearch::default(),
6439 window,
6440 cx,
6441 )
6442 })
6443 .unwrap();
6444 let search_view = workspace
6445 .update(cx, |workspace, _, cx| {
6446 workspace
6447 .active_pane()
6448 .read(cx)
6449 .items()
6450 .find_map(|item| item.downcast::<ProjectSearchView>())
6451 .expect("Project search view expected to appear after new search event trigger")
6452 })
6453 .unwrap();
6454
6455 let query = "static";
6456 perform_project_search(&search_view, query, cx);
6457 search_view.update(cx, |search_view, cx| {
6458 search_view
6459 .results_editor()
6460 .update(cx, |results_editor, cx| {
6461 assert_eq!(
6462 results_editor.display_text(cx).match_indices(query).count(),
6463 4
6464 );
6465 });
6466 });
6467
6468 cx.executor()
6469 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6470 cx.run_until_parked();
6471 outline_panel.update(cx, |outline_panel, cx| {
6472 assert_eq!(
6473 display_entries(
6474 &project,
6475 &snapshot(outline_panel, cx),
6476 &outline_panel.cached_entries,
6477 outline_panel.selected_entry(),
6478 cx,
6479 ),
6480 format!(
6481 r#"frontend-project/
6482 public/lottie/
6483 syntax-tree.json
6484 search: {{ "something": "«static»" }} <==== selected
6485 src/
6486 app/(site)/
6487 (about)/jobs/[slug]/
6488 page.tsx
6489 search: «static»
6490 (blog)/post/[slug]/
6491 page.tsx
6492 search: «static»
6493 components/
6494 ErrorBoundary.tsx
6495 search: «static»"#
6496 )
6497 );
6498 });
6499
6500 outline_panel.update_in(cx, |outline_panel, window, cx| {
6501 // Move to 5th element in the list, 3 items down.
6502 for _ in 0..2 {
6503 outline_panel.select_next(&SelectNext, window, cx);
6504 }
6505 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6506 });
6507 cx.executor()
6508 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6509 cx.run_until_parked();
6510 outline_panel.update(cx, |outline_panel, cx| {
6511 assert_eq!(
6512 display_entries(
6513 &project,
6514 &snapshot(outline_panel, cx),
6515 &outline_panel.cached_entries,
6516 outline_panel.selected_entry(),
6517 cx,
6518 ),
6519 format!(
6520 r#"frontend-project/
6521 public/lottie/
6522 syntax-tree.json
6523 search: {{ "something": "«static»" }}
6524 src/
6525 app/(site)/ <==== selected
6526 components/
6527 ErrorBoundary.tsx
6528 search: «static»"#
6529 )
6530 );
6531 });
6532
6533 outline_panel.update_in(cx, |outline_panel, window, cx| {
6534 // Move to the next visible non-FS entry
6535 for _ in 0..3 {
6536 outline_panel.select_next(&SelectNext, window, cx);
6537 }
6538 });
6539 cx.run_until_parked();
6540 outline_panel.update(cx, |outline_panel, cx| {
6541 assert_eq!(
6542 display_entries(
6543 &project,
6544 &snapshot(outline_panel, cx),
6545 &outline_panel.cached_entries,
6546 outline_panel.selected_entry(),
6547 cx,
6548 ),
6549 format!(
6550 r#"frontend-project/
6551 public/lottie/
6552 syntax-tree.json
6553 search: {{ "something": "«static»" }}
6554 src/
6555 app/(site)/
6556 components/
6557 ErrorBoundary.tsx
6558 search: «static» <==== selected"#
6559 )
6560 );
6561 });
6562
6563 outline_panel.update_in(cx, |outline_panel, window, cx| {
6564 outline_panel
6565 .active_editor()
6566 .expect("Should have an active editor")
6567 .update(cx, |editor, cx| {
6568 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6569 });
6570 });
6571 cx.executor()
6572 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6573 cx.run_until_parked();
6574 outline_panel.update(cx, |outline_panel, cx| {
6575 assert_eq!(
6576 display_entries(
6577 &project,
6578 &snapshot(outline_panel, cx),
6579 &outline_panel.cached_entries,
6580 outline_panel.selected_entry(),
6581 cx,
6582 ),
6583 format!(
6584 r#"frontend-project/
6585 public/lottie/
6586 syntax-tree.json
6587 search: {{ "something": "«static»" }}
6588 src/
6589 app/(site)/
6590 components/
6591 ErrorBoundary.tsx <==== selected"#
6592 )
6593 );
6594 });
6595
6596 outline_panel.update_in(cx, |outline_panel, window, cx| {
6597 outline_panel
6598 .active_editor()
6599 .expect("Should have an active editor")
6600 .update(cx, |editor, cx| {
6601 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6602 });
6603 });
6604 cx.executor()
6605 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6606 cx.run_until_parked();
6607 outline_panel.update(cx, |outline_panel, cx| {
6608 assert_eq!(
6609 display_entries(
6610 &project,
6611 &snapshot(outline_panel, cx),
6612 &outline_panel.cached_entries,
6613 outline_panel.selected_entry(),
6614 cx,
6615 ),
6616 format!(
6617 r#"frontend-project/
6618 public/lottie/
6619 syntax-tree.json <==== selected
6620 src/
6621 app/(site)/
6622 components/
6623 ErrorBoundary.tsx"#
6624 )
6625 );
6626 });
6627
6628 outline_panel.update_in(cx, |outline_panel, window, cx| {
6629 outline_panel.collapse_all_entries(&CollapseAllEntries, window, cx);
6630 });
6631 cx.executor()
6632 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6633 cx.run_until_parked();
6634 outline_panel.update(cx, |outline_panel, cx| {
6635 assert_eq!(
6636 display_entries(
6637 &project,
6638 &snapshot(outline_panel, cx),
6639 &outline_panel.cached_entries,
6640 outline_panel.selected_entry(),
6641 cx,
6642 ),
6643 format!(r#"frontend-project/"#)
6644 );
6645 });
6646
6647 outline_panel.update_in(cx, |outline_panel, window, cx| {
6648 outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
6649 });
6650 cx.executor()
6651 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6652 cx.run_until_parked();
6653 outline_panel.update(cx, |outline_panel, cx| {
6654 assert_eq!(
6655 display_entries(
6656 &project,
6657 &snapshot(outline_panel, cx),
6658 &outline_panel.cached_entries,
6659 outline_panel.selected_entry(),
6660 cx,
6661 ),
6662 format!(
6663 r#"frontend-project/
6664 public/lottie/
6665 syntax-tree.json <==== selected
6666 search: {{ "something": "«static»" }}
6667 src/
6668 app/(site)/
6669 (about)/jobs/[slug]/
6670 page.tsx
6671 search: «static»
6672 (blog)/post/[slug]/
6673 page.tsx
6674 search: «static»
6675 components/
6676 ErrorBoundary.tsx
6677 search: «static»"#
6678 )
6679 );
6680 });
6681 }
6682
6683 async fn add_outline_panel(
6684 project: &Entity<Project>,
6685 cx: &mut TestAppContext,
6686 ) -> WindowHandle<Workspace> {
6687 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6688
6689 let outline_panel = window
6690 .update(cx, |_, window, cx| {
6691 cx.spawn_in(window, async |this, cx| {
6692 OutlinePanel::load(this, cx.clone()).await
6693 })
6694 })
6695 .unwrap()
6696 .await
6697 .expect("Failed to load outline panel");
6698
6699 window
6700 .update(cx, |workspace, window, cx| {
6701 workspace.add_panel(outline_panel, window, cx);
6702 })
6703 .unwrap();
6704 window
6705 }
6706
6707 fn outline_panel(
6708 workspace: &WindowHandle<Workspace>,
6709 cx: &mut TestAppContext,
6710 ) -> Entity<OutlinePanel> {
6711 workspace
6712 .update(cx, |workspace, _, cx| {
6713 workspace
6714 .panel::<OutlinePanel>(cx)
6715 .expect("no outline panel")
6716 })
6717 .unwrap()
6718 }
6719
6720 fn display_entries(
6721 project: &Entity<Project>,
6722 multi_buffer_snapshot: &MultiBufferSnapshot,
6723 cached_entries: &[CachedEntry],
6724 selected_entry: Option<&PanelEntry>,
6725 cx: &mut App,
6726 ) -> String {
6727 let project = project.read(cx);
6728 let mut display_string = String::new();
6729 for entry in cached_entries {
6730 if !display_string.is_empty() {
6731 display_string += "\n";
6732 }
6733 for _ in 0..entry.depth {
6734 display_string += " ";
6735 }
6736 display_string += &match &entry.entry {
6737 PanelEntry::Fs(entry) => match entry {
6738 FsEntry::ExternalFile(_) => {
6739 panic!("Did not cover external files with tests")
6740 }
6741 FsEntry::Directory(directory) => {
6742 let path = if let Some(worktree) = project
6743 .worktree_for_id(directory.worktree_id, cx)
6744 .filter(|worktree| {
6745 worktree.read(cx).root_entry() == Some(&directory.entry.entry)
6746 }) {
6747 worktree
6748 .read(cx)
6749 .root_name()
6750 .join(&directory.entry.path)
6751 .as_unix_str()
6752 .to_string()
6753 } else {
6754 directory
6755 .entry
6756 .path
6757 .file_name()
6758 .unwrap_or_default()
6759 .to_string()
6760 };
6761 format!("{path}/")
6762 }
6763 FsEntry::File(file) => file
6764 .entry
6765 .path
6766 .file_name()
6767 .map(|name| name.to_string())
6768 .unwrap_or_default(),
6769 },
6770 PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6771 .entries
6772 .iter()
6773 .filter_map(|dir| dir.path.file_name())
6774 .map(|name| name.to_string() + "/")
6775 .collect(),
6776 PanelEntry::Outline(outline_entry) => match outline_entry {
6777 OutlineEntry::Excerpt(_) => continue,
6778 OutlineEntry::Outline(outline_entry) => {
6779 format!("outline: {}", outline_entry.outline.text)
6780 }
6781 },
6782 PanelEntry::Search(search_entry) => {
6783 let search_data = search_entry.render_data.get_or_init(|| {
6784 SearchData::new(&search_entry.match_range, multi_buffer_snapshot)
6785 });
6786 let mut search_result = String::new();
6787 let mut last_end = 0;
6788 for range in &search_data.search_match_indices {
6789 search_result.push_str(&search_data.context_text[last_end..range.start]);
6790 search_result.push('«');
6791 search_result.push_str(&search_data.context_text[range.start..range.end]);
6792 search_result.push('»');
6793 last_end = range.end;
6794 }
6795 search_result.push_str(&search_data.context_text[last_end..]);
6796
6797 format!("search: {search_result}")
6798 }
6799 };
6800
6801 if Some(&entry.entry) == selected_entry {
6802 display_string += SELECTED_MARKER;
6803 }
6804 }
6805 display_string
6806 }
6807
6808 fn init_test(cx: &mut TestAppContext) {
6809 cx.update(|cx| {
6810 let settings = SettingsStore::test(cx);
6811 cx.set_global(settings);
6812
6813 theme::init(theme::LoadThemes::JustBase, cx);
6814
6815 editor::init(cx);
6816 project_search::init(cx);
6817 buffer_search::init(cx);
6818 super::init(cx);
6819 });
6820 }
6821
6822 // Based on https://github.com/rust-lang/rust-analyzer/
6823 async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6824 fs.insert_tree(
6825 root,
6826 json!({
6827 "crates": {
6828 "ide": {
6829 "src": {
6830 "inlay_hints": {
6831 "fn_lifetime_fn.rs": r##"
6832 pub(super) fn hints(
6833 acc: &mut Vec<InlayHint>,
6834 config: &InlayHintsConfig,
6835 func: ast::Fn,
6836 ) -> Option<()> {
6837 // ... snip
6838
6839 let mut used_names: FxHashMap<SmolStr, usize> =
6840 match config.param_names_for_lifetime_elision_hints {
6841 true => generic_param_list
6842 .iter()
6843 .flat_map(|gpl| gpl.lifetime_params())
6844 .filter_map(|param| param.lifetime())
6845 .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6846 .collect(),
6847 false => Default::default(),
6848 };
6849 {
6850 let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6851 if self_param.is_some() && potential_lt_refs.next().is_some() {
6852 allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6853 // self can't be used as a lifetime, so no need to check for collisions
6854 "'self".into()
6855 } else {
6856 gen_idx_name()
6857 });
6858 }
6859 potential_lt_refs.for_each(|(name, ..)| {
6860 let name = match name {
6861 Some(it) if config.param_names_for_lifetime_elision_hints => {
6862 if let Some(c) = used_names.get_mut(it.text().as_str()) {
6863 *c += 1;
6864 SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6865 } else {
6866 used_names.insert(it.text().as_str().into(), 0);
6867 SmolStr::from_iter(["\'", it.text().as_str()])
6868 }
6869 }
6870 _ => gen_idx_name(),
6871 };
6872 allocated_lifetimes.push(name);
6873 });
6874 }
6875
6876 // ... snip
6877 }
6878
6879 // ... snip
6880
6881 #[test]
6882 fn hints_lifetimes_named() {
6883 check_with_config(
6884 InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6885 r#"
6886 fn nested_in<'named>(named: & &X< &()>) {}
6887 // ^'named1, 'named2, 'named3, $
6888 //^'named1 ^'named2 ^'named3
6889 "#,
6890 );
6891 }
6892
6893 // ... snip
6894 "##,
6895 },
6896 "inlay_hints.rs": r#"
6897 #[derive(Clone, Debug, PartialEq, Eq)]
6898 pub struct InlayHintsConfig {
6899 // ... snip
6900 pub param_names_for_lifetime_elision_hints: bool,
6901 pub max_length: Option<usize>,
6902 // ... snip
6903 }
6904
6905 impl Config {
6906 pub fn inlay_hints(&self) -> InlayHintsConfig {
6907 InlayHintsConfig {
6908 // ... snip
6909 param_names_for_lifetime_elision_hints: self
6910 .inlayHints_lifetimeElisionHints_useParameterNames()
6911 .to_owned(),
6912 max_length: self.inlayHints_maxLength().to_owned(),
6913 // ... snip
6914 }
6915 }
6916 }
6917 "#,
6918 "static_index.rs": r#"
6919// ... snip
6920 fn add_file(&mut self, file_id: FileId) {
6921 let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6922 let folds = self.analysis.folding_ranges(file_id).unwrap();
6923 let inlay_hints = self
6924 .analysis
6925 .inlay_hints(
6926 &InlayHintsConfig {
6927 // ... snip
6928 closure_style: hir::ClosureStyle::ImplFn,
6929 param_names_for_lifetime_elision_hints: false,
6930 binding_mode_hints: false,
6931 max_length: Some(25),
6932 closure_capture_hints: false,
6933 // ... snip
6934 },
6935 file_id,
6936 None,
6937 )
6938 .unwrap();
6939 // ... snip
6940 }
6941// ... snip
6942 "#
6943 }
6944 },
6945 "rust-analyzer": {
6946 "src": {
6947 "cli": {
6948 "analysis_stats.rs": r#"
6949 // ... snip
6950 for &file_id in &file_ids {
6951 _ = analysis.inlay_hints(
6952 &InlayHintsConfig {
6953 // ... snip
6954 implicit_drop_hints: true,
6955 lifetime_elision_hints: ide::LifetimeElisionHints::Always,
6956 param_names_for_lifetime_elision_hints: true,
6957 hide_named_constructor_hints: false,
6958 hide_closure_initialization_hints: false,
6959 closure_style: hir::ClosureStyle::ImplFn,
6960 max_length: Some(25),
6961 closing_brace_hints_min_lines: Some(20),
6962 fields_to_resolve: InlayFieldsToResolve::empty(),
6963 range_exclusive_hints: true,
6964 },
6965 file_id.into(),
6966 None,
6967 );
6968 }
6969 // ... snip
6970 "#,
6971 },
6972 "config.rs": r#"
6973 config_data! {
6974 /// Configs that only make sense when they are set by a client. As such they can only be defined
6975 /// by setting them using client's settings (e.g `settings.json` on VS Code).
6976 client: struct ClientDefaultConfigData <- ClientConfigInput -> {
6977 // ... snip
6978 /// Maximum length for inlay hints. Set to null to have an unlimited length.
6979 inlayHints_maxLength: Option<usize> = Some(25),
6980 // ... snip
6981 /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
6982 inlayHints_lifetimeElisionHints_useParameterNames: bool = false,
6983 // ... snip
6984 }
6985 }
6986
6987 impl Config {
6988 // ... snip
6989 pub fn inlay_hints(&self) -> InlayHintsConfig {
6990 InlayHintsConfig {
6991 // ... snip
6992 param_names_for_lifetime_elision_hints: self
6993 .inlayHints_lifetimeElisionHints_useParameterNames()
6994 .to_owned(),
6995 max_length: self.inlayHints_maxLength().to_owned(),
6996 // ... snip
6997 }
6998 }
6999 // ... snip
7000 }
7001 "#
7002 }
7003 }
7004 }
7005 }),
7006 )
7007 .await;
7008 }
7009
7010 fn rust_lang() -> Language {
7011 Language::new(
7012 LanguageConfig {
7013 name: "Rust".into(),
7014 matcher: LanguageMatcher {
7015 path_suffixes: vec!["rs".to_string()],
7016 ..Default::default()
7017 },
7018 ..Default::default()
7019 },
7020 Some(tree_sitter_rust::LANGUAGE.into()),
7021 )
7022 .with_highlights_query(
7023 r#"
7024 (field_identifier) @field
7025 (struct_expression) @struct
7026 "#,
7027 )
7028 .unwrap()
7029 .with_injection_query(
7030 r#"
7031 (macro_invocation
7032 (token_tree) @injection.content
7033 (#set! injection.language "rust"))
7034 "#,
7035 )
7036 .unwrap()
7037 }
7038
7039 fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
7040 outline_panel
7041 .active_editor()
7042 .unwrap()
7043 .read(cx)
7044 .buffer()
7045 .read(cx)
7046 .snapshot(cx)
7047 }
7048
7049 fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
7050 editor.update(cx, |editor, cx| {
7051 let selections = editor.selections.all::<language::Point>(&editor.display_snapshot(cx));
7052 assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
7053 let selection = selections.first().unwrap();
7054 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
7055 let line_start = language::Point::new(selection.start.row, 0);
7056 let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
7057 multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
7058 })
7059 }
7060
7061 #[gpui::test]
7062 async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) {
7063 init_test(cx);
7064
7065 let fs = FakeFs::new(cx.background_executor.clone());
7066 fs.insert_tree(
7067 "/test",
7068 json!({
7069 "src": {
7070 "lib.rs": indoc!("
7071 mod outer {
7072 pub struct OuterStruct {
7073 field: String,
7074 }
7075 impl OuterStruct {
7076 pub fn new() -> Self {
7077 Self { field: String::new() }
7078 }
7079 pub fn method(&self) {
7080 println!(\"{}\", self.field);
7081 }
7082 }
7083 mod inner {
7084 pub fn inner_function() {
7085 let x = 42;
7086 println!(\"{}\", x);
7087 }
7088 pub struct InnerStruct {
7089 value: i32,
7090 }
7091 }
7092 }
7093 fn main() {
7094 let s = outer::OuterStruct::new();
7095 s.method();
7096 }
7097 "),
7098 }
7099 }),
7100 )
7101 .await;
7102
7103 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7104 project.read_with(cx, |project, _| {
7105 project.languages().add(Arc::new(
7106 rust_lang()
7107 .with_outline_query(
7108 r#"
7109 (struct_item
7110 (visibility_modifier)? @context
7111 "struct" @context
7112 name: (_) @name) @item
7113 (impl_item
7114 "impl" @context
7115 trait: (_)? @context
7116 "for"? @context
7117 type: (_) @context
7118 body: (_)) @item
7119 (function_item
7120 (visibility_modifier)? @context
7121 "fn" @context
7122 name: (_) @name
7123 parameters: (_) @context) @item
7124 (mod_item
7125 (visibility_modifier)? @context
7126 "mod" @context
7127 name: (_) @name) @item
7128 (enum_item
7129 (visibility_modifier)? @context
7130 "enum" @context
7131 name: (_) @name) @item
7132 (field_declaration
7133 (visibility_modifier)? @context
7134 name: (_) @name
7135 ":" @context
7136 type: (_) @context) @item
7137 "#,
7138 )
7139 .unwrap(),
7140 ))
7141 });
7142 let workspace = add_outline_panel(&project, cx).await;
7143 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7144 let outline_panel = outline_panel(&workspace, cx);
7145
7146 outline_panel.update_in(cx, |outline_panel, window, cx| {
7147 outline_panel.set_active(true, window, cx)
7148 });
7149
7150 workspace
7151 .update(cx, |workspace, window, cx| {
7152 workspace.open_abs_path(
7153 PathBuf::from("/test/src/lib.rs"),
7154 OpenOptions {
7155 visible: Some(OpenVisible::All),
7156 ..Default::default()
7157 },
7158 window,
7159 cx,
7160 )
7161 })
7162 .unwrap()
7163 .await
7164 .unwrap();
7165
7166 cx.executor()
7167 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7168 cx.run_until_parked();
7169
7170 // Force another update cycle to ensure outlines are fetched
7171 outline_panel.update_in(cx, |panel, window, cx| {
7172 panel.update_non_fs_items(window, cx);
7173 panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7174 });
7175 cx.executor()
7176 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7177 cx.run_until_parked();
7178
7179 outline_panel.update(cx, |outline_panel, cx| {
7180 assert_eq!(
7181 display_entries(
7182 &project,
7183 &snapshot(outline_panel, cx),
7184 &outline_panel.cached_entries,
7185 outline_panel.selected_entry(),
7186 cx,
7187 ),
7188 indoc!(
7189 "
7190outline: mod outer <==== selected
7191 outline: pub struct OuterStruct
7192 outline: field: String
7193 outline: impl OuterStruct
7194 outline: pub fn new()
7195 outline: pub fn method(&self)
7196 outline: mod inner
7197 outline: pub fn inner_function()
7198 outline: pub struct InnerStruct
7199 outline: value: i32
7200outline: fn main()"
7201 )
7202 );
7203 });
7204
7205 let parent_outline = outline_panel
7206 .read_with(cx, |panel, _cx| {
7207 panel
7208 .cached_entries
7209 .iter()
7210 .find_map(|entry| match &entry.entry {
7211 PanelEntry::Outline(OutlineEntry::Outline(outline))
7212 if panel
7213 .outline_children_cache
7214 .get(&outline.buffer_id)
7215 .and_then(|children_map| {
7216 let key =
7217 (outline.outline.range.clone(), outline.outline.depth);
7218 children_map.get(&key)
7219 })
7220 .copied()
7221 .unwrap_or(false) =>
7222 {
7223 Some(entry.entry.clone())
7224 }
7225 _ => None,
7226 })
7227 })
7228 .expect("Should find an outline with children");
7229
7230 outline_panel.update_in(cx, |panel, window, cx| {
7231 panel.select_entry(parent_outline.clone(), true, window, cx);
7232 panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7233 });
7234 cx.executor()
7235 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7236 cx.run_until_parked();
7237
7238 outline_panel.update(cx, |outline_panel, cx| {
7239 assert_eq!(
7240 display_entries(
7241 &project,
7242 &snapshot(outline_panel, cx),
7243 &outline_panel.cached_entries,
7244 outline_panel.selected_entry(),
7245 cx,
7246 ),
7247 indoc!(
7248 "
7249outline: mod outer <==== selected
7250outline: fn main()"
7251 )
7252 );
7253 });
7254
7255 outline_panel.update_in(cx, |panel, window, cx| {
7256 panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7257 });
7258 cx.executor()
7259 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7260 cx.run_until_parked();
7261
7262 outline_panel.update(cx, |outline_panel, cx| {
7263 assert_eq!(
7264 display_entries(
7265 &project,
7266 &snapshot(outline_panel, cx),
7267 &outline_panel.cached_entries,
7268 outline_panel.selected_entry(),
7269 cx,
7270 ),
7271 indoc!(
7272 "
7273outline: mod outer <==== selected
7274 outline: pub struct OuterStruct
7275 outline: field: String
7276 outline: impl OuterStruct
7277 outline: pub fn new()
7278 outline: pub fn method(&self)
7279 outline: mod inner
7280 outline: pub fn inner_function()
7281 outline: pub struct InnerStruct
7282 outline: value: i32
7283outline: fn main()"
7284 )
7285 );
7286 });
7287
7288 outline_panel.update_in(cx, |panel, window, cx| {
7289 panel.collapsed_entries.clear();
7290 panel.update_cached_entries(None, window, cx);
7291 });
7292 cx.executor()
7293 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7294 cx.run_until_parked();
7295
7296 outline_panel.update_in(cx, |panel, window, cx| {
7297 let outlines_with_children: Vec<_> = panel
7298 .cached_entries
7299 .iter()
7300 .filter_map(|entry| match &entry.entry {
7301 PanelEntry::Outline(OutlineEntry::Outline(outline))
7302 if panel
7303 .outline_children_cache
7304 .get(&outline.buffer_id)
7305 .and_then(|children_map| {
7306 let key = (outline.outline.range.clone(), outline.outline.depth);
7307 children_map.get(&key)
7308 })
7309 .copied()
7310 .unwrap_or(false) =>
7311 {
7312 Some(entry.entry.clone())
7313 }
7314 _ => None,
7315 })
7316 .collect();
7317
7318 for outline in outlines_with_children {
7319 panel.select_entry(outline, false, window, cx);
7320 panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7321 }
7322 });
7323 cx.executor()
7324 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7325 cx.run_until_parked();
7326
7327 outline_panel.update(cx, |outline_panel, cx| {
7328 assert_eq!(
7329 display_entries(
7330 &project,
7331 &snapshot(outline_panel, cx),
7332 &outline_panel.cached_entries,
7333 outline_panel.selected_entry(),
7334 cx,
7335 ),
7336 indoc!(
7337 "
7338outline: mod outer
7339outline: fn main()"
7340 )
7341 );
7342 });
7343
7344 let collapsed_entries_count =
7345 outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len());
7346 assert!(
7347 collapsed_entries_count > 0,
7348 "Should have collapsed entries tracked"
7349 );
7350 }
7351
7352 #[gpui::test]
7353 async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) {
7354 init_test(cx);
7355
7356 let fs = FakeFs::new(cx.background_executor.clone());
7357 fs.insert_tree(
7358 "/test",
7359 json!({
7360 "src": {
7361 "main.rs": indoc!("
7362 struct Config {
7363 name: String,
7364 value: i32,
7365 }
7366 impl Config {
7367 fn new(name: String) -> Self {
7368 Self { name, value: 0 }
7369 }
7370 fn get_value(&self) -> i32 {
7371 self.value
7372 }
7373 }
7374 enum Status {
7375 Active,
7376 Inactive,
7377 }
7378 fn process_config(config: Config) -> Status {
7379 if config.get_value() > 0 {
7380 Status::Active
7381 } else {
7382 Status::Inactive
7383 }
7384 }
7385 fn main() {
7386 let config = Config::new(\"test\".to_string());
7387 let status = process_config(config);
7388 }
7389 "),
7390 }
7391 }),
7392 )
7393 .await;
7394
7395 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7396 project.read_with(cx, |project, _| {
7397 project.languages().add(Arc::new(
7398 rust_lang()
7399 .with_outline_query(
7400 r#"
7401 (struct_item
7402 (visibility_modifier)? @context
7403 "struct" @context
7404 name: (_) @name) @item
7405 (impl_item
7406 "impl" @context
7407 trait: (_)? @context
7408 "for"? @context
7409 type: (_) @context
7410 body: (_)) @item
7411 (function_item
7412 (visibility_modifier)? @context
7413 "fn" @context
7414 name: (_) @name
7415 parameters: (_) @context) @item
7416 (mod_item
7417 (visibility_modifier)? @context
7418 "mod" @context
7419 name: (_) @name) @item
7420 (enum_item
7421 (visibility_modifier)? @context
7422 "enum" @context
7423 name: (_) @name) @item
7424 (field_declaration
7425 (visibility_modifier)? @context
7426 name: (_) @name
7427 ":" @context
7428 type: (_) @context) @item
7429 "#,
7430 )
7431 .unwrap(),
7432 ))
7433 });
7434
7435 let workspace = add_outline_panel(&project, cx).await;
7436 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7437 let outline_panel = outline_panel(&workspace, cx);
7438
7439 outline_panel.update_in(cx, |outline_panel, window, cx| {
7440 outline_panel.set_active(true, window, cx)
7441 });
7442
7443 let _editor = workspace
7444 .update(cx, |workspace, window, cx| {
7445 workspace.open_abs_path(
7446 PathBuf::from("/test/src/main.rs"),
7447 OpenOptions {
7448 visible: Some(OpenVisible::All),
7449 ..Default::default()
7450 },
7451 window,
7452 cx,
7453 )
7454 })
7455 .unwrap()
7456 .await
7457 .unwrap();
7458
7459 cx.executor()
7460 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7461 cx.run_until_parked();
7462
7463 outline_panel.update(cx, |outline_panel, _cx| {
7464 outline_panel.selected_entry = SelectedEntry::None;
7465 });
7466
7467 // Check initial state - all entries should be expanded by default
7468 outline_panel.update(cx, |outline_panel, cx| {
7469 assert_eq!(
7470 display_entries(
7471 &project,
7472 &snapshot(outline_panel, cx),
7473 &outline_panel.cached_entries,
7474 outline_panel.selected_entry(),
7475 cx,
7476 ),
7477 indoc!(
7478 "
7479outline: struct Config
7480 outline: name: String
7481 outline: value: i32
7482outline: impl Config
7483 outline: fn new(name: String)
7484 outline: fn get_value(&self)
7485outline: enum Status
7486outline: fn process_config(config: Config)
7487outline: fn main()"
7488 )
7489 );
7490 });
7491
7492 outline_panel.update(cx, |outline_panel, _cx| {
7493 outline_panel.selected_entry = SelectedEntry::None;
7494 });
7495
7496 cx.update(|window, cx| {
7497 outline_panel.update(cx, |outline_panel, cx| {
7498 outline_panel.select_first(&SelectFirst, window, cx);
7499 });
7500 });
7501
7502 cx.executor()
7503 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7504 cx.run_until_parked();
7505
7506 outline_panel.update(cx, |outline_panel, cx| {
7507 assert_eq!(
7508 display_entries(
7509 &project,
7510 &snapshot(outline_panel, cx),
7511 &outline_panel.cached_entries,
7512 outline_panel.selected_entry(),
7513 cx,
7514 ),
7515 indoc!(
7516 "
7517outline: struct Config <==== selected
7518 outline: name: String
7519 outline: value: i32
7520outline: impl Config
7521 outline: fn new(name: String)
7522 outline: fn get_value(&self)
7523outline: enum Status
7524outline: fn process_config(config: Config)
7525outline: fn main()"
7526 )
7527 );
7528 });
7529
7530 cx.update(|window, cx| {
7531 outline_panel.update(cx, |outline_panel, cx| {
7532 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7533 });
7534 });
7535
7536 cx.executor()
7537 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7538 cx.run_until_parked();
7539
7540 outline_panel.update(cx, |outline_panel, cx| {
7541 assert_eq!(
7542 display_entries(
7543 &project,
7544 &snapshot(outline_panel, cx),
7545 &outline_panel.cached_entries,
7546 outline_panel.selected_entry(),
7547 cx,
7548 ),
7549 indoc!(
7550 "
7551outline: struct Config <==== selected
7552outline: impl Config
7553 outline: fn new(name: String)
7554 outline: fn get_value(&self)
7555outline: enum Status
7556outline: fn process_config(config: Config)
7557outline: fn main()"
7558 )
7559 );
7560 });
7561
7562 cx.update(|window, cx| {
7563 outline_panel.update(cx, |outline_panel, cx| {
7564 outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7565 });
7566 });
7567
7568 cx.executor()
7569 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7570 cx.run_until_parked();
7571
7572 outline_panel.update(cx, |outline_panel, cx| {
7573 assert_eq!(
7574 display_entries(
7575 &project,
7576 &snapshot(outline_panel, cx),
7577 &outline_panel.cached_entries,
7578 outline_panel.selected_entry(),
7579 cx,
7580 ),
7581 indoc!(
7582 "
7583outline: struct Config <==== selected
7584 outline: name: String
7585 outline: value: i32
7586outline: impl Config
7587 outline: fn new(name: String)
7588 outline: fn get_value(&self)
7589outline: enum Status
7590outline: fn process_config(config: Config)
7591outline: fn main()"
7592 )
7593 );
7594 });
7595 }
7596
7597 #[gpui::test]
7598 async fn test_outline_expand_collapse_all(cx: &mut TestAppContext) {
7599 init_test(cx);
7600
7601 let fs = FakeFs::new(cx.background_executor.clone());
7602 fs.insert_tree(
7603 "/test",
7604 json!({
7605 "src": {
7606 "lib.rs": indoc!("
7607 mod outer {
7608 pub struct OuterStruct {
7609 field: String,
7610 }
7611 impl OuterStruct {
7612 pub fn new() -> Self {
7613 Self { field: String::new() }
7614 }
7615 pub fn method(&self) {
7616 println!(\"{}\", self.field);
7617 }
7618 }
7619 mod inner {
7620 pub fn inner_function() {
7621 let x = 42;
7622 println!(\"{}\", x);
7623 }
7624 pub struct InnerStruct {
7625 value: i32,
7626 }
7627 }
7628 }
7629 fn main() {
7630 let s = outer::OuterStruct::new();
7631 s.method();
7632 }
7633 "),
7634 }
7635 }),
7636 )
7637 .await;
7638
7639 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7640 project.read_with(cx, |project, _| {
7641 project.languages().add(Arc::new(
7642 rust_lang()
7643 .with_outline_query(
7644 r#"
7645 (struct_item
7646 (visibility_modifier)? @context
7647 "struct" @context
7648 name: (_) @name) @item
7649 (impl_item
7650 "impl" @context
7651 trait: (_)? @context
7652 "for"? @context
7653 type: (_) @context
7654 body: (_)) @item
7655 (function_item
7656 (visibility_modifier)? @context
7657 "fn" @context
7658 name: (_) @name
7659 parameters: (_) @context) @item
7660 (mod_item
7661 (visibility_modifier)? @context
7662 "mod" @context
7663 name: (_) @name) @item
7664 (enum_item
7665 (visibility_modifier)? @context
7666 "enum" @context
7667 name: (_) @name) @item
7668 (field_declaration
7669 (visibility_modifier)? @context
7670 name: (_) @name
7671 ":" @context
7672 type: (_) @context) @item
7673 "#,
7674 )
7675 .unwrap(),
7676 ))
7677 });
7678 let workspace = add_outline_panel(&project, cx).await;
7679 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7680 let outline_panel = outline_panel(&workspace, cx);
7681
7682 outline_panel.update_in(cx, |outline_panel, window, cx| {
7683 outline_panel.set_active(true, window, cx)
7684 });
7685
7686 workspace
7687 .update(cx, |workspace, window, cx| {
7688 workspace.open_abs_path(
7689 PathBuf::from("/test/src/lib.rs"),
7690 OpenOptions {
7691 visible: Some(OpenVisible::All),
7692 ..Default::default()
7693 },
7694 window,
7695 cx,
7696 )
7697 })
7698 .unwrap()
7699 .await
7700 .unwrap();
7701
7702 cx.executor()
7703 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7704 cx.run_until_parked();
7705
7706 // Force another update cycle to ensure outlines are fetched
7707 outline_panel.update_in(cx, |panel, window, cx| {
7708 panel.update_non_fs_items(window, cx);
7709 panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7710 });
7711 cx.executor()
7712 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7713 cx.run_until_parked();
7714
7715 outline_panel.update(cx, |outline_panel, cx| {
7716 assert_eq!(
7717 display_entries(
7718 &project,
7719 &snapshot(outline_panel, cx),
7720 &outline_panel.cached_entries,
7721 outline_panel.selected_entry(),
7722 cx,
7723 ),
7724 indoc!(
7725 "
7726outline: mod outer <==== selected
7727 outline: pub struct OuterStruct
7728 outline: field: String
7729 outline: impl OuterStruct
7730 outline: pub fn new()
7731 outline: pub fn method(&self)
7732 outline: mod inner
7733 outline: pub fn inner_function()
7734 outline: pub struct InnerStruct
7735 outline: value: i32
7736outline: fn main()"
7737 )
7738 );
7739 });
7740
7741 let _parent_outline = outline_panel
7742 .read_with(cx, |panel, _cx| {
7743 panel
7744 .cached_entries
7745 .iter()
7746 .find_map(|entry| match &entry.entry {
7747 PanelEntry::Outline(OutlineEntry::Outline(outline))
7748 if panel
7749 .outline_children_cache
7750 .get(&outline.buffer_id)
7751 .and_then(|children_map| {
7752 let key =
7753 (outline.outline.range.clone(), outline.outline.depth);
7754 children_map.get(&key)
7755 })
7756 .copied()
7757 .unwrap_or(false) =>
7758 {
7759 Some(entry.entry.clone())
7760 }
7761 _ => None,
7762 })
7763 })
7764 .expect("Should find an outline with children");
7765
7766 // Collapse all entries
7767 outline_panel.update_in(cx, |panel, window, cx| {
7768 panel.collapse_all_entries(&CollapseAllEntries, window, cx);
7769 });
7770 cx.executor()
7771 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7772 cx.run_until_parked();
7773
7774 let expected_collapsed_output = indoc!(
7775 "
7776 outline: mod outer <==== selected
7777 outline: fn main()"
7778 );
7779
7780 outline_panel.update(cx, |panel, cx| {
7781 assert_eq! {
7782 display_entries(
7783 &project,
7784 &snapshot(panel, cx),
7785 &panel.cached_entries,
7786 panel.selected_entry(),
7787 cx,
7788 ),
7789 expected_collapsed_output
7790 };
7791 });
7792
7793 // Expand all entries
7794 outline_panel.update_in(cx, |panel, window, cx| {
7795 panel.expand_all_entries(&ExpandAllEntries, window, cx);
7796 });
7797 cx.executor()
7798 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7799 cx.run_until_parked();
7800
7801 let expected_expanded_output = indoc!(
7802 "
7803 outline: mod outer <==== selected
7804 outline: pub struct OuterStruct
7805 outline: field: String
7806 outline: impl OuterStruct
7807 outline: pub fn new()
7808 outline: pub fn method(&self)
7809 outline: mod inner
7810 outline: pub fn inner_function()
7811 outline: pub struct InnerStruct
7812 outline: value: i32
7813 outline: fn main()"
7814 );
7815
7816 outline_panel.update(cx, |panel, cx| {
7817 assert_eq! {
7818 display_entries(
7819 &project,
7820 &snapshot(panel, cx),
7821 &panel.cached_entries,
7822 panel.selected_entry(),
7823 cx,
7824 ),
7825 expected_expanded_output
7826 };
7827 });
7828 }
7829
7830 #[gpui::test]
7831 async fn test_buffer_search(cx: &mut TestAppContext) {
7832 init_test(cx);
7833
7834 let fs = FakeFs::new(cx.background_executor.clone());
7835 fs.insert_tree(
7836 "/test",
7837 json!({
7838 "foo.txt": r#"<_constitution>
7839
7840</_constitution>
7841
7842
7843
7844## 📊 Output
7845
7846| Field | Meaning |
7847"#
7848 }),
7849 )
7850 .await;
7851
7852 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7853 let workspace = add_outline_panel(&project, cx).await;
7854 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7855
7856 let editor = workspace
7857 .update(cx, |workspace, window, cx| {
7858 workspace.open_abs_path(
7859 PathBuf::from("/test/foo.txt"),
7860 OpenOptions {
7861 visible: Some(OpenVisible::All),
7862 ..OpenOptions::default()
7863 },
7864 window,
7865 cx,
7866 )
7867 })
7868 .unwrap()
7869 .await
7870 .unwrap()
7871 .downcast::<Editor>()
7872 .unwrap();
7873
7874 let search_bar = workspace
7875 .update(cx, |_, window, cx| {
7876 cx.new(|cx| {
7877 let mut search_bar = BufferSearchBar::new(None, window, cx);
7878 search_bar.set_active_pane_item(Some(&editor), window, cx);
7879 search_bar.show(window, cx);
7880 search_bar
7881 })
7882 })
7883 .unwrap();
7884
7885 let outline_panel = outline_panel(&workspace, cx);
7886
7887 outline_panel.update_in(cx, |outline_panel, window, cx| {
7888 outline_panel.set_active(true, window, cx)
7889 });
7890
7891 search_bar
7892 .update_in(cx, |search_bar, window, cx| {
7893 search_bar.search(" ", None, true, window, cx)
7894 })
7895 .await
7896 .unwrap();
7897
7898 cx.executor()
7899 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7900 cx.run_until_parked();
7901
7902 outline_panel.update(cx, |outline_panel, cx| {
7903 assert_eq!(
7904 display_entries(
7905 &project,
7906 &snapshot(outline_panel, cx),
7907 &outline_panel.cached_entries,
7908 outline_panel.selected_entry(),
7909 cx,
7910 ),
7911 "search: | Field« » | Meaning | <==== selected
7912search: | Field « » | Meaning |
7913search: | Field « » | Meaning |
7914search: | Field « » | Meaning |
7915search: | Field « »| Meaning |
7916search: | Field | Meaning« » |
7917search: | Field | Meaning « » |
7918search: | Field | Meaning « » |
7919search: | Field | Meaning « » |
7920search: | Field | Meaning « » |
7921search: | Field | Meaning « » |
7922search: | Field | Meaning « » |
7923search: | Field | Meaning « »|"
7924 );
7925 });
7926 }
7927}