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