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