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