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