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