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