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