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