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