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.extend(
1615 self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry),
1616 );
1617 }
1618 folded
1619 }
1620 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
1621 .collapsed_entries
1622 .insert(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)),
1623 PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1624 self.collapsed_entries.insert(CollapsedEntry::Outline(
1625 outline.buffer_id,
1626 outline.excerpt_id,
1627 outline.outline.range.clone(),
1628 ))
1629 }
1630 PanelEntry::Search(_) => false,
1631 };
1632
1633 if collapsed {
1634 active_editor.update(cx, |editor, cx| {
1635 buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
1636 });
1637 self.select_entry(selected_entry, true, window, cx);
1638 if buffers_to_fold.is_empty() {
1639 self.update_cached_entries(None, window, cx);
1640 } else {
1641 self.toggle_buffers_fold(buffers_to_fold, true, window, cx)
1642 .detach();
1643 }
1644 } else {
1645 self.select_parent(&SelectParent, window, cx);
1646 }
1647 }
1648
1649 pub fn expand_all_entries(
1650 &mut self,
1651 _: &ExpandAllEntries,
1652 window: &mut Window,
1653 cx: &mut Context<Self>,
1654 ) {
1655 let Some(active_editor) = self.active_editor() else {
1656 return;
1657 };
1658 let mut buffers_to_unfold = HashSet::default();
1659 let expanded_entries =
1660 self.fs_entries
1661 .iter()
1662 .fold(HashSet::default(), |mut entries, fs_entry| {
1663 match fs_entry {
1664 FsEntry::ExternalFile(external_file) => {
1665 buffers_to_unfold.insert(external_file.buffer_id);
1666 entries.insert(CollapsedEntry::ExternalFile(external_file.buffer_id));
1667 entries.extend(
1668 self.excerpts
1669 .get(&external_file.buffer_id)
1670 .into_iter()
1671 .flat_map(|excerpts| {
1672 excerpts.keys().map(|excerpt_id| {
1673 CollapsedEntry::Excerpt(
1674 external_file.buffer_id,
1675 *excerpt_id,
1676 )
1677 })
1678 }),
1679 );
1680 }
1681 FsEntry::Directory(directory) => {
1682 entries.insert(CollapsedEntry::Dir(
1683 directory.worktree_id,
1684 directory.entry.id,
1685 ));
1686 }
1687 FsEntry::File(file) => {
1688 buffers_to_unfold.insert(file.buffer_id);
1689 entries.insert(CollapsedEntry::File(file.worktree_id, file.buffer_id));
1690 entries.extend(
1691 self.excerpts.get(&file.buffer_id).into_iter().flat_map(
1692 |excerpts| {
1693 excerpts.keys().map(|excerpt_id| {
1694 CollapsedEntry::Excerpt(file.buffer_id, *excerpt_id)
1695 })
1696 },
1697 ),
1698 );
1699 }
1700 };
1701 entries
1702 });
1703 self.collapsed_entries
1704 .retain(|entry| !expanded_entries.contains(entry));
1705 active_editor.update(cx, |editor, cx| {
1706 buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
1707 });
1708 if buffers_to_unfold.is_empty() {
1709 self.update_cached_entries(None, window, cx);
1710 } else {
1711 self.toggle_buffers_fold(buffers_to_unfold, false, window, cx)
1712 .detach();
1713 }
1714 }
1715
1716 pub fn collapse_all_entries(
1717 &mut self,
1718 _: &CollapseAllEntries,
1719 window: &mut Window,
1720 cx: &mut Context<Self>,
1721 ) {
1722 let Some(active_editor) = self.active_editor() else {
1723 return;
1724 };
1725 let mut buffers_to_fold = HashSet::default();
1726 let new_entries = self
1727 .cached_entries
1728 .iter()
1729 .flat_map(|cached_entry| match &cached_entry.entry {
1730 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1731 worktree_id, entry, ..
1732 })) => Some(CollapsedEntry::Dir(*worktree_id, entry.id)),
1733 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1734 worktree_id,
1735 buffer_id,
1736 ..
1737 })) => {
1738 buffers_to_fold.insert(*buffer_id);
1739 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1740 }
1741 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1742 buffers_to_fold.insert(external_file.buffer_id);
1743 Some(CollapsedEntry::ExternalFile(external_file.buffer_id))
1744 }
1745 PanelEntry::FoldedDirs(FoldedDirsEntry {
1746 worktree_id,
1747 entries,
1748 ..
1749 }) => Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)),
1750 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1751 Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id))
1752 }
1753 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1754 })
1755 .collect::<Vec<_>>();
1756 self.collapsed_entries.extend(new_entries);
1757
1758 active_editor.update(cx, |editor, cx| {
1759 buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
1760 });
1761 if buffers_to_fold.is_empty() {
1762 self.update_cached_entries(None, window, cx);
1763 } else {
1764 self.toggle_buffers_fold(buffers_to_fold, true, window, cx)
1765 .detach();
1766 }
1767 }
1768
1769 fn toggle_expanded(&mut self, entry: &PanelEntry, window: &mut Window, cx: &mut Context<Self>) {
1770 let Some(active_editor) = self.active_editor() else {
1771 return;
1772 };
1773 let mut fold = false;
1774 let mut buffers_to_toggle = HashSet::default();
1775 match entry {
1776 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1777 worktree_id,
1778 entry: dir_entry,
1779 ..
1780 })) => {
1781 let entry_id = dir_entry.id;
1782 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1783 buffers_to_toggle.extend(self.buffers_inside_directory(*worktree_id, dir_entry));
1784 if self.collapsed_entries.remove(&collapsed_entry) {
1785 self.project
1786 .update(cx, |project, cx| {
1787 project.expand_entry(*worktree_id, entry_id, cx)
1788 })
1789 .unwrap_or_else(|| Task::ready(Ok(())))
1790 .detach_and_log_err(cx);
1791 } else {
1792 self.collapsed_entries.insert(collapsed_entry);
1793 fold = true;
1794 }
1795 }
1796 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1797 worktree_id,
1798 buffer_id,
1799 ..
1800 })) => {
1801 let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1802 buffers_to_toggle.insert(*buffer_id);
1803 if !self.collapsed_entries.remove(&collapsed_entry) {
1804 self.collapsed_entries.insert(collapsed_entry);
1805 fold = true;
1806 }
1807 }
1808 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1809 let collapsed_entry = CollapsedEntry::ExternalFile(external_file.buffer_id);
1810 buffers_to_toggle.insert(external_file.buffer_id);
1811 if !self.collapsed_entries.remove(&collapsed_entry) {
1812 self.collapsed_entries.insert(collapsed_entry);
1813 fold = true;
1814 }
1815 }
1816 PanelEntry::FoldedDirs(FoldedDirsEntry {
1817 worktree_id,
1818 entries: dir_entries,
1819 ..
1820 }) => {
1821 if let Some(dir_entry) = dir_entries.first() {
1822 let entry_id = dir_entry.id;
1823 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1824 buffers_to_toggle
1825 .extend(self.buffers_inside_directory(*worktree_id, dir_entry));
1826 if self.collapsed_entries.remove(&collapsed_entry) {
1827 self.project
1828 .update(cx, |project, cx| {
1829 project.expand_entry(*worktree_id, entry_id, cx)
1830 })
1831 .unwrap_or_else(|| Task::ready(Ok(())))
1832 .detach_and_log_err(cx);
1833 } else {
1834 self.collapsed_entries.insert(collapsed_entry);
1835 fold = true;
1836 }
1837 }
1838 }
1839 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1840 let collapsed_entry = CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id);
1841 if !self.collapsed_entries.remove(&collapsed_entry) {
1842 self.collapsed_entries.insert(collapsed_entry);
1843 }
1844 }
1845 PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1846 let collapsed_entry = CollapsedEntry::Outline(
1847 outline.buffer_id,
1848 outline.excerpt_id,
1849 outline.outline.range.clone(),
1850 );
1851 if !self.collapsed_entries.remove(&collapsed_entry) {
1852 self.collapsed_entries.insert(collapsed_entry);
1853 }
1854 }
1855 _ => {}
1856 }
1857
1858 active_editor.update(cx, |editor, cx| {
1859 buffers_to_toggle.retain(|buffer_id| {
1860 let folded = editor.is_buffer_folded(*buffer_id, cx);
1861 if fold { !folded } else { folded }
1862 });
1863 });
1864
1865 self.select_entry(entry.clone(), true, window, cx);
1866 if buffers_to_toggle.is_empty() {
1867 self.update_cached_entries(None, window, cx);
1868 } else {
1869 self.toggle_buffers_fold(buffers_to_toggle, fold, window, cx)
1870 .detach();
1871 }
1872 }
1873
1874 fn toggle_buffers_fold(
1875 &self,
1876 buffers: HashSet<BufferId>,
1877 fold: bool,
1878 window: &mut Window,
1879 cx: &mut Context<Self>,
1880 ) -> Task<()> {
1881 let Some(active_editor) = self.active_editor() else {
1882 return Task::ready(());
1883 };
1884 cx.spawn_in(window, async move |outline_panel, cx| {
1885 outline_panel
1886 .update_in(cx, |outline_panel, window, cx| {
1887 active_editor.update(cx, |editor, cx| {
1888 for buffer_id in buffers {
1889 outline_panel
1890 .preserve_selection_on_buffer_fold_toggles
1891 .insert(buffer_id);
1892 if fold {
1893 editor.fold_buffer(buffer_id, cx);
1894 } else {
1895 editor.unfold_buffer(buffer_id, cx);
1896 }
1897 }
1898 });
1899 if let Some(selection) = outline_panel.selected_entry().cloned() {
1900 outline_panel.scroll_editor_to_entry(&selection, false, false, window, cx);
1901 }
1902 })
1903 .ok();
1904 })
1905 }
1906
1907 fn copy_path(
1908 &mut self,
1909 _: &zed_actions::workspace::CopyPath,
1910 _: &mut Window,
1911 cx: &mut Context<Self>,
1912 ) {
1913 if let Some(clipboard_text) = self
1914 .selected_entry()
1915 .and_then(|entry| self.abs_path(entry, cx))
1916 .map(|p| p.to_string_lossy().to_string())
1917 {
1918 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1919 }
1920 }
1921
1922 fn copy_relative_path(
1923 &mut self,
1924 _: &zed_actions::workspace::CopyRelativePath,
1925 _: &mut Window,
1926 cx: &mut Context<Self>,
1927 ) {
1928 if let Some(clipboard_text) = self
1929 .selected_entry()
1930 .and_then(|entry| match entry {
1931 PanelEntry::Fs(entry) => self.relative_path(entry, cx),
1932 PanelEntry::FoldedDirs(folded_dirs) => {
1933 folded_dirs.entries.last().map(|entry| entry.path.clone())
1934 }
1935 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1936 })
1937 .map(|p| p.to_string_lossy().to_string())
1938 {
1939 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1940 }
1941 }
1942
1943 fn reveal_in_finder(
1944 &mut self,
1945 _: &RevealInFileManager,
1946 _: &mut Window,
1947 cx: &mut Context<Self>,
1948 ) {
1949 if let Some(abs_path) = self
1950 .selected_entry()
1951 .and_then(|entry| self.abs_path(entry, cx))
1952 {
1953 cx.reveal_path(&abs_path);
1954 }
1955 }
1956
1957 fn open_in_terminal(
1958 &mut self,
1959 _: &OpenInTerminal,
1960 window: &mut Window,
1961 cx: &mut Context<Self>,
1962 ) {
1963 let selected_entry = self.selected_entry();
1964 let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx));
1965 let working_directory = if let (
1966 Some(abs_path),
1967 Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1968 ) = (&abs_path, selected_entry)
1969 {
1970 abs_path.parent().map(|p| p.to_owned())
1971 } else {
1972 abs_path
1973 };
1974
1975 if let Some(working_directory) = working_directory {
1976 window.dispatch_action(
1977 workspace::OpenTerminal { working_directory }.boxed_clone(),
1978 cx,
1979 )
1980 }
1981 }
1982
1983 fn reveal_entry_for_selection(
1984 &mut self,
1985 editor: Entity<Editor>,
1986 window: &mut Window,
1987 cx: &mut Context<Self>,
1988 ) {
1989 if !self.active
1990 || !OutlinePanelSettings::get_global(cx).auto_reveal_entries
1991 || self.focus_handle.contains_focused(window, cx)
1992 {
1993 return;
1994 }
1995 let project = self.project.clone();
1996 self.reveal_selection_task = cx.spawn_in(window, async move |outline_panel, cx| {
1997 cx.background_executor().timer(UPDATE_DEBOUNCE).await;
1998 let entry_with_selection =
1999 outline_panel.update_in(cx, |outline_panel, window, cx| {
2000 outline_panel.location_for_editor_selection(&editor, window, cx)
2001 })?;
2002 let Some(entry_with_selection) = entry_with_selection else {
2003 outline_panel.update(cx, |outline_panel, cx| {
2004 outline_panel.selected_entry = SelectedEntry::None;
2005 cx.notify();
2006 })?;
2007 return Ok(());
2008 };
2009 let related_buffer_entry = match &entry_with_selection {
2010 PanelEntry::Fs(FsEntry::File(FsEntryFile {
2011 worktree_id,
2012 buffer_id,
2013 ..
2014 })) => project.update(cx, |project, cx| {
2015 let entry_id = project
2016 .buffer_for_id(*buffer_id, cx)
2017 .and_then(|buffer| buffer.read(cx).entry_id(cx));
2018 project
2019 .worktree_for_id(*worktree_id, cx)
2020 .zip(entry_id)
2021 .and_then(|(worktree, entry_id)| {
2022 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
2023 Some((worktree, entry))
2024 })
2025 })?,
2026 PanelEntry::Outline(outline_entry) => {
2027 let (buffer_id, excerpt_id) = outline_entry.ids();
2028 outline_panel.update(cx, |outline_panel, cx| {
2029 outline_panel
2030 .collapsed_entries
2031 .remove(&CollapsedEntry::ExternalFile(buffer_id));
2032 outline_panel
2033 .collapsed_entries
2034 .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
2035 let project = outline_panel.project.read(cx);
2036 let entry_id = project
2037 .buffer_for_id(buffer_id, cx)
2038 .and_then(|buffer| buffer.read(cx).entry_id(cx));
2039
2040 entry_id.and_then(|entry_id| {
2041 project
2042 .worktree_for_entry(entry_id, cx)
2043 .and_then(|worktree| {
2044 let worktree_id = worktree.read(cx).id();
2045 outline_panel
2046 .collapsed_entries
2047 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
2048 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
2049 Some((worktree, entry))
2050 })
2051 })
2052 })?
2053 }
2054 PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
2055 PanelEntry::Search(SearchEntry { match_range, .. }) => match_range
2056 .start
2057 .buffer_id
2058 .or(match_range.end.buffer_id)
2059 .map(|buffer_id| {
2060 outline_panel.update(cx, |outline_panel, cx| {
2061 outline_panel
2062 .collapsed_entries
2063 .remove(&CollapsedEntry::ExternalFile(buffer_id));
2064 let project = project.read(cx);
2065 let entry_id = project
2066 .buffer_for_id(buffer_id, cx)
2067 .and_then(|buffer| buffer.read(cx).entry_id(cx));
2068
2069 entry_id.and_then(|entry_id| {
2070 project
2071 .worktree_for_entry(entry_id, cx)
2072 .and_then(|worktree| {
2073 let worktree_id = worktree.read(cx).id();
2074 outline_panel
2075 .collapsed_entries
2076 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
2077 let entry =
2078 worktree.read(cx).entry_for_id(entry_id)?.clone();
2079 Some((worktree, entry))
2080 })
2081 })
2082 })
2083 })
2084 .transpose()?
2085 .flatten(),
2086 _ => return anyhow::Ok(()),
2087 };
2088 if let Some((worktree, buffer_entry)) = related_buffer_entry {
2089 outline_panel.update(cx, |outline_panel, cx| {
2090 let worktree_id = worktree.read(cx).id();
2091 let mut dirs_to_expand = Vec::new();
2092 {
2093 let mut traversal = worktree.read(cx).traverse_from_path(
2094 true,
2095 true,
2096 true,
2097 buffer_entry.path.as_ref(),
2098 );
2099 let mut current_entry = buffer_entry;
2100 loop {
2101 if current_entry.is_dir()
2102 && outline_panel
2103 .collapsed_entries
2104 .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
2105 {
2106 dirs_to_expand.push(current_entry.id);
2107 }
2108
2109 if traversal.back_to_parent()
2110 && let Some(parent_entry) = traversal.entry() {
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 search_state
2478 .highlight_search_match_tx
2479 .try_send(HighlightArguments {
2480 multi_buffer_snapshot: multi_buffer_snapshot.clone(),
2481 match_range: match_range.clone(),
2482 search_data: Arc::clone(render_data),
2483 })
2484 .ok();
2485 }
2486 return None;
2487 }
2488 };
2489 let search_matches = string_match
2490 .iter()
2491 .flat_map(|string_match| string_match.ranges())
2492 .collect::<Vec<_>>();
2493 let match_ranges = if search_matches.is_empty() {
2494 &search_data.search_match_indices
2495 } else {
2496 &search_matches
2497 };
2498 let label_element = outline::render_item(
2499 &OutlineItem {
2500 depth,
2501 annotation_range: None,
2502 range: search_data.context_range.clone(),
2503 text: search_data.context_text.clone(),
2504 highlight_ranges: search_data
2505 .highlights_data
2506 .get()
2507 .cloned()
2508 .unwrap_or_default(),
2509 name_ranges: search_data.search_match_indices.clone(),
2510 body_range: Some(search_data.context_range.clone()),
2511 },
2512 match_ranges.iter().cloned(),
2513 cx,
2514 );
2515 let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
2516 let entire_label = h_flex()
2517 .justify_center()
2518 .p_0()
2519 .when(search_data.truncated_left, |parent| {
2520 parent.child(truncated_contents_label())
2521 })
2522 .child(label_element)
2523 .when(search_data.truncated_right, |parent| {
2524 parent.child(truncated_contents_label())
2525 })
2526 .into_any_element();
2527
2528 let is_active = match self.selected_entry() {
2529 Some(PanelEntry::Search(SearchEntry {
2530 match_range: selected_match_range,
2531 ..
2532 })) => match_range == selected_match_range,
2533 _ => false,
2534 };
2535 Some(self.entry_element(
2536 PanelEntry::Search(SearchEntry {
2537 kind,
2538 match_range: match_range.clone(),
2539 render_data: render_data.clone(),
2540 }),
2541 ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
2542 depth,
2543 empty_icon(),
2544 is_active,
2545 entire_label,
2546 window,
2547 cx,
2548 ))
2549 }
2550
2551 fn entry_element(
2552 &self,
2553 rendered_entry: PanelEntry,
2554 item_id: ElementId,
2555 depth: usize,
2556 icon_element: AnyElement,
2557 is_active: bool,
2558 label_element: gpui::AnyElement,
2559 window: &mut Window,
2560 cx: &mut Context<OutlinePanel>,
2561 ) -> Stateful<Div> {
2562 let settings = OutlinePanelSettings::get_global(cx);
2563 div()
2564 .text_ui(cx)
2565 .id(item_id.clone())
2566 .on_click({
2567 let clicked_entry = rendered_entry.clone();
2568 cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| {
2569 if event.is_right_click() || event.first_focus() {
2570 return;
2571 }
2572
2573 let change_focus = event.click_count() > 1;
2574 outline_panel.toggle_expanded(&clicked_entry, window, cx);
2575
2576 outline_panel.scroll_editor_to_entry(
2577 &clicked_entry,
2578 true,
2579 change_focus,
2580 window,
2581 cx,
2582 );
2583 })
2584 })
2585 .cursor_pointer()
2586 .child(
2587 ListItem::new(item_id)
2588 .indent_level(depth)
2589 .indent_step_size(px(settings.indent_size))
2590 .toggle_state(is_active)
2591 .child(
2592 h_flex()
2593 .child(h_flex().w(px(16.)).justify_center().child(icon_element))
2594 .child(h_flex().h_6().child(label_element).ml_1()),
2595 )
2596 .on_secondary_mouse_down(cx.listener(
2597 move |outline_panel, event: &MouseDownEvent, window, cx| {
2598 // Stop propagation to prevent the catch-all context menu for the project
2599 // panel from being deployed.
2600 cx.stop_propagation();
2601 outline_panel.deploy_context_menu(
2602 event.position,
2603 rendered_entry.clone(),
2604 window,
2605 cx,
2606 )
2607 },
2608 )),
2609 )
2610 .border_1()
2611 .border_r_2()
2612 .rounded_none()
2613 .hover(|style| {
2614 if is_active {
2615 style
2616 } else {
2617 let hover_color = cx.theme().colors().ghost_element_hover;
2618 style.bg(hover_color).border_color(hover_color)
2619 }
2620 })
2621 .when(
2622 is_active && self.focus_handle.contains_focused(window, cx),
2623 |div| div.border_color(Color::Selected.color(cx)),
2624 )
2625 }
2626
2627 fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String {
2628 let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2629 Some(worktree) => {
2630 let worktree = worktree.read(cx);
2631 match worktree.snapshot().root_entry() {
2632 Some(root_entry) => {
2633 if root_entry.id == entry.id {
2634 file_name(worktree.abs_path().as_ref())
2635 } else {
2636 let path = worktree.absolutize(entry.path.as_ref()).ok();
2637 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2638 file_name(path)
2639 }
2640 }
2641 None => {
2642 let path = worktree.absolutize(entry.path.as_ref()).ok();
2643 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2644 file_name(path)
2645 }
2646 }
2647 }
2648 None => file_name(entry.path.as_ref()),
2649 };
2650 name
2651 }
2652
2653 fn update_fs_entries(
2654 &mut self,
2655 active_editor: Entity<Editor>,
2656 debounce: Option<Duration>,
2657 window: &mut Window,
2658 cx: &mut Context<Self>,
2659 ) {
2660 if !self.active {
2661 return;
2662 }
2663
2664 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2665 let active_multi_buffer = active_editor.read(cx).buffer().clone();
2666 let new_entries = self.new_entries_for_fs_update.clone();
2667 let repo_snapshots = self.project.update(cx, |project, cx| {
2668 project.git_store().read(cx).repo_snapshots(cx)
2669 });
2670 self.updating_fs_entries = true;
2671 self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
2672 if let Some(debounce) = debounce {
2673 cx.background_executor().timer(debounce).await;
2674 }
2675
2676 let mut new_collapsed_entries = HashSet::default();
2677 let mut new_unfolded_dirs = HashMap::default();
2678 let mut root_entries = HashSet::default();
2679 let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
2680 let Ok(buffer_excerpts) = outline_panel.update(cx, |outline_panel, cx| {
2681 let git_store = outline_panel.project.read(cx).git_store().clone();
2682 new_collapsed_entries = outline_panel.collapsed_entries.clone();
2683 new_unfolded_dirs = outline_panel.unfolded_dirs.clone();
2684 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
2685 let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
2686 HashMap::default(),
2687 |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
2688 let buffer_id = buffer_snapshot.remote_id();
2689 let file = File::from_dyn(buffer_snapshot.file());
2690 let entry_id = file.and_then(|file| file.project_entry_id(cx));
2691 let worktree = file.map(|file| file.worktree.read(cx).snapshot());
2692 let is_new = new_entries.contains(&excerpt_id)
2693 || !outline_panel.excerpts.contains_key(&buffer_id);
2694 let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
2695 let status = git_store
2696 .read(cx)
2697 .repository_and_path_for_buffer_id(buffer_id, cx)
2698 .and_then(|(repo, path)| {
2699 Some(repo.read(cx).status_for_path(&path)?.status)
2700 });
2701 buffer_excerpts
2702 .entry(buffer_id)
2703 .or_insert_with(|| {
2704 (is_new, is_folded, Vec::new(), entry_id, worktree, status)
2705 })
2706 .2
2707 .push(excerpt_id);
2708
2709 let outlines = match outline_panel
2710 .excerpts
2711 .get(&buffer_id)
2712 .and_then(|excerpts| excerpts.get(&excerpt_id))
2713 {
2714 Some(old_excerpt) => match &old_excerpt.outlines {
2715 ExcerptOutlines::Outlines(outlines) => {
2716 ExcerptOutlines::Outlines(outlines.clone())
2717 }
2718 ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
2719 ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
2720 },
2721 None => ExcerptOutlines::NotFetched,
2722 };
2723 new_excerpts.entry(buffer_id).or_default().insert(
2724 excerpt_id,
2725 Excerpt {
2726 range: excerpt_range,
2727 outlines,
2728 },
2729 );
2730 buffer_excerpts
2731 },
2732 );
2733 buffer_excerpts
2734 }) else {
2735 return;
2736 };
2737
2738 let Some((
2739 new_collapsed_entries,
2740 new_unfolded_dirs,
2741 new_fs_entries,
2742 new_depth_map,
2743 new_children_count,
2744 )) = cx
2745 .background_spawn(async move {
2746 let mut processed_external_buffers = HashSet::default();
2747 let mut new_worktree_entries =
2748 BTreeMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
2749 let mut worktree_excerpts = HashMap::<
2750 WorktreeId,
2751 HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
2752 >::default();
2753 let mut external_excerpts = HashMap::default();
2754
2755 for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree, status)) in
2756 buffer_excerpts
2757 {
2758 if is_folded {
2759 match &worktree {
2760 Some(worktree) => {
2761 new_collapsed_entries
2762 .insert(CollapsedEntry::File(worktree.id(), buffer_id));
2763 }
2764 None => {
2765 new_collapsed_entries
2766 .insert(CollapsedEntry::ExternalFile(buffer_id));
2767 }
2768 }
2769 } else if is_new {
2770 match &worktree {
2771 Some(worktree) => {
2772 new_collapsed_entries
2773 .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
2774 }
2775 None => {
2776 new_collapsed_entries
2777 .remove(&CollapsedEntry::ExternalFile(buffer_id));
2778 }
2779 }
2780 }
2781
2782 if let Some(worktree) = worktree {
2783 let worktree_id = worktree.id();
2784 let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
2785
2786 match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
2787 Some(entry) => {
2788 let entry = GitEntry {
2789 git_summary: status
2790 .map(|status| status.summary())
2791 .unwrap_or_default(),
2792 entry,
2793 };
2794 let mut traversal = GitTraversal::new(
2795 &repo_snapshots,
2796 worktree.traverse_from_path(
2797 true,
2798 true,
2799 true,
2800 entry.path.as_ref(),
2801 ),
2802 );
2803
2804 let mut entries_to_add = HashMap::default();
2805 worktree_excerpts
2806 .entry(worktree_id)
2807 .or_default()
2808 .insert(entry.id, (buffer_id, excerpts));
2809 let mut current_entry = entry;
2810 loop {
2811 if current_entry.is_dir() {
2812 let is_root =
2813 worktree.root_entry().map(|entry| entry.id)
2814 == Some(current_entry.id);
2815 if is_root {
2816 root_entries.insert(current_entry.id);
2817 if auto_fold_dirs {
2818 unfolded_dirs.insert(current_entry.id);
2819 }
2820 }
2821 if is_new {
2822 new_collapsed_entries.remove(&CollapsedEntry::Dir(
2823 worktree_id,
2824 current_entry.id,
2825 ));
2826 }
2827 }
2828
2829 let new_entry_added = entries_to_add
2830 .insert(current_entry.id, current_entry)
2831 .is_none();
2832 if new_entry_added && traversal.back_to_parent()
2833 && let Some(parent_entry) = traversal.entry() {
2834 current_entry = parent_entry.to_owned();
2835 continue;
2836 }
2837 break;
2838 }
2839 new_worktree_entries
2840 .entry(worktree_id)
2841 .or_insert_with(HashMap::default)
2842 .extend(entries_to_add);
2843 }
2844 None => {
2845 if processed_external_buffers.insert(buffer_id) {
2846 external_excerpts
2847 .entry(buffer_id)
2848 .or_insert_with(Vec::new)
2849 .extend(excerpts);
2850 }
2851 }
2852 }
2853 } else if processed_external_buffers.insert(buffer_id) {
2854 external_excerpts
2855 .entry(buffer_id)
2856 .or_insert_with(Vec::new)
2857 .extend(excerpts);
2858 }
2859 }
2860
2861 let mut new_children_count =
2862 HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
2863
2864 let worktree_entries = new_worktree_entries
2865 .into_iter()
2866 .map(|(worktree_id, entries)| {
2867 let mut entries = entries.into_values().collect::<Vec<_>>();
2868 entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
2869 (worktree_id, entries)
2870 })
2871 .flat_map(|(worktree_id, entries)| {
2872 {
2873 entries
2874 .into_iter()
2875 .filter_map(|entry| {
2876 if auto_fold_dirs
2877 && let Some(parent) = entry.path.parent() {
2878 let children = new_children_count
2879 .entry(worktree_id)
2880 .or_default()
2881 .entry(Arc::from(parent))
2882 .or_default();
2883 if entry.is_dir() {
2884 children.dirs += 1;
2885 } else {
2886 children.files += 1;
2887 }
2888 }
2889
2890 if entry.is_dir() {
2891 Some(FsEntry::Directory(FsEntryDirectory {
2892 worktree_id,
2893 entry,
2894 }))
2895 } else {
2896 let (buffer_id, excerpts) = worktree_excerpts
2897 .get_mut(&worktree_id)
2898 .and_then(|worktree_excerpts| {
2899 worktree_excerpts.remove(&entry.id)
2900 })?;
2901 Some(FsEntry::File(FsEntryFile {
2902 worktree_id,
2903 buffer_id,
2904 entry,
2905 excerpts,
2906 }))
2907 }
2908 })
2909 .collect::<Vec<_>>()
2910 }
2911 })
2912 .collect::<Vec<_>>();
2913
2914 let mut visited_dirs = Vec::new();
2915 let mut new_depth_map = HashMap::default();
2916 let new_visible_entries = external_excerpts
2917 .into_iter()
2918 .sorted_by_key(|(id, _)| *id)
2919 .map(|(buffer_id, excerpts)| {
2920 FsEntry::ExternalFile(FsEntryExternalFile {
2921 buffer_id,
2922 excerpts,
2923 })
2924 })
2925 .chain(worktree_entries)
2926 .filter(|visible_item| {
2927 match visible_item {
2928 FsEntry::Directory(directory) => {
2929 let parent_id = back_to_common_visited_parent(
2930 &mut visited_dirs,
2931 &directory.worktree_id,
2932 &directory.entry,
2933 );
2934
2935 let mut depth = 0;
2936 if !root_entries.contains(&directory.entry.id) {
2937 if auto_fold_dirs {
2938 let children = new_children_count
2939 .get(&directory.worktree_id)
2940 .and_then(|children_count| {
2941 children_count.get(&directory.entry.path)
2942 })
2943 .copied()
2944 .unwrap_or_default();
2945
2946 if !children.may_be_fold_part()
2947 || (children.dirs == 0
2948 && visited_dirs
2949 .last()
2950 .map(|(parent_dir_id, _)| {
2951 new_unfolded_dirs
2952 .get(&directory.worktree_id)
2953 .map_or(true, |unfolded_dirs| {
2954 unfolded_dirs
2955 .contains(parent_dir_id)
2956 })
2957 })
2958 .unwrap_or(true))
2959 {
2960 new_unfolded_dirs
2961 .entry(directory.worktree_id)
2962 .or_default()
2963 .insert(directory.entry.id);
2964 }
2965 }
2966
2967 depth = parent_id
2968 .and_then(|(worktree_id, id)| {
2969 new_depth_map.get(&(worktree_id, id)).copied()
2970 })
2971 .unwrap_or(0)
2972 + 1;
2973 };
2974 visited_dirs
2975 .push((directory.entry.id, directory.entry.path.clone()));
2976 new_depth_map
2977 .insert((directory.worktree_id, directory.entry.id), depth);
2978 }
2979 FsEntry::File(FsEntryFile {
2980 worktree_id,
2981 entry: file_entry,
2982 ..
2983 }) => {
2984 let parent_id = back_to_common_visited_parent(
2985 &mut visited_dirs,
2986 worktree_id,
2987 file_entry,
2988 );
2989 let depth = if root_entries.contains(&file_entry.id) {
2990 0
2991 } else {
2992 parent_id
2993 .and_then(|(worktree_id, id)| {
2994 new_depth_map.get(&(worktree_id, id)).copied()
2995 })
2996 .unwrap_or(0)
2997 + 1
2998 };
2999 new_depth_map.insert((*worktree_id, file_entry.id), depth);
3000 }
3001 FsEntry::ExternalFile(..) => {
3002 visited_dirs.clear();
3003 }
3004 }
3005
3006 true
3007 })
3008 .collect::<Vec<_>>();
3009
3010 anyhow::Ok((
3011 new_collapsed_entries,
3012 new_unfolded_dirs,
3013 new_visible_entries,
3014 new_depth_map,
3015 new_children_count,
3016 ))
3017 })
3018 .await
3019 .log_err()
3020 else {
3021 return;
3022 };
3023
3024 outline_panel
3025 .update_in(cx, |outline_panel, window, cx| {
3026 outline_panel.updating_fs_entries = false;
3027 outline_panel.new_entries_for_fs_update.clear();
3028 outline_panel.excerpts = new_excerpts;
3029 outline_panel.collapsed_entries = new_collapsed_entries;
3030 outline_panel.unfolded_dirs = new_unfolded_dirs;
3031 outline_panel.fs_entries = new_fs_entries;
3032 outline_panel.fs_entries_depth = new_depth_map;
3033 outline_panel.fs_children_count = new_children_count;
3034 outline_panel.update_non_fs_items(window, cx);
3035
3036 // Only update cached entries if we don't have outlines to fetch
3037 // If we do have outlines to fetch, let fetch_outdated_outlines handle the update
3038 if outline_panel.excerpt_fetch_ranges(cx).is_empty() {
3039 outline_panel.update_cached_entries(debounce, window, cx);
3040 }
3041
3042 cx.notify();
3043 })
3044 .ok();
3045 });
3046 }
3047
3048 fn replace_active_editor(
3049 &mut self,
3050 new_active_item: Box<dyn ItemHandle>,
3051 new_active_editor: Entity<Editor>,
3052 window: &mut Window,
3053 cx: &mut Context<Self>,
3054 ) {
3055 self.clear_previous(window, cx);
3056
3057 let default_expansion_depth =
3058 OutlinePanelSettings::get_global(cx).expand_outlines_with_depth;
3059 // We'll apply the expansion depth after outlines are loaded
3060 self.pending_default_expansion_depth = Some(default_expansion_depth);
3061
3062 let buffer_search_subscription = cx.subscribe_in(
3063 &new_active_editor,
3064 window,
3065 |outline_panel: &mut Self,
3066 _,
3067 e: &SearchEvent,
3068 window: &mut Window,
3069 cx: &mut Context<Self>| {
3070 if matches!(e, SearchEvent::MatchesInvalidated) {
3071 let update_cached_items = outline_panel.update_search_matches(window, cx);
3072 if update_cached_items {
3073 outline_panel.selected_entry.invalidate();
3074 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
3075 }
3076 };
3077 outline_panel.autoscroll(cx);
3078 },
3079 );
3080 self.active_item = Some(ActiveItem {
3081 _buffer_search_subscription: buffer_search_subscription,
3082 _editor_subscription: subscribe_for_editor_events(&new_active_editor, window, cx),
3083 item_handle: new_active_item.downgrade_item(),
3084 active_editor: new_active_editor.downgrade(),
3085 });
3086 self.new_entries_for_fs_update
3087 .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
3088 self.selected_entry.invalidate();
3089 self.update_fs_entries(new_active_editor, None, window, cx);
3090 }
3091
3092 fn clear_previous(&mut self, window: &mut Window, cx: &mut App) {
3093 self.fs_entries_update_task = Task::ready(());
3094 self.outline_fetch_tasks.clear();
3095 self.cached_entries_update_task = Task::ready(());
3096 self.reveal_selection_task = Task::ready(Ok(()));
3097 self.filter_editor
3098 .update(cx, |editor, cx| editor.clear(window, cx));
3099 self.collapsed_entries.clear();
3100 self.unfolded_dirs.clear();
3101 self.active_item = None;
3102 self.fs_entries.clear();
3103 self.fs_entries_depth.clear();
3104 self.fs_children_count.clear();
3105 self.excerpts.clear();
3106 self.cached_entries = Vec::new();
3107 self.selected_entry = SelectedEntry::None;
3108 self.pinned = false;
3109 self.mode = ItemsDisplayMode::Outline;
3110 self.pending_default_expansion_depth = None;
3111 }
3112
3113 fn location_for_editor_selection(
3114 &self,
3115 editor: &Entity<Editor>,
3116 window: &mut Window,
3117 cx: &mut Context<Self>,
3118 ) -> Option<PanelEntry> {
3119 let selection = editor.update(cx, |editor, cx| {
3120 editor.selections.newest::<language::Point>(cx).head()
3121 });
3122 let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
3123 let multi_buffer = editor.read(cx).buffer();
3124 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
3125 let (excerpt_id, buffer, _) = editor
3126 .read(cx)
3127 .buffer()
3128 .read(cx)
3129 .excerpt_containing(selection, cx)?;
3130 let buffer_id = buffer.read(cx).remote_id();
3131
3132 if editor.read(cx).is_buffer_folded(buffer_id, cx) {
3133 return self
3134 .fs_entries
3135 .iter()
3136 .find(|fs_entry| match fs_entry {
3137 FsEntry::Directory(..) => false,
3138 FsEntry::File(FsEntryFile {
3139 buffer_id: other_buffer_id,
3140 ..
3141 })
3142 | FsEntry::ExternalFile(FsEntryExternalFile {
3143 buffer_id: other_buffer_id,
3144 ..
3145 }) => buffer_id == *other_buffer_id,
3146 })
3147 .cloned()
3148 .map(PanelEntry::Fs);
3149 }
3150
3151 let selection_display_point = selection.to_display_point(&editor_snapshot);
3152
3153 match &self.mode {
3154 ItemsDisplayMode::Search(search_state) => search_state
3155 .matches
3156 .iter()
3157 .rev()
3158 .min_by_key(|&(match_range, _)| {
3159 let match_display_range =
3160 match_range.clone().to_display_points(&editor_snapshot);
3161 let start_distance = if selection_display_point < match_display_range.start {
3162 match_display_range.start - selection_display_point
3163 } else {
3164 selection_display_point - match_display_range.start
3165 };
3166 let end_distance = if selection_display_point < match_display_range.end {
3167 match_display_range.end - selection_display_point
3168 } else {
3169 selection_display_point - match_display_range.end
3170 };
3171 start_distance + end_distance
3172 })
3173 .and_then(|(closest_range, _)| {
3174 self.cached_entries.iter().find_map(|cached_entry| {
3175 if let PanelEntry::Search(SearchEntry { match_range, .. }) =
3176 &cached_entry.entry
3177 {
3178 if match_range == closest_range {
3179 Some(cached_entry.entry.clone())
3180 } else {
3181 None
3182 }
3183 } else {
3184 None
3185 }
3186 })
3187 }),
3188 ItemsDisplayMode::Outline => self.outline_location(
3189 buffer_id,
3190 excerpt_id,
3191 multi_buffer_snapshot,
3192 editor_snapshot,
3193 selection_display_point,
3194 ),
3195 }
3196 }
3197
3198 fn outline_location(
3199 &self,
3200 buffer_id: BufferId,
3201 excerpt_id: ExcerptId,
3202 multi_buffer_snapshot: editor::MultiBufferSnapshot,
3203 editor_snapshot: editor::EditorSnapshot,
3204 selection_display_point: DisplayPoint,
3205 ) -> Option<PanelEntry> {
3206 let excerpt_outlines = self
3207 .excerpts
3208 .get(&buffer_id)
3209 .and_then(|excerpts| excerpts.get(&excerpt_id))
3210 .into_iter()
3211 .flat_map(|excerpt| excerpt.iter_outlines())
3212 .flat_map(|outline| {
3213 let start = multi_buffer_snapshot
3214 .anchor_in_excerpt(excerpt_id, outline.range.start)?
3215 .to_display_point(&editor_snapshot);
3216 let end = multi_buffer_snapshot
3217 .anchor_in_excerpt(excerpt_id, outline.range.end)?
3218 .to_display_point(&editor_snapshot);
3219 Some((start..end, outline))
3220 })
3221 .collect::<Vec<_>>();
3222
3223 let mut matching_outline_indices = Vec::new();
3224 let mut children = HashMap::default();
3225 let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
3226
3227 for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
3228 if outline_range
3229 .to_inclusive()
3230 .contains(&selection_display_point)
3231 {
3232 matching_outline_indices.push(i);
3233 } else if (outline_range.start.row()..outline_range.end.row())
3234 .to_inclusive()
3235 .contains(&selection_display_point.row())
3236 {
3237 matching_outline_indices.push(i);
3238 }
3239
3240 while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
3241 if parent_outline.depth >= outline.depth
3242 || !parent_range.contains(&outline_range.start)
3243 {
3244 parents_stack.pop();
3245 } else {
3246 break;
3247 }
3248 }
3249 if let Some((_, _, parent_index)) = parents_stack.last_mut() {
3250 children
3251 .entry(*parent_index)
3252 .or_insert_with(Vec::new)
3253 .push(i);
3254 }
3255 parents_stack.push((outline_range, outline, i));
3256 }
3257
3258 let outline_item = matching_outline_indices
3259 .into_iter()
3260 .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
3261 .filter(|(i, _)| {
3262 children
3263 .get(i)
3264 .map(|children| {
3265 children.iter().all(|child_index| {
3266 excerpt_outlines
3267 .get(*child_index)
3268 .map(|(child_range, _)| child_range.start > selection_display_point)
3269 .unwrap_or(false)
3270 })
3271 })
3272 .unwrap_or(true)
3273 })
3274 .min_by_key(|(_, (outline_range, outline))| {
3275 let distance_from_start = if outline_range.start > selection_display_point {
3276 outline_range.start - selection_display_point
3277 } else {
3278 selection_display_point - outline_range.start
3279 };
3280 let distance_from_end = if outline_range.end > selection_display_point {
3281 outline_range.end - selection_display_point
3282 } else {
3283 selection_display_point - outline_range.end
3284 };
3285
3286 (
3287 cmp::Reverse(outline.depth),
3288 distance_from_start + distance_from_end,
3289 )
3290 })
3291 .map(|(_, (_, outline))| *outline)
3292 .cloned();
3293
3294 let closest_container = match outline_item {
3295 Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
3296 buffer_id,
3297 excerpt_id,
3298 outline,
3299 })),
3300 None => {
3301 self.cached_entries.iter().rev().find_map(|cached_entry| {
3302 match &cached_entry.entry {
3303 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
3304 if excerpt.buffer_id == buffer_id && excerpt.id == excerpt_id {
3305 Some(cached_entry.entry.clone())
3306 } else {
3307 None
3308 }
3309 }
3310 PanelEntry::Fs(
3311 FsEntry::ExternalFile(FsEntryExternalFile {
3312 buffer_id: file_buffer_id,
3313 excerpts: file_excerpts,
3314 })
3315 | FsEntry::File(FsEntryFile {
3316 buffer_id: file_buffer_id,
3317 excerpts: file_excerpts,
3318 ..
3319 }),
3320 ) => {
3321 if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
3322 Some(cached_entry.entry.clone())
3323 } else {
3324 None
3325 }
3326 }
3327 _ => None,
3328 }
3329 })?
3330 }
3331 };
3332 Some(closest_container)
3333 }
3334
3335 fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3336 let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
3337 if excerpt_fetch_ranges.is_empty() {
3338 return;
3339 }
3340
3341 let syntax_theme = cx.theme().syntax().clone();
3342 let first_update = Arc::new(AtomicBool::new(true));
3343 for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
3344 for (excerpt_id, excerpt_range) in excerpt_ranges {
3345 let syntax_theme = syntax_theme.clone();
3346 let buffer_snapshot = buffer_snapshot.clone();
3347 let first_update = first_update.clone();
3348 self.outline_fetch_tasks.insert(
3349 (buffer_id, excerpt_id),
3350 cx.spawn_in(window, async move |outline_panel, cx| {
3351 let buffer_language = buffer_snapshot.language().cloned();
3352 let fetched_outlines = cx
3353 .background_spawn(async move {
3354 let mut outlines = buffer_snapshot
3355 .outline_items_containing(
3356 excerpt_range.context,
3357 false,
3358 Some(&syntax_theme),
3359 )
3360 .unwrap_or_default();
3361 outlines.retain(|outline| {
3362 buffer_language.is_none()
3363 || buffer_language.as_ref()
3364 == buffer_snapshot.language_at(outline.range.start)
3365 });
3366
3367 let outlines_with_children = outlines
3368 .windows(2)
3369 .filter_map(|window| {
3370 let current = &window[0];
3371 let next = &window[1];
3372 if next.depth > current.depth {
3373 Some((current.range.clone(), current.depth))
3374 } else {
3375 None
3376 }
3377 })
3378 .collect::<HashSet<_>>();
3379
3380 (outlines, outlines_with_children)
3381 })
3382 .await;
3383
3384 let (fetched_outlines, outlines_with_children) = fetched_outlines;
3385
3386 outline_panel
3387 .update_in(cx, |outline_panel, window, cx| {
3388 let pending_default_depth =
3389 outline_panel.pending_default_expansion_depth.take();
3390
3391 let debounce =
3392 if first_update.fetch_and(false, atomic::Ordering::AcqRel) {
3393 None
3394 } else {
3395 Some(UPDATE_DEBOUNCE)
3396 };
3397
3398 if let Some(excerpt) = outline_panel
3399 .excerpts
3400 .entry(buffer_id)
3401 .or_default()
3402 .get_mut(&excerpt_id)
3403 {
3404 excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
3405
3406 if let Some(default_depth) = pending_default_depth
3407 && let ExcerptOutlines::Outlines(outlines) =
3408 &excerpt.outlines
3409 {
3410 outlines
3411 .iter()
3412 .filter(|outline| {
3413 (default_depth == 0
3414 || outline.depth >= default_depth)
3415 && outlines_with_children.contains(&(
3416 outline.range.clone(),
3417 outline.depth,
3418 ))
3419 })
3420 .for_each(|outline| {
3421 outline_panel.collapsed_entries.insert(
3422 CollapsedEntry::Outline(
3423 buffer_id,
3424 excerpt_id,
3425 outline.range.clone(),
3426 ),
3427 );
3428 });
3429 }
3430
3431 // Even if no outlines to check, we still need to update cached entries
3432 // to show the outline entries that were just fetched
3433 outline_panel.update_cached_entries(debounce, window, cx);
3434 }
3435 })
3436 .ok();
3437 }),
3438 );
3439 }
3440 }
3441 }
3442
3443 fn is_singleton_active(&self, cx: &App) -> bool {
3444 self.active_editor().map_or(false, |active_editor| {
3445 active_editor.read(cx).buffer().read(cx).is_singleton()
3446 })
3447 }
3448
3449 fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
3450 self.outline_fetch_tasks.clear();
3451 let mut ids = ids.iter().collect::<HashSet<_>>();
3452 for excerpts in self.excerpts.values_mut() {
3453 ids.retain(|id| {
3454 if let Some(excerpt) = excerpts.get_mut(id) {
3455 excerpt.invalidate_outlines();
3456 false
3457 } else {
3458 true
3459 }
3460 });
3461 if ids.is_empty() {
3462 break;
3463 }
3464 }
3465 }
3466
3467 fn excerpt_fetch_ranges(
3468 &self,
3469 cx: &App,
3470 ) -> HashMap<
3471 BufferId,
3472 (
3473 BufferSnapshot,
3474 HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
3475 ),
3476 > {
3477 self.fs_entries
3478 .iter()
3479 .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
3480 match fs_entry {
3481 FsEntry::File(FsEntryFile {
3482 buffer_id,
3483 excerpts: file_excerpts,
3484 ..
3485 })
3486 | FsEntry::ExternalFile(FsEntryExternalFile {
3487 buffer_id,
3488 excerpts: file_excerpts,
3489 }) => {
3490 let excerpts = self.excerpts.get(buffer_id);
3491 for &file_excerpt in file_excerpts {
3492 if let Some(excerpt) = excerpts
3493 .and_then(|excerpts| excerpts.get(&file_excerpt))
3494 .filter(|excerpt| excerpt.should_fetch_outlines())
3495 {
3496 match excerpts_to_fetch.entry(*buffer_id) {
3497 hash_map::Entry::Occupied(mut o) => {
3498 o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
3499 }
3500 hash_map::Entry::Vacant(v) => {
3501 if let Some(buffer_snapshot) =
3502 self.buffer_snapshot_for_id(*buffer_id, cx)
3503 {
3504 v.insert((buffer_snapshot, HashMap::default()))
3505 .1
3506 .insert(file_excerpt, excerpt.range.clone());
3507 }
3508 }
3509 }
3510 }
3511 }
3512 }
3513 FsEntry::Directory(..) => {}
3514 }
3515 excerpts_to_fetch
3516 })
3517 }
3518
3519 fn buffer_snapshot_for_id(&self, buffer_id: BufferId, cx: &App) -> Option<BufferSnapshot> {
3520 let editor = self.active_editor()?;
3521 Some(
3522 editor
3523 .read(cx)
3524 .buffer()
3525 .read(cx)
3526 .buffer(buffer_id)?
3527 .read(cx)
3528 .snapshot(),
3529 )
3530 }
3531
3532 fn abs_path(&self, entry: &PanelEntry, cx: &App) -> Option<PathBuf> {
3533 match entry {
3534 PanelEntry::Fs(
3535 FsEntry::File(FsEntryFile { buffer_id, .. })
3536 | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }),
3537 ) => self
3538 .buffer_snapshot_for_id(*buffer_id, cx)
3539 .and_then(|buffer_snapshot| {
3540 let file = File::from_dyn(buffer_snapshot.file())?;
3541 file.worktree.read(cx).absolutize(&file.path).ok()
3542 }),
3543 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3544 worktree_id, entry, ..
3545 })) => self
3546 .project
3547 .read(cx)
3548 .worktree_for_id(*worktree_id, cx)?
3549 .read(cx)
3550 .absolutize(&entry.path)
3551 .ok(),
3552 PanelEntry::FoldedDirs(FoldedDirsEntry {
3553 worktree_id,
3554 entries: dirs,
3555 ..
3556 }) => dirs.last().and_then(|entry| {
3557 self.project
3558 .read(cx)
3559 .worktree_for_id(*worktree_id, cx)
3560 .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
3561 }),
3562 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
3563 }
3564 }
3565
3566 fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<Path>> {
3567 match entry {
3568 FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3569 let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
3570 Some(buffer_snapshot.file()?.path().clone())
3571 }
3572 FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()),
3573 FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()),
3574 }
3575 }
3576
3577 fn update_cached_entries(
3578 &mut self,
3579 debounce: Option<Duration>,
3580 window: &mut Window,
3581 cx: &mut Context<OutlinePanel>,
3582 ) {
3583 if !self.active {
3584 return;
3585 }
3586
3587 let is_singleton = self.is_singleton_active(cx);
3588 let query = self.query(cx);
3589 self.updating_cached_entries = true;
3590 self.cached_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
3591 if let Some(debounce) = debounce {
3592 cx.background_executor().timer(debounce).await;
3593 }
3594 let Some(new_cached_entries) = outline_panel
3595 .update_in(cx, |outline_panel, window, cx| {
3596 outline_panel.generate_cached_entries(is_singleton, query, window, cx)
3597 })
3598 .ok()
3599 else {
3600 return;
3601 };
3602 let (new_cached_entries, max_width_item_index) = new_cached_entries.await;
3603 outline_panel
3604 .update_in(cx, |outline_panel, window, cx| {
3605 outline_panel.cached_entries = new_cached_entries;
3606 outline_panel.max_width_item_index = max_width_item_index;
3607 if (outline_panel.selected_entry.is_invalidated()
3608 || matches!(outline_panel.selected_entry, SelectedEntry::None))
3609 && let Some(new_selected_entry) =
3610 outline_panel.active_editor().and_then(|active_editor| {
3611 outline_panel.location_for_editor_selection(
3612 &active_editor,
3613 window,
3614 cx,
3615 )
3616 })
3617 {
3618 outline_panel.select_entry(new_selected_entry, false, window, cx);
3619 }
3620
3621 outline_panel.autoscroll(cx);
3622 outline_panel.updating_cached_entries = false;
3623 cx.notify();
3624 })
3625 .ok();
3626 });
3627 }
3628
3629 fn generate_cached_entries(
3630 &self,
3631 is_singleton: bool,
3632 query: Option<String>,
3633 window: &mut Window,
3634 cx: &mut Context<Self>,
3635 ) -> Task<(Vec<CachedEntry>, Option<usize>)> {
3636 let project = self.project.clone();
3637 let Some(active_editor) = self.active_editor() else {
3638 return Task::ready((Vec::new(), None));
3639 };
3640 cx.spawn_in(window, async move |outline_panel, cx| {
3641 let mut generation_state = GenerationState::default();
3642
3643 let Ok(()) = outline_panel.update(cx, |outline_panel, cx| {
3644 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
3645 let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>;
3646 let track_matches = query.is_some();
3647
3648 #[derive(Debug)]
3649 struct ParentStats {
3650 path: Arc<Path>,
3651 folded: bool,
3652 expanded: bool,
3653 depth: usize,
3654 }
3655 let mut parent_dirs = Vec::<ParentStats>::new();
3656 for entry in outline_panel.fs_entries.clone() {
3657 let is_expanded = outline_panel.is_expanded(&entry);
3658 let (depth, should_add) = match &entry {
3659 FsEntry::Directory(directory_entry) => {
3660 let mut should_add = true;
3661 let is_root = project
3662 .read(cx)
3663 .worktree_for_id(directory_entry.worktree_id, cx)
3664 .map_or(false, |worktree| {
3665 worktree.read(cx).root_entry() == Some(&directory_entry.entry)
3666 });
3667 let folded = auto_fold_dirs
3668 && !is_root
3669 && outline_panel
3670 .unfolded_dirs
3671 .get(&directory_entry.worktree_id)
3672 .map_or(true, |unfolded_dirs| {
3673 !unfolded_dirs.contains(&directory_entry.entry.id)
3674 });
3675 let fs_depth = outline_panel
3676 .fs_entries_depth
3677 .get(&(directory_entry.worktree_id, directory_entry.entry.id))
3678 .copied()
3679 .unwrap_or(0);
3680 while let Some(parent) = parent_dirs.last() {
3681 if !is_root && directory_entry.entry.path.starts_with(&parent.path)
3682 {
3683 break;
3684 }
3685 parent_dirs.pop();
3686 }
3687 let auto_fold = match parent_dirs.last() {
3688 Some(parent) => {
3689 parent.folded
3690 && Some(parent.path.as_ref())
3691 == directory_entry.entry.path.parent()
3692 && outline_panel
3693 .fs_children_count
3694 .get(&directory_entry.worktree_id)
3695 .and_then(|entries| {
3696 entries.get(&directory_entry.entry.path)
3697 })
3698 .copied()
3699 .unwrap_or_default()
3700 .may_be_fold_part()
3701 }
3702 None => false,
3703 };
3704 let folded = folded || auto_fold;
3705 let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
3706 Some(parent) => {
3707 let parent_folded = parent.folded;
3708 let parent_expanded = parent.expanded;
3709 let new_depth = if parent_folded {
3710 parent.depth
3711 } else {
3712 parent.depth + 1
3713 };
3714 parent_dirs.push(ParentStats {
3715 path: directory_entry.entry.path.clone(),
3716 folded,
3717 expanded: parent_expanded && is_expanded,
3718 depth: new_depth,
3719 });
3720 (new_depth, parent_expanded, parent_folded)
3721 }
3722 None => {
3723 parent_dirs.push(ParentStats {
3724 path: directory_entry.entry.path.clone(),
3725 folded,
3726 expanded: is_expanded,
3727 depth: fs_depth,
3728 });
3729 (fs_depth, true, false)
3730 }
3731 };
3732
3733 if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take()
3734 {
3735 if folded
3736 && directory_entry.worktree_id == folded_dirs.worktree_id
3737 && directory_entry.entry.path.parent()
3738 == folded_dirs
3739 .entries
3740 .last()
3741 .map(|entry| entry.path.as_ref())
3742 {
3743 folded_dirs.entries.push(directory_entry.entry.clone());
3744 folded_dirs_entry = Some((folded_depth, folded_dirs))
3745 } else {
3746 if !is_singleton {
3747 let start_of_collapsed_dir_sequence = !parent_expanded
3748 && parent_dirs
3749 .iter()
3750 .rev()
3751 .nth(folded_dirs.entries.len() + 1)
3752 .map_or(true, |parent| parent.expanded);
3753 if start_of_collapsed_dir_sequence
3754 || parent_expanded
3755 || query.is_some()
3756 {
3757 if parent_folded {
3758 folded_dirs
3759 .entries
3760 .push(directory_entry.entry.clone());
3761 should_add = false;
3762 }
3763 let new_folded_dirs =
3764 PanelEntry::FoldedDirs(folded_dirs.clone());
3765 outline_panel.push_entry(
3766 &mut generation_state,
3767 track_matches,
3768 new_folded_dirs,
3769 folded_depth,
3770 cx,
3771 );
3772 }
3773 }
3774
3775 folded_dirs_entry = if parent_folded {
3776 None
3777 } else {
3778 Some((
3779 depth,
3780 FoldedDirsEntry {
3781 worktree_id: directory_entry.worktree_id,
3782 entries: vec![directory_entry.entry.clone()],
3783 },
3784 ))
3785 };
3786 }
3787 } else if folded {
3788 folded_dirs_entry = Some((
3789 depth,
3790 FoldedDirsEntry {
3791 worktree_id: directory_entry.worktree_id,
3792 entries: vec![directory_entry.entry.clone()],
3793 },
3794 ));
3795 }
3796
3797 let should_add =
3798 should_add && parent_expanded && folded_dirs_entry.is_none();
3799 (depth, should_add)
3800 }
3801 FsEntry::ExternalFile(..) => {
3802 if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() {
3803 let parent_expanded = parent_dirs
3804 .iter()
3805 .rev()
3806 .find(|parent| {
3807 folded_dir
3808 .entries
3809 .iter()
3810 .all(|entry| entry.path != parent.path)
3811 })
3812 .map_or(true, |parent| parent.expanded);
3813 if !is_singleton && (parent_expanded || query.is_some()) {
3814 outline_panel.push_entry(
3815 &mut generation_state,
3816 track_matches,
3817 PanelEntry::FoldedDirs(folded_dir),
3818 folded_depth,
3819 cx,
3820 );
3821 }
3822 }
3823 parent_dirs.clear();
3824 (0, true)
3825 }
3826 FsEntry::File(file) => {
3827 if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3828 let parent_expanded = parent_dirs
3829 .iter()
3830 .rev()
3831 .find(|parent| {
3832 folded_dirs
3833 .entries
3834 .iter()
3835 .all(|entry| entry.path != parent.path)
3836 })
3837 .map_or(true, |parent| parent.expanded);
3838 if !is_singleton && (parent_expanded || query.is_some()) {
3839 outline_panel.push_entry(
3840 &mut generation_state,
3841 track_matches,
3842 PanelEntry::FoldedDirs(folded_dirs),
3843 folded_depth,
3844 cx,
3845 );
3846 }
3847 }
3848
3849 let fs_depth = outline_panel
3850 .fs_entries_depth
3851 .get(&(file.worktree_id, file.entry.id))
3852 .copied()
3853 .unwrap_or(0);
3854 while let Some(parent) = parent_dirs.last() {
3855 if file.entry.path.starts_with(&parent.path) {
3856 break;
3857 }
3858 parent_dirs.pop();
3859 }
3860 match parent_dirs.last() {
3861 Some(parent) => {
3862 let new_depth = parent.depth + 1;
3863 (new_depth, parent.expanded)
3864 }
3865 None => (fs_depth, true),
3866 }
3867 }
3868 };
3869
3870 if !is_singleton
3871 && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3872 {
3873 outline_panel.push_entry(
3874 &mut generation_state,
3875 track_matches,
3876 PanelEntry::Fs(entry.clone()),
3877 depth,
3878 cx,
3879 );
3880 }
3881
3882 match outline_panel.mode {
3883 ItemsDisplayMode::Search(_) => {
3884 if is_singleton || query.is_some() || (should_add && is_expanded) {
3885 outline_panel.add_search_entries(
3886 &mut generation_state,
3887 &active_editor,
3888 entry.clone(),
3889 depth,
3890 query.clone(),
3891 is_singleton,
3892 cx,
3893 );
3894 }
3895 }
3896 ItemsDisplayMode::Outline => {
3897 let excerpts_to_consider =
3898 if is_singleton || query.is_some() || (should_add && is_expanded) {
3899 match &entry {
3900 FsEntry::File(FsEntryFile {
3901 buffer_id,
3902 excerpts,
3903 ..
3904 })
3905 | FsEntry::ExternalFile(FsEntryExternalFile {
3906 buffer_id,
3907 excerpts,
3908 ..
3909 }) => Some((*buffer_id, excerpts)),
3910 _ => None,
3911 }
3912 } else {
3913 None
3914 };
3915 if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider
3916 && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) {
3917 outline_panel.add_excerpt_entries(
3918 &mut generation_state,
3919 buffer_id,
3920 entry_excerpts,
3921 depth,
3922 track_matches,
3923 is_singleton,
3924 query.as_deref(),
3925 cx,
3926 );
3927 }
3928 }
3929 }
3930
3931 if is_singleton
3932 && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
3933 && !generation_state.entries.iter().any(|item| {
3934 matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
3935 })
3936 {
3937 outline_panel.push_entry(
3938 &mut generation_state,
3939 track_matches,
3940 PanelEntry::Fs(entry.clone()),
3941 0,
3942 cx,
3943 );
3944 }
3945 }
3946
3947 if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3948 let parent_expanded = parent_dirs
3949 .iter()
3950 .rev()
3951 .find(|parent| {
3952 folded_dirs
3953 .entries
3954 .iter()
3955 .all(|entry| entry.path != parent.path)
3956 })
3957 .map_or(true, |parent| parent.expanded);
3958 if parent_expanded || query.is_some() {
3959 outline_panel.push_entry(
3960 &mut generation_state,
3961 track_matches,
3962 PanelEntry::FoldedDirs(folded_dirs),
3963 folded_depth,
3964 cx,
3965 );
3966 }
3967 }
3968 }) else {
3969 return (Vec::new(), None);
3970 };
3971
3972 let Some(query) = query else {
3973 return (
3974 generation_state.entries,
3975 generation_state
3976 .max_width_estimate_and_index
3977 .map(|(_, index)| index),
3978 );
3979 };
3980
3981 let mut matched_ids = match_strings(
3982 &generation_state.match_candidates,
3983 &query,
3984 true,
3985 true,
3986 usize::MAX,
3987 &AtomicBool::default(),
3988 cx.background_executor().clone(),
3989 )
3990 .await
3991 .into_iter()
3992 .map(|string_match| (string_match.candidate_id, string_match))
3993 .collect::<HashMap<_, _>>();
3994
3995 let mut id = 0;
3996 generation_state.entries.retain_mut(|cached_entry| {
3997 let retain = match matched_ids.remove(&id) {
3998 Some(string_match) => {
3999 cached_entry.string_match = Some(string_match);
4000 true
4001 }
4002 None => false,
4003 };
4004 id += 1;
4005 retain
4006 });
4007
4008 (
4009 generation_state.entries,
4010 generation_state
4011 .max_width_estimate_and_index
4012 .map(|(_, index)| index),
4013 )
4014 })
4015 }
4016
4017 fn push_entry(
4018 &self,
4019 state: &mut GenerationState,
4020 track_matches: bool,
4021 entry: PanelEntry,
4022 depth: usize,
4023 cx: &mut App,
4024 ) {
4025 let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry {
4026 match folded_dirs_entry.entries.len() {
4027 0 => {
4028 debug_panic!("Empty folded dirs receiver");
4029 return;
4030 }
4031 1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
4032 worktree_id: folded_dirs_entry.worktree_id,
4033 entry: folded_dirs_entry.entries[0].clone(),
4034 })),
4035 _ => entry,
4036 }
4037 } else {
4038 entry
4039 };
4040
4041 if track_matches {
4042 let id = state.entries.len();
4043 match &entry {
4044 PanelEntry::Fs(fs_entry) => {
4045 if let Some(file_name) =
4046 self.relative_path(fs_entry, cx).as_deref().map(file_name)
4047 {
4048 state
4049 .match_candidates
4050 .push(StringMatchCandidate::new(id, &file_name));
4051 }
4052 }
4053 PanelEntry::FoldedDirs(folded_dir_entry) => {
4054 let dir_names = self.dir_names_string(
4055 &folded_dir_entry.entries,
4056 folded_dir_entry.worktree_id,
4057 cx,
4058 );
4059 {
4060 state
4061 .match_candidates
4062 .push(StringMatchCandidate::new(id, &dir_names));
4063 }
4064 }
4065 PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state
4066 .match_candidates
4067 .push(StringMatchCandidate::new(id, &outline_entry.outline.text)),
4068 PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {}
4069 PanelEntry::Search(new_search_entry) => {
4070 if let Some(search_data) = new_search_entry.render_data.get() {
4071 state
4072 .match_candidates
4073 .push(StringMatchCandidate::new(id, &search_data.context_text));
4074 }
4075 }
4076 }
4077 }
4078
4079 let width_estimate = self.width_estimate(depth, &entry, cx);
4080 if Some(width_estimate)
4081 > state
4082 .max_width_estimate_and_index
4083 .map(|(estimate, _)| estimate)
4084 {
4085 state.max_width_estimate_and_index = Some((width_estimate, state.entries.len()));
4086 }
4087 state.entries.push(CachedEntry {
4088 depth,
4089 entry,
4090 string_match: None,
4091 });
4092 }
4093
4094 fn dir_names_string(&self, entries: &[GitEntry], worktree_id: WorktreeId, cx: &App) -> String {
4095 let dir_names_segment = entries
4096 .iter()
4097 .map(|entry| self.entry_name(&worktree_id, entry, cx))
4098 .collect::<PathBuf>();
4099 dir_names_segment.to_string_lossy().to_string()
4100 }
4101
4102 fn query(&self, cx: &App) -> Option<String> {
4103 let query = self.filter_editor.read(cx).text(cx);
4104 if query.trim().is_empty() {
4105 None
4106 } else {
4107 Some(query)
4108 }
4109 }
4110
4111 fn is_expanded(&self, entry: &FsEntry) -> bool {
4112 let entry_to_check = match entry {
4113 FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
4114 CollapsedEntry::ExternalFile(*buffer_id)
4115 }
4116 FsEntry::File(FsEntryFile {
4117 worktree_id,
4118 buffer_id,
4119 ..
4120 }) => CollapsedEntry::File(*worktree_id, *buffer_id),
4121 FsEntry::Directory(FsEntryDirectory {
4122 worktree_id, entry, ..
4123 }) => CollapsedEntry::Dir(*worktree_id, entry.id),
4124 };
4125 !self.collapsed_entries.contains(&entry_to_check)
4126 }
4127
4128 fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) -> bool {
4129 if !self.active {
4130 return false;
4131 }
4132
4133 let mut update_cached_items = false;
4134 update_cached_items |= self.update_search_matches(window, cx);
4135 self.fetch_outdated_outlines(window, cx);
4136 if update_cached_items {
4137 self.selected_entry.invalidate();
4138 }
4139 update_cached_items
4140 }
4141
4142 fn update_search_matches(
4143 &mut self,
4144 window: &mut Window,
4145 cx: &mut Context<OutlinePanel>,
4146 ) -> bool {
4147 if !self.active {
4148 return false;
4149 }
4150
4151 let project_search = self
4152 .active_item()
4153 .and_then(|item| item.downcast::<ProjectSearchView>());
4154 let project_search_matches = project_search
4155 .as_ref()
4156 .map(|project_search| project_search.read(cx).get_matches(cx))
4157 .unwrap_or_default();
4158
4159 let buffer_search = self
4160 .active_item()
4161 .as_deref()
4162 .and_then(|active_item| {
4163 self.workspace
4164 .upgrade()
4165 .and_then(|workspace| workspace.read(cx).pane_for(active_item))
4166 })
4167 .and_then(|pane| {
4168 pane.read(cx)
4169 .toolbar()
4170 .read(cx)
4171 .item_of_type::<BufferSearchBar>()
4172 });
4173 let buffer_search_matches = self
4174 .active_editor()
4175 .map(|active_editor| {
4176 active_editor.update(cx, |editor, cx| editor.get_matches(window, cx))
4177 })
4178 .unwrap_or_default();
4179
4180 let mut update_cached_entries = false;
4181 if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
4182 if matches!(self.mode, ItemsDisplayMode::Search(_)) {
4183 self.mode = ItemsDisplayMode::Outline;
4184 update_cached_entries = true;
4185 }
4186 } else {
4187 let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
4188 (
4189 SearchKind::Project,
4190 project_search_matches,
4191 project_search
4192 .map(|project_search| project_search.read(cx).search_query_text(cx))
4193 .unwrap_or_default(),
4194 )
4195 } else {
4196 (
4197 SearchKind::Buffer,
4198 buffer_search_matches,
4199 buffer_search
4200 .map(|buffer_search| buffer_search.read(cx).query(cx))
4201 .unwrap_or_default(),
4202 )
4203 };
4204
4205 let mut previous_matches = HashMap::default();
4206 update_cached_entries = match &mut self.mode {
4207 ItemsDisplayMode::Search(current_search_state) => {
4208 let update = current_search_state.query != new_search_query
4209 || current_search_state.kind != kind
4210 || current_search_state.matches.is_empty()
4211 || current_search_state.matches.iter().enumerate().any(
4212 |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
4213 );
4214 if current_search_state.kind == kind {
4215 previous_matches.extend(current_search_state.matches.drain(..));
4216 }
4217 update
4218 }
4219 ItemsDisplayMode::Outline => true,
4220 };
4221 self.mode = ItemsDisplayMode::Search(SearchState::new(
4222 kind,
4223 new_search_query,
4224 previous_matches,
4225 new_search_matches,
4226 cx.theme().syntax().clone(),
4227 window,
4228 cx,
4229 ));
4230 }
4231 update_cached_entries
4232 }
4233
4234 fn add_excerpt_entries(
4235 &mut self,
4236 state: &mut GenerationState,
4237 buffer_id: BufferId,
4238 entries_to_add: &[ExcerptId],
4239 parent_depth: usize,
4240 track_matches: bool,
4241 is_singleton: bool,
4242 query: Option<&str>,
4243 cx: &mut Context<Self>,
4244 ) {
4245 if let Some(excerpts) = self.excerpts.get(&buffer_id) {
4246 let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx);
4247
4248 for &excerpt_id in entries_to_add {
4249 let Some(excerpt) = excerpts.get(&excerpt_id) else {
4250 continue;
4251 };
4252 let excerpt_depth = parent_depth + 1;
4253 self.push_entry(
4254 state,
4255 track_matches,
4256 PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
4257 buffer_id,
4258 id: excerpt_id,
4259 range: excerpt.range.clone(),
4260 })),
4261 excerpt_depth,
4262 cx,
4263 );
4264
4265 let mut outline_base_depth = excerpt_depth + 1;
4266 if is_singleton {
4267 outline_base_depth = 0;
4268 state.clear();
4269 } else if query.is_none()
4270 && self
4271 .collapsed_entries
4272 .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
4273 {
4274 continue;
4275 }
4276
4277 let mut last_depth_at_level: Vec<Option<Range<Anchor>>> = vec![None; 10];
4278
4279 let all_outlines: Vec<_> = excerpt.iter_outlines().collect();
4280
4281 let mut outline_has_children = HashMap::default();
4282 let mut visible_outlines = Vec::new();
4283 let mut collapsed_state: Option<(usize, Range<Anchor>)> = None;
4284
4285 for (i, &outline) in all_outlines.iter().enumerate() {
4286 let has_children = all_outlines
4287 .get(i + 1)
4288 .map(|next| next.depth > outline.depth)
4289 .unwrap_or(false);
4290
4291 outline_has_children
4292 .insert((outline.range.clone(), outline.depth), has_children);
4293
4294 let mut should_include = true;
4295
4296 if let Some((collapsed_depth, collapsed_range)) = &collapsed_state {
4297 if outline.depth <= *collapsed_depth {
4298 collapsed_state = None;
4299 } else if let Some(buffer_snapshot) = buffer_snapshot.as_ref() {
4300 let outline_start = outline.range.start;
4301 if outline_start
4302 .cmp(&collapsed_range.start, buffer_snapshot)
4303 .is_ge()
4304 && outline_start
4305 .cmp(&collapsed_range.end, buffer_snapshot)
4306 .is_lt()
4307 {
4308 should_include = false; // Skip - inside collapsed range
4309 } else {
4310 collapsed_state = None;
4311 }
4312 }
4313 }
4314
4315 // Check if this outline itself is collapsed
4316 if should_include
4317 && self.collapsed_entries.contains(&CollapsedEntry::Outline(
4318 buffer_id,
4319 excerpt_id,
4320 outline.range.clone(),
4321 ))
4322 {
4323 collapsed_state = Some((outline.depth, outline.range.clone()));
4324 }
4325
4326 if should_include {
4327 visible_outlines.push(outline);
4328 }
4329 }
4330
4331 self.outline_children_cache
4332 .entry(buffer_id)
4333 .or_default()
4334 .extend(outline_has_children);
4335
4336 for outline in visible_outlines {
4337 let outline_entry = OutlineEntryOutline {
4338 buffer_id,
4339 excerpt_id,
4340 outline: outline.clone(),
4341 };
4342
4343 if outline.depth < last_depth_at_level.len() {
4344 last_depth_at_level[outline.depth] = Some(outline.range.clone());
4345 // Clear deeper levels when we go back to a shallower depth
4346 for d in (outline.depth + 1)..last_depth_at_level.len() {
4347 last_depth_at_level[d] = None;
4348 }
4349 }
4350
4351 self.push_entry(
4352 state,
4353 track_matches,
4354 PanelEntry::Outline(OutlineEntry::Outline(outline_entry)),
4355 outline_base_depth + outline.depth,
4356 cx,
4357 );
4358 }
4359 }
4360 }
4361 }
4362
4363 fn add_search_entries(
4364 &mut self,
4365 state: &mut GenerationState,
4366 active_editor: &Entity<Editor>,
4367 parent_entry: FsEntry,
4368 parent_depth: usize,
4369 filter_query: Option<String>,
4370 is_singleton: bool,
4371 cx: &mut Context<Self>,
4372 ) {
4373 let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
4374 return;
4375 };
4376
4377 let kind = search_state.kind;
4378 let related_excerpts = match &parent_entry {
4379 FsEntry::Directory(_) => return,
4380 FsEntry::ExternalFile(external) => &external.excerpts,
4381 FsEntry::File(file) => &file.excerpts,
4382 }
4383 .iter()
4384 .copied()
4385 .collect::<HashSet<_>>();
4386
4387 let depth = if is_singleton { 0 } else { parent_depth + 1 };
4388 let new_search_matches = search_state
4389 .matches
4390 .iter()
4391 .filter(|(match_range, _)| {
4392 related_excerpts.contains(&match_range.start.excerpt_id)
4393 || related_excerpts.contains(&match_range.end.excerpt_id)
4394 })
4395 .filter(|(match_range, _)| {
4396 let editor = active_editor.read(cx);
4397 if let Some(buffer_id) = match_range.start.buffer_id
4398 && editor.is_buffer_folded(buffer_id, cx) {
4399 return false;
4400 }
4401 if let Some(buffer_id) = match_range.start.buffer_id
4402 && editor.is_buffer_folded(buffer_id, cx) {
4403 return false;
4404 }
4405 true
4406 });
4407
4408 let new_search_entries = new_search_matches
4409 .map(|(match_range, search_data)| SearchEntry {
4410 match_range: match_range.clone(),
4411 kind,
4412 render_data: Arc::clone(search_data),
4413 })
4414 .collect::<Vec<_>>();
4415 for new_search_entry in new_search_entries {
4416 self.push_entry(
4417 state,
4418 filter_query.is_some(),
4419 PanelEntry::Search(new_search_entry),
4420 depth,
4421 cx,
4422 );
4423 }
4424 }
4425
4426 fn active_editor(&self) -> Option<Entity<Editor>> {
4427 self.active_item.as_ref()?.active_editor.upgrade()
4428 }
4429
4430 fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
4431 self.active_item.as_ref()?.item_handle.upgrade()
4432 }
4433
4434 fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
4435 self.active_item().map_or(true, |active_item| {
4436 !self.pinned && active_item.item_id() != new_active_item.item_id()
4437 })
4438 }
4439
4440 pub fn toggle_active_editor_pin(
4441 &mut self,
4442 _: &ToggleActiveEditorPin,
4443 window: &mut Window,
4444 cx: &mut Context<Self>,
4445 ) {
4446 self.pinned = !self.pinned;
4447 if !self.pinned
4448 && let Some((active_item, active_editor)) = self
4449 .workspace
4450 .upgrade()
4451 .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
4452 && self.should_replace_active_item(active_item.as_ref()) {
4453 self.replace_active_editor(active_item, active_editor, window, cx);
4454 }
4455
4456 cx.notify();
4457 }
4458
4459 fn selected_entry(&self) -> Option<&PanelEntry> {
4460 match &self.selected_entry {
4461 SelectedEntry::Invalidated(entry) => entry.as_ref(),
4462 SelectedEntry::Valid(entry, _) => Some(entry),
4463 SelectedEntry::None => None,
4464 }
4465 }
4466
4467 fn select_entry(
4468 &mut self,
4469 entry: PanelEntry,
4470 focus: bool,
4471 window: &mut Window,
4472 cx: &mut Context<Self>,
4473 ) {
4474 if focus {
4475 self.focus_handle.focus(window);
4476 }
4477 let ix = self
4478 .cached_entries
4479 .iter()
4480 .enumerate()
4481 .find(|(_, cached_entry)| &cached_entry.entry == &entry)
4482 .map(|(i, _)| i)
4483 .unwrap_or_default();
4484
4485 self.selected_entry = SelectedEntry::Valid(entry, ix);
4486
4487 self.autoscroll(cx);
4488 cx.notify();
4489 }
4490
4491 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4492 if !Self::should_show_scrollbar(cx)
4493 || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4494 {
4495 return None;
4496 }
4497 Some(
4498 div()
4499 .occlude()
4500 .id("project-panel-vertical-scroll")
4501 .on_mouse_move(cx.listener(|_, _, _, cx| {
4502 cx.notify();
4503 cx.stop_propagation()
4504 }))
4505 .on_hover(|_, _, cx| {
4506 cx.stop_propagation();
4507 })
4508 .on_any_mouse_down(|_, _, cx| {
4509 cx.stop_propagation();
4510 })
4511 .on_mouse_up(
4512 MouseButton::Left,
4513 cx.listener(|outline_panel, _, window, cx| {
4514 if !outline_panel.vertical_scrollbar_state.is_dragging()
4515 && !outline_panel.focus_handle.contains_focused(window, cx)
4516 {
4517 outline_panel.hide_scrollbar(window, cx);
4518 cx.notify();
4519 }
4520
4521 cx.stop_propagation();
4522 }),
4523 )
4524 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4525 cx.notify();
4526 }))
4527 .h_full()
4528 .absolute()
4529 .right_1()
4530 .top_1()
4531 .bottom_0()
4532 .w(px(12.))
4533 .cursor_default()
4534 .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone())),
4535 )
4536 }
4537
4538 fn render_horizontal_scrollbar(
4539 &self,
4540 _: &mut Window,
4541 cx: &mut Context<Self>,
4542 ) -> Option<Stateful<Div>> {
4543 if !Self::should_show_scrollbar(cx)
4544 || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4545 {
4546 return None;
4547 }
4548 Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| {
4549 div()
4550 .occlude()
4551 .id("project-panel-horizontal-scroll")
4552 .on_mouse_move(cx.listener(|_, _, _, cx| {
4553 cx.notify();
4554 cx.stop_propagation()
4555 }))
4556 .on_hover(|_, _, cx| {
4557 cx.stop_propagation();
4558 })
4559 .on_any_mouse_down(|_, _, cx| {
4560 cx.stop_propagation();
4561 })
4562 .on_mouse_up(
4563 MouseButton::Left,
4564 cx.listener(|outline_panel, _, window, cx| {
4565 if !outline_panel.horizontal_scrollbar_state.is_dragging()
4566 && !outline_panel.focus_handle.contains_focused(window, cx)
4567 {
4568 outline_panel.hide_scrollbar(window, cx);
4569 cx.notify();
4570 }
4571
4572 cx.stop_propagation();
4573 }),
4574 )
4575 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4576 cx.notify();
4577 }))
4578 .w_full()
4579 .absolute()
4580 .right_1()
4581 .left_1()
4582 .bottom_0()
4583 .h(px(12.))
4584 .cursor_default()
4585 .child(scrollbar)
4586 })
4587 }
4588
4589 fn should_show_scrollbar(cx: &App) -> bool {
4590 let show = OutlinePanelSettings::get_global(cx)
4591 .scrollbar
4592 .show
4593 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4594 match show {
4595 ShowScrollbar::Auto => true,
4596 ShowScrollbar::System => true,
4597 ShowScrollbar::Always => true,
4598 ShowScrollbar::Never => false,
4599 }
4600 }
4601
4602 fn should_autohide_scrollbar(cx: &App) -> bool {
4603 let show = OutlinePanelSettings::get_global(cx)
4604 .scrollbar
4605 .show
4606 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4607 match show {
4608 ShowScrollbar::Auto => true,
4609 ShowScrollbar::System => cx
4610 .try_global::<ScrollbarAutoHide>()
4611 .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4612 ShowScrollbar::Always => false,
4613 ShowScrollbar::Never => true,
4614 }
4615 }
4616
4617 fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4618 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4619 if !Self::should_autohide_scrollbar(cx) {
4620 return;
4621 }
4622 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4623 cx.background_executor()
4624 .timer(SCROLLBAR_SHOW_INTERVAL)
4625 .await;
4626 panel
4627 .update(cx, |panel, cx| {
4628 panel.show_scrollbar = false;
4629 cx.notify();
4630 })
4631 .log_err();
4632 }))
4633 }
4634
4635 fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
4636 let item_text_chars = match entry {
4637 PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
4638 .buffer_snapshot_for_id(external.buffer_id, cx)
4639 .and_then(|snapshot| {
4640 Some(snapshot.file()?.path().file_name()?.to_string_lossy().len())
4641 })
4642 .unwrap_or_default(),
4643 PanelEntry::Fs(FsEntry::Directory(directory)) => directory
4644 .entry
4645 .path
4646 .file_name()
4647 .map(|name| name.to_string_lossy().len())
4648 .unwrap_or_default(),
4649 PanelEntry::Fs(FsEntry::File(file)) => file
4650 .entry
4651 .path
4652 .file_name()
4653 .map(|name| name.to_string_lossy().len())
4654 .unwrap_or_default(),
4655 PanelEntry::FoldedDirs(folded_dirs) => {
4656 folded_dirs
4657 .entries
4658 .iter()
4659 .map(|dir| {
4660 dir.path
4661 .file_name()
4662 .map(|name| name.to_string_lossy().len())
4663 .unwrap_or_default()
4664 })
4665 .sum::<usize>()
4666 + folded_dirs.entries.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len()
4667 }
4668 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
4669 .excerpt_label(excerpt.buffer_id, &excerpt.range, cx)
4670 .map(|label| label.len())
4671 .unwrap_or_default(),
4672 PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(),
4673 PanelEntry::Search(search) => search
4674 .render_data
4675 .get()
4676 .map(|data| data.context_text.len())
4677 .unwrap_or_default(),
4678 };
4679
4680 (item_text_chars + depth) as u64
4681 }
4682
4683 fn render_main_contents(
4684 &mut self,
4685 query: Option<String>,
4686 show_indent_guides: bool,
4687 indent_size: f32,
4688 window: &mut Window,
4689 cx: &mut Context<Self>,
4690 ) -> Div {
4691 let contents = if self.cached_entries.is_empty() {
4692 let header = if self.updating_fs_entries || self.updating_cached_entries {
4693 None
4694 } else if query.is_some() {
4695 Some("No matches for query")
4696 } else {
4697 Some("No outlines available")
4698 };
4699
4700 v_flex()
4701 .flex_1()
4702 .justify_center()
4703 .size_full()
4704 .when_some(header, |panel, header| {
4705 panel
4706 .child(h_flex().justify_center().child(Label::new(header)))
4707 .when_some(query.clone(), |panel, query| {
4708 panel.child(h_flex().justify_center().child(Label::new(query)))
4709 })
4710 .child(
4711 h_flex()
4712 .pt(DynamicSpacing::Base04.rems(cx))
4713 .justify_center()
4714 .child({
4715 let keystroke =
4716 match self.position(window, cx) {
4717 DockPosition::Left => window
4718 .keystroke_text_for(&workspace::ToggleLeftDock),
4719 DockPosition::Bottom => window
4720 .keystroke_text_for(&workspace::ToggleBottomDock),
4721 DockPosition::Right => window
4722 .keystroke_text_for(&workspace::ToggleRightDock),
4723 };
4724 Label::new(format!("Toggle this panel with {keystroke}"))
4725 }),
4726 )
4727 })
4728 } else {
4729 let list_contents = {
4730 let items_len = self.cached_entries.len();
4731 let multi_buffer_snapshot = self
4732 .active_editor()
4733 .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
4734 uniform_list(
4735 "entries",
4736 items_len,
4737 cx.processor(move |outline_panel, range: Range<usize>, window, cx| {
4738 let entries = outline_panel.cached_entries.get(range);
4739 entries
4740 .map(|entries| entries.to_vec())
4741 .unwrap_or_default()
4742 .into_iter()
4743 .filter_map(|cached_entry| match cached_entry.entry {
4744 PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4745 &entry,
4746 cached_entry.depth,
4747 cached_entry.string_match.as_ref(),
4748 window,
4749 cx,
4750 )),
4751 PanelEntry::FoldedDirs(folded_dirs_entry) => {
4752 Some(outline_panel.render_folded_dirs(
4753 &folded_dirs_entry,
4754 cached_entry.depth,
4755 cached_entry.string_match.as_ref(),
4756 window,
4757 cx,
4758 ))
4759 }
4760 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4761 outline_panel.render_excerpt(
4762 &excerpt,
4763 cached_entry.depth,
4764 window,
4765 cx,
4766 )
4767 }
4768 PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4769 Some(outline_panel.render_outline(
4770 &entry,
4771 cached_entry.depth,
4772 cached_entry.string_match.as_ref(),
4773 window,
4774 cx,
4775 ))
4776 }
4777 PanelEntry::Search(SearchEntry {
4778 match_range,
4779 render_data,
4780 kind,
4781 ..
4782 }) => outline_panel.render_search_match(
4783 multi_buffer_snapshot.as_ref(),
4784 &match_range,
4785 &render_data,
4786 kind,
4787 cached_entry.depth,
4788 cached_entry.string_match.as_ref(),
4789 window,
4790 cx,
4791 ),
4792 })
4793 .collect()
4794 }),
4795 )
4796 .with_sizing_behavior(ListSizingBehavior::Infer)
4797 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4798 .with_width_from_item(self.max_width_item_index)
4799 .track_scroll(self.scroll_handle.clone())
4800 .when(show_indent_guides, |list| {
4801 list.with_decoration(
4802 ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
4803 .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| {
4804 let entries = outline_panel.cached_entries.get(range);
4805 if let Some(entries) = entries {
4806 entries.into_iter().map(|item| item.depth).collect()
4807 } else {
4808 smallvec::SmallVec::new()
4809 }
4810 })
4811 .with_render_fn(cx.entity(), move |outline_panel, params, _, _| {
4812 const LEFT_OFFSET: Pixels = px(14.);
4813
4814 let indent_size = params.indent_size;
4815 let item_height = params.item_height;
4816 let active_indent_guide_ix = find_active_indent_guide_ix(
4817 outline_panel,
4818 ¶ms.indent_guides,
4819 );
4820
4821 params
4822 .indent_guides
4823 .into_iter()
4824 .enumerate()
4825 .map(|(ix, layout)| {
4826 let bounds = Bounds::new(
4827 point(
4828 layout.offset.x * indent_size + LEFT_OFFSET,
4829 layout.offset.y * item_height,
4830 ),
4831 size(px(1.), layout.length * item_height),
4832 );
4833 ui::RenderedIndentGuide {
4834 bounds,
4835 layout,
4836 is_active: active_indent_guide_ix == Some(ix),
4837 hitbox: None,
4838 }
4839 })
4840 .collect()
4841 }),
4842 )
4843 })
4844 };
4845
4846 v_flex()
4847 .flex_shrink()
4848 .size_full()
4849 .child(list_contents.size_full().flex_shrink())
4850 .children(self.render_vertical_scrollbar(cx))
4851 .when_some(
4852 self.render_horizontal_scrollbar(window, cx),
4853 |this, scrollbar| this.pb_4().child(scrollbar),
4854 )
4855 }
4856 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4857 deferred(
4858 anchored()
4859 .position(*position)
4860 .anchor(gpui::Corner::TopLeft)
4861 .child(menu.clone()),
4862 )
4863 .with_priority(1)
4864 }));
4865
4866 v_flex().w_full().flex_1().overflow_hidden().child(contents)
4867 }
4868
4869 fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4870 v_flex().flex_none().child(horizontal_separator(cx)).child(
4871 h_flex()
4872 .p_2()
4873 .w_full()
4874 .child(self.filter_editor.clone())
4875 .child(
4876 div().child(
4877 IconButton::new(
4878 "outline-panel-menu",
4879 if pinned {
4880 IconName::Unpin
4881 } else {
4882 IconName::Pin
4883 },
4884 )
4885 .tooltip(Tooltip::text(if pinned {
4886 "Unpin Outline"
4887 } else {
4888 "Pin Active Outline"
4889 }))
4890 .shape(IconButtonShape::Square)
4891 .on_click(cx.listener(
4892 |outline_panel, _, window, cx| {
4893 outline_panel.toggle_active_editor_pin(
4894 &ToggleActiveEditorPin,
4895 window,
4896 cx,
4897 );
4898 },
4899 )),
4900 ),
4901 ),
4902 )
4903 }
4904
4905 fn buffers_inside_directory(
4906 &self,
4907 dir_worktree: WorktreeId,
4908 dir_entry: &GitEntry,
4909 ) -> HashSet<BufferId> {
4910 if !dir_entry.is_dir() {
4911 debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4912 return HashSet::default();
4913 }
4914
4915 self.fs_entries
4916 .iter()
4917 .skip_while(|fs_entry| match fs_entry {
4918 FsEntry::Directory(directory) => {
4919 directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4920 }
4921 _ => true,
4922 })
4923 .skip(1)
4924 .take_while(|fs_entry| match fs_entry {
4925 FsEntry::ExternalFile(..) => false,
4926 FsEntry::Directory(directory) => {
4927 directory.worktree_id == dir_worktree
4928 && directory.entry.path.starts_with(&dir_entry.path)
4929 }
4930 FsEntry::File(file) => {
4931 file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4932 }
4933 })
4934 .filter_map(|fs_entry| match fs_entry {
4935 FsEntry::File(file) => Some(file.buffer_id),
4936 _ => None,
4937 })
4938 .collect()
4939 }
4940}
4941
4942fn workspace_active_editor(
4943 workspace: &Workspace,
4944 cx: &App,
4945) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4946 let active_item = workspace.active_item(cx)?;
4947 let active_editor = active_item
4948 .act_as::<Editor>(cx)
4949 .filter(|editor| editor.read(cx).mode().is_full())?;
4950 Some((active_item, active_editor))
4951}
4952
4953fn back_to_common_visited_parent(
4954 visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
4955 worktree_id: &WorktreeId,
4956 new_entry: &Entry,
4957) -> Option<(WorktreeId, ProjectEntryId)> {
4958 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4959 match new_entry.path.parent() {
4960 Some(parent_path) => {
4961 if parent_path == visited_path.as_ref() {
4962 return Some((*worktree_id, *visited_dir_id));
4963 }
4964 }
4965 None => {
4966 break;
4967 }
4968 }
4969 visited_dirs.pop();
4970 }
4971 None
4972}
4973
4974fn file_name(path: &Path) -> String {
4975 let mut current_path = path;
4976 loop {
4977 if let Some(file_name) = current_path.file_name() {
4978 return file_name.to_string_lossy().into_owned();
4979 }
4980 match current_path.parent() {
4981 Some(parent) => current_path = parent,
4982 None => return path.to_string_lossy().into_owned(),
4983 }
4984 }
4985}
4986
4987impl Panel for OutlinePanel {
4988 fn persistent_name() -> &'static str {
4989 "Outline Panel"
4990 }
4991
4992 fn position(&self, _: &Window, cx: &App) -> DockPosition {
4993 match OutlinePanelSettings::get_global(cx).dock {
4994 OutlinePanelDockPosition::Left => DockPosition::Left,
4995 OutlinePanelDockPosition::Right => DockPosition::Right,
4996 }
4997 }
4998
4999 fn position_is_valid(&self, position: DockPosition) -> bool {
5000 matches!(position, DockPosition::Left | DockPosition::Right)
5001 }
5002
5003 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
5004 settings::update_settings_file::<OutlinePanelSettings>(
5005 self.fs.clone(),
5006 cx,
5007 move |settings, _| {
5008 let dock = match position {
5009 DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
5010 DockPosition::Right => OutlinePanelDockPosition::Right,
5011 };
5012 settings.dock = Some(dock);
5013 },
5014 );
5015 }
5016
5017 fn size(&self, _: &Window, cx: &App) -> Pixels {
5018 self.width
5019 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
5020 }
5021
5022 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
5023 self.width = size;
5024 cx.notify();
5025 cx.defer_in(window, |this, _, cx| {
5026 this.serialize(cx);
5027 });
5028 }
5029
5030 fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
5031 OutlinePanelSettings::get_global(cx)
5032 .button
5033 .then_some(IconName::ListTree)
5034 }
5035
5036 fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
5037 Some("Outline Panel")
5038 }
5039
5040 fn toggle_action(&self) -> Box<dyn Action> {
5041 Box::new(ToggleFocus)
5042 }
5043
5044 fn starts_open(&self, _window: &Window, _: &App) -> bool {
5045 self.active
5046 }
5047
5048 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
5049 cx.spawn_in(window, async move |outline_panel, cx| {
5050 outline_panel
5051 .update_in(cx, |outline_panel, window, cx| {
5052 let old_active = outline_panel.active;
5053 outline_panel.active = active;
5054 if old_active != active {
5055 if active
5056 && let Some((active_item, active_editor)) =
5057 outline_panel.workspace.upgrade().and_then(|workspace| {
5058 workspace_active_editor(workspace.read(cx), cx)
5059 })
5060 {
5061 if outline_panel.should_replace_active_item(active_item.as_ref()) {
5062 outline_panel.replace_active_editor(
5063 active_item,
5064 active_editor,
5065 window,
5066 cx,
5067 );
5068 } else {
5069 outline_panel.update_fs_entries(active_editor, None, window, cx)
5070 }
5071 return;
5072 }
5073
5074 if !outline_panel.pinned {
5075 outline_panel.clear_previous(window, cx);
5076 }
5077 }
5078 outline_panel.serialize(cx);
5079 })
5080 .ok();
5081 })
5082 .detach()
5083 }
5084
5085 fn activation_priority(&self) -> u32 {
5086 5
5087 }
5088}
5089
5090impl Focusable for OutlinePanel {
5091 fn focus_handle(&self, cx: &App) -> FocusHandle {
5092 self.filter_editor.focus_handle(cx).clone()
5093 }
5094}
5095
5096impl EventEmitter<Event> for OutlinePanel {}
5097
5098impl EventEmitter<PanelEvent> for OutlinePanel {}
5099
5100impl Render for OutlinePanel {
5101 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5102 let (is_local, is_via_ssh) = self
5103 .project
5104 .read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
5105 let query = self.query(cx);
5106 let pinned = self.pinned;
5107 let settings = OutlinePanelSettings::get_global(cx);
5108 let indent_size = settings.indent_size;
5109 let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
5110
5111 let search_query = match &self.mode {
5112 ItemsDisplayMode::Search(search_query) => Some(search_query),
5113 _ => None,
5114 };
5115
5116 v_flex()
5117 .id("outline-panel")
5118 .size_full()
5119 .overflow_hidden()
5120 .relative()
5121 .on_hover(cx.listener(|this, hovered, window, cx| {
5122 if *hovered {
5123 this.show_scrollbar = true;
5124 this.hide_scrollbar_task.take();
5125 cx.notify();
5126 } else if !this.focus_handle.contains_focused(window, cx) {
5127 this.hide_scrollbar(window, cx);
5128 }
5129 }))
5130 .key_context(self.dispatch_context(window, cx))
5131 .on_action(cx.listener(Self::open_selected_entry))
5132 .on_action(cx.listener(Self::cancel))
5133 .on_action(cx.listener(Self::select_next))
5134 .on_action(cx.listener(Self::select_previous))
5135 .on_action(cx.listener(Self::select_first))
5136 .on_action(cx.listener(Self::select_last))
5137 .on_action(cx.listener(Self::select_parent))
5138 .on_action(cx.listener(Self::expand_selected_entry))
5139 .on_action(cx.listener(Self::collapse_selected_entry))
5140 .on_action(cx.listener(Self::expand_all_entries))
5141 .on_action(cx.listener(Self::collapse_all_entries))
5142 .on_action(cx.listener(Self::copy_path))
5143 .on_action(cx.listener(Self::copy_relative_path))
5144 .on_action(cx.listener(Self::toggle_active_editor_pin))
5145 .on_action(cx.listener(Self::unfold_directory))
5146 .on_action(cx.listener(Self::fold_directory))
5147 .on_action(cx.listener(Self::open_excerpts))
5148 .on_action(cx.listener(Self::open_excerpts_split))
5149 .when(is_local, |el| {
5150 el.on_action(cx.listener(Self::reveal_in_finder))
5151 })
5152 .when(is_local || is_via_ssh, |el| {
5153 el.on_action(cx.listener(Self::open_in_terminal))
5154 })
5155 .on_mouse_down(
5156 MouseButton::Right,
5157 cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
5158 if let Some(entry) = outline_panel.selected_entry().cloned() {
5159 outline_panel.deploy_context_menu(event.position, entry, window, cx)
5160 } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
5161 outline_panel.deploy_context_menu(
5162 event.position,
5163 PanelEntry::Fs(entry),
5164 window,
5165 cx,
5166 )
5167 }
5168 }),
5169 )
5170 .track_focus(&self.focus_handle)
5171 .when_some(search_query, |outline_panel, search_state| {
5172 outline_panel.child(
5173 h_flex()
5174 .py_1p5()
5175 .px_2()
5176 .h(DynamicSpacing::Base32.px(cx))
5177 .flex_shrink_0()
5178 .border_b_1()
5179 .border_color(cx.theme().colors().border)
5180 .gap_0p5()
5181 .child(Label::new("Searching:").color(Color::Muted))
5182 .child(Label::new(search_state.query.to_string())),
5183 )
5184 })
5185 .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
5186 .child(self.render_filter_footer(pinned, cx))
5187 }
5188}
5189
5190fn find_active_indent_guide_ix(
5191 outline_panel: &OutlinePanel,
5192 candidates: &[IndentGuideLayout],
5193) -> Option<usize> {
5194 let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
5195 return None;
5196 };
5197 let target_depth = outline_panel
5198 .cached_entries
5199 .get(*target_ix)
5200 .map(|cached_entry| cached_entry.depth)?;
5201
5202 let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
5203 .cached_entries
5204 .get(target_ix + 1)
5205 .filter(|cached_entry| cached_entry.depth > target_depth)
5206 .map(|entry| entry.depth)
5207 {
5208 (target_ix + 1, target_depth.saturating_sub(1))
5209 } else {
5210 (*target_ix, target_depth.saturating_sub(1))
5211 };
5212
5213 candidates
5214 .iter()
5215 .enumerate()
5216 .find(|(_, guide)| {
5217 guide.offset.y <= target_ix
5218 && target_ix < guide.offset.y + guide.length
5219 && guide.offset.x == target_depth
5220 })
5221 .map(|(ix, _)| ix)
5222}
5223
5224fn subscribe_for_editor_events(
5225 editor: &Entity<Editor>,
5226 window: &mut Window,
5227 cx: &mut Context<OutlinePanel>,
5228) -> Subscription {
5229 let debounce = Some(UPDATE_DEBOUNCE);
5230 cx.subscribe_in(
5231 editor,
5232 window,
5233 move |outline_panel, editor, e: &EditorEvent, window, cx| {
5234 if !outline_panel.active {
5235 return;
5236 }
5237 match e {
5238 EditorEvent::SelectionsChanged { local: true } => {
5239 outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5240 cx.notify();
5241 }
5242 EditorEvent::ExcerptsAdded { excerpts, .. } => {
5243 outline_panel
5244 .new_entries_for_fs_update
5245 .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
5246 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5247 }
5248 EditorEvent::ExcerptsRemoved { ids, .. } => {
5249 let mut ids = ids.iter().collect::<HashSet<_>>();
5250 for excerpts in outline_panel.excerpts.values_mut() {
5251 excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
5252 if ids.is_empty() {
5253 break;
5254 }
5255 }
5256 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5257 }
5258 EditorEvent::ExcerptsExpanded { ids } => {
5259 outline_panel.invalidate_outlines(ids);
5260 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5261 if update_cached_items {
5262 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5263 }
5264 }
5265 EditorEvent::ExcerptsEdited { 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::BufferFoldToggled { ids, .. } => {
5273 outline_panel.invalidate_outlines(ids);
5274 let mut latest_unfolded_buffer_id = None;
5275 let mut latest_folded_buffer_id = None;
5276 let mut ignore_selections_change = false;
5277 outline_panel.new_entries_for_fs_update.extend(
5278 ids.iter()
5279 .filter(|id| {
5280 outline_panel
5281 .excerpts
5282 .iter()
5283 .find_map(|(buffer_id, excerpts)| {
5284 if excerpts.contains_key(id) {
5285 ignore_selections_change |= outline_panel
5286 .preserve_selection_on_buffer_fold_toggles
5287 .remove(buffer_id);
5288 Some(buffer_id)
5289 } else {
5290 None
5291 }
5292 })
5293 .map(|buffer_id| {
5294 if editor.read(cx).is_buffer_folded(*buffer_id, cx) {
5295 latest_folded_buffer_id = Some(*buffer_id);
5296 false
5297 } else {
5298 latest_unfolded_buffer_id = Some(*buffer_id);
5299 true
5300 }
5301 })
5302 .unwrap_or(true)
5303 })
5304 .copied(),
5305 );
5306 if !ignore_selections_change
5307 && let Some(entry_to_select) = latest_unfolded_buffer_id
5308 .or(latest_folded_buffer_id)
5309 .and_then(|toggled_buffer_id| {
5310 outline_panel.fs_entries.iter().find_map(
5311 |fs_entry| match fs_entry {
5312 FsEntry::ExternalFile(external) => {
5313 if external.buffer_id == toggled_buffer_id {
5314 Some(fs_entry.clone())
5315 } else {
5316 None
5317 }
5318 }
5319 FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5320 if *buffer_id == toggled_buffer_id {
5321 Some(fs_entry.clone())
5322 } else {
5323 None
5324 }
5325 }
5326 FsEntry::Directory(..) => None,
5327 },
5328 )
5329 })
5330 .map(PanelEntry::Fs)
5331 {
5332 outline_panel.select_entry(entry_to_select, true, window, cx);
5333 }
5334
5335 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5336 }
5337 EditorEvent::Reparsed(buffer_id) => {
5338 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
5339 for (_, excerpt) in excerpts {
5340 excerpt.invalidate_outlines();
5341 }
5342 }
5343 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5344 if update_cached_items {
5345 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5346 }
5347 }
5348 _ => {}
5349 }
5350 },
5351 )
5352}
5353
5354fn empty_icon() -> AnyElement {
5355 h_flex()
5356 .size(IconSize::default().rems())
5357 .invisible()
5358 .flex_none()
5359 .into_any_element()
5360}
5361
5362fn horizontal_separator(cx: &mut App) -> Div {
5363 div().mx_2().border_primary(cx).border_t_1()
5364}
5365
5366#[derive(Debug, Default)]
5367struct GenerationState {
5368 entries: Vec<CachedEntry>,
5369 match_candidates: Vec<StringMatchCandidate>,
5370 max_width_estimate_and_index: Option<(u64, usize)>,
5371}
5372
5373impl GenerationState {
5374 fn clear(&mut self) {
5375 self.entries.clear();
5376 self.match_candidates.clear();
5377 self.max_width_estimate_and_index = None;
5378 }
5379}
5380
5381#[cfg(test)]
5382mod tests {
5383 use db::indoc;
5384 use gpui::{TestAppContext, VisualTestContext, WindowHandle};
5385 use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
5386 use pretty_assertions::assert_eq;
5387 use project::FakeFs;
5388 use search::project_search::{self, perform_project_search};
5389 use serde_json::json;
5390 use util::path;
5391 use workspace::{OpenOptions, OpenVisible};
5392
5393 use super::*;
5394
5395 const SELECTED_MARKER: &str = " <==== selected";
5396
5397 #[gpui::test(iterations = 10)]
5398 async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5399 init_test(cx);
5400
5401 let fs = FakeFs::new(cx.background_executor.clone());
5402 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5403 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5404 project.read_with(cx, |project, _| {
5405 project.languages().add(Arc::new(rust_lang()))
5406 });
5407 let workspace = add_outline_panel(&project, cx).await;
5408 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5409 let outline_panel = outline_panel(&workspace, cx);
5410 outline_panel.update_in(cx, |outline_panel, window, cx| {
5411 outline_panel.set_active(true, window, cx)
5412 });
5413
5414 workspace
5415 .update(cx, |workspace, window, cx| {
5416 ProjectSearchView::deploy_search(
5417 workspace,
5418 &workspace::DeploySearch::default(),
5419 window,
5420 cx,
5421 )
5422 })
5423 .unwrap();
5424 let search_view = workspace
5425 .update(cx, |workspace, _, cx| {
5426 workspace
5427 .active_pane()
5428 .read(cx)
5429 .items()
5430 .find_map(|item| item.downcast::<ProjectSearchView>())
5431 .expect("Project search view expected to appear after new search event trigger")
5432 })
5433 .unwrap();
5434
5435 let query = "param_names_for_lifetime_elision_hints";
5436 perform_project_search(&search_view, query, cx);
5437 search_view.update(cx, |search_view, cx| {
5438 search_view
5439 .results_editor()
5440 .update(cx, |results_editor, cx| {
5441 assert_eq!(
5442 results_editor.display_text(cx).match_indices(query).count(),
5443 9
5444 );
5445 });
5446 });
5447
5448 let all_matches = r#"/rust-analyzer/
5449 crates/
5450 ide/src/
5451 inlay_hints/
5452 fn_lifetime_fn.rs
5453 search: match config.param_names_for_lifetime_elision_hints {
5454 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5455 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5456 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5457 inlay_hints.rs
5458 search: pub param_names_for_lifetime_elision_hints: bool,
5459 search: param_names_for_lifetime_elision_hints: self
5460 static_index.rs
5461 search: param_names_for_lifetime_elision_hints: false,
5462 rust-analyzer/src/
5463 cli/
5464 analysis_stats.rs
5465 search: param_names_for_lifetime_elision_hints: true,
5466 config.rs
5467 search: param_names_for_lifetime_elision_hints: self"#;
5468 let select_first_in_all_matches = |line_to_select: &str| {
5469 assert!(all_matches.contains(line_to_select));
5470 all_matches.replacen(
5471 line_to_select,
5472 &format!("{line_to_select}{SELECTED_MARKER}"),
5473 1,
5474 )
5475 };
5476
5477 cx.executor()
5478 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5479 cx.run_until_parked();
5480 outline_panel.update(cx, |outline_panel, cx| {
5481 assert_eq!(
5482 display_entries(
5483 &project,
5484 &snapshot(outline_panel, cx),
5485 &outline_panel.cached_entries,
5486 outline_panel.selected_entry(),
5487 cx,
5488 ),
5489 select_first_in_all_matches(
5490 "search: match config.param_names_for_lifetime_elision_hints {"
5491 )
5492 );
5493 });
5494
5495 outline_panel.update_in(cx, |outline_panel, window, cx| {
5496 outline_panel.select_parent(&SelectParent, window, cx);
5497 assert_eq!(
5498 display_entries(
5499 &project,
5500 &snapshot(outline_panel, cx),
5501 &outline_panel.cached_entries,
5502 outline_panel.selected_entry(),
5503 cx,
5504 ),
5505 select_first_in_all_matches("fn_lifetime_fn.rs")
5506 );
5507 });
5508 outline_panel.update_in(cx, |outline_panel, window, cx| {
5509 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5510 });
5511 cx.executor()
5512 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5513 cx.run_until_parked();
5514 outline_panel.update(cx, |outline_panel, cx| {
5515 assert_eq!(
5516 display_entries(
5517 &project,
5518 &snapshot(outline_panel, cx),
5519 &outline_panel.cached_entries,
5520 outline_panel.selected_entry(),
5521 cx,
5522 ),
5523 format!(
5524 r#"/rust-analyzer/
5525 crates/
5526 ide/src/
5527 inlay_hints/
5528 fn_lifetime_fn.rs{SELECTED_MARKER}
5529 inlay_hints.rs
5530 search: pub param_names_for_lifetime_elision_hints: bool,
5531 search: param_names_for_lifetime_elision_hints: self
5532 static_index.rs
5533 search: param_names_for_lifetime_elision_hints: false,
5534 rust-analyzer/src/
5535 cli/
5536 analysis_stats.rs
5537 search: param_names_for_lifetime_elision_hints: true,
5538 config.rs
5539 search: param_names_for_lifetime_elision_hints: self"#,
5540 )
5541 );
5542 });
5543
5544 outline_panel.update_in(cx, |outline_panel, window, cx| {
5545 outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5546 });
5547 cx.executor()
5548 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5549 cx.run_until_parked();
5550 outline_panel.update_in(cx, |outline_panel, window, cx| {
5551 outline_panel.select_parent(&SelectParent, window, cx);
5552 assert_eq!(
5553 display_entries(
5554 &project,
5555 &snapshot(outline_panel, cx),
5556 &outline_panel.cached_entries,
5557 outline_panel.selected_entry(),
5558 cx,
5559 ),
5560 select_first_in_all_matches("inlay_hints/")
5561 );
5562 });
5563
5564 outline_panel.update_in(cx, |outline_panel, window, cx| {
5565 outline_panel.select_parent(&SelectParent, window, cx);
5566 assert_eq!(
5567 display_entries(
5568 &project,
5569 &snapshot(outline_panel, cx),
5570 &outline_panel.cached_entries,
5571 outline_panel.selected_entry(),
5572 cx,
5573 ),
5574 select_first_in_all_matches("ide/src/")
5575 );
5576 });
5577
5578 outline_panel.update_in(cx, |outline_panel, window, cx| {
5579 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5580 });
5581 cx.executor()
5582 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5583 cx.run_until_parked();
5584 outline_panel.update(cx, |outline_panel, cx| {
5585 assert_eq!(
5586 display_entries(
5587 &project,
5588 &snapshot(outline_panel, cx),
5589 &outline_panel.cached_entries,
5590 outline_panel.selected_entry(),
5591 cx,
5592 ),
5593 format!(
5594 r#"/rust-analyzer/
5595 crates/
5596 ide/src/{SELECTED_MARKER}
5597 rust-analyzer/src/
5598 cli/
5599 analysis_stats.rs
5600 search: param_names_for_lifetime_elision_hints: true,
5601 config.rs
5602 search: param_names_for_lifetime_elision_hints: self"#,
5603 )
5604 );
5605 });
5606 outline_panel.update_in(cx, |outline_panel, window, cx| {
5607 outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5608 });
5609 cx.executor()
5610 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5611 cx.run_until_parked();
5612 outline_panel.update(cx, |outline_panel, cx| {
5613 assert_eq!(
5614 display_entries(
5615 &project,
5616 &snapshot(outline_panel, cx),
5617 &outline_panel.cached_entries,
5618 outline_panel.selected_entry(),
5619 cx,
5620 ),
5621 select_first_in_all_matches("ide/src/")
5622 );
5623 });
5624 }
5625
5626 #[gpui::test(iterations = 10)]
5627 async fn test_item_filtering(cx: &mut TestAppContext) {
5628 init_test(cx);
5629
5630 let fs = FakeFs::new(cx.background_executor.clone());
5631 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5632 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5633 project.read_with(cx, |project, _| {
5634 project.languages().add(Arc::new(rust_lang()))
5635 });
5636 let workspace = add_outline_panel(&project, cx).await;
5637 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5638 let outline_panel = outline_panel(&workspace, cx);
5639 outline_panel.update_in(cx, |outline_panel, window, cx| {
5640 outline_panel.set_active(true, window, cx)
5641 });
5642
5643 workspace
5644 .update(cx, |workspace, window, cx| {
5645 ProjectSearchView::deploy_search(
5646 workspace,
5647 &workspace::DeploySearch::default(),
5648 window,
5649 cx,
5650 )
5651 })
5652 .unwrap();
5653 let search_view = workspace
5654 .update(cx, |workspace, _, cx| {
5655 workspace
5656 .active_pane()
5657 .read(cx)
5658 .items()
5659 .find_map(|item| item.downcast::<ProjectSearchView>())
5660 .expect("Project search view expected to appear after new search event trigger")
5661 })
5662 .unwrap();
5663
5664 let query = "param_names_for_lifetime_elision_hints";
5665 perform_project_search(&search_view, query, cx);
5666 search_view.update(cx, |search_view, cx| {
5667 search_view
5668 .results_editor()
5669 .update(cx, |results_editor, cx| {
5670 assert_eq!(
5671 results_editor.display_text(cx).match_indices(query).count(),
5672 9
5673 );
5674 });
5675 });
5676 let all_matches = r#"/rust-analyzer/
5677 crates/
5678 ide/src/
5679 inlay_hints/
5680 fn_lifetime_fn.rs
5681 search: match config.param_names_for_lifetime_elision_hints {
5682 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5683 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5684 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5685 inlay_hints.rs
5686 search: pub param_names_for_lifetime_elision_hints: bool,
5687 search: param_names_for_lifetime_elision_hints: self
5688 static_index.rs
5689 search: param_names_for_lifetime_elision_hints: false,
5690 rust-analyzer/src/
5691 cli/
5692 analysis_stats.rs
5693 search: param_names_for_lifetime_elision_hints: true,
5694 config.rs
5695 search: param_names_for_lifetime_elision_hints: self"#;
5696
5697 cx.executor()
5698 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5699 cx.run_until_parked();
5700 outline_panel.update(cx, |outline_panel, cx| {
5701 assert_eq!(
5702 display_entries(
5703 &project,
5704 &snapshot(outline_panel, cx),
5705 &outline_panel.cached_entries,
5706 None,
5707 cx,
5708 ),
5709 all_matches,
5710 );
5711 });
5712
5713 let filter_text = "a";
5714 outline_panel.update_in(cx, |outline_panel, window, cx| {
5715 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5716 filter_editor.set_text(filter_text, window, cx);
5717 });
5718 });
5719 cx.executor()
5720 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5721 cx.run_until_parked();
5722
5723 outline_panel.update(cx, |outline_panel, cx| {
5724 assert_eq!(
5725 display_entries(
5726 &project,
5727 &snapshot(outline_panel, cx),
5728 &outline_panel.cached_entries,
5729 None,
5730 cx,
5731 ),
5732 all_matches
5733 .lines()
5734 .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5735 .filter(|item| item.contains(filter_text))
5736 .collect::<Vec<_>>()
5737 .join("\n"),
5738 );
5739 });
5740
5741 outline_panel.update_in(cx, |outline_panel, window, cx| {
5742 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5743 filter_editor.set_text("", window, cx);
5744 });
5745 });
5746 cx.executor()
5747 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5748 cx.run_until_parked();
5749 outline_panel.update(cx, |outline_panel, cx| {
5750 assert_eq!(
5751 display_entries(
5752 &project,
5753 &snapshot(outline_panel, cx),
5754 &outline_panel.cached_entries,
5755 None,
5756 cx,
5757 ),
5758 all_matches,
5759 );
5760 });
5761 }
5762
5763 #[gpui::test(iterations = 10)]
5764 async fn test_item_opening(cx: &mut TestAppContext) {
5765 init_test(cx);
5766
5767 let fs = FakeFs::new(cx.background_executor.clone());
5768 populate_with_test_ra_project(&fs, path!("/rust-analyzer")).await;
5769 let project = Project::test(fs.clone(), [path!("/rust-analyzer").as_ref()], cx).await;
5770 project.read_with(cx, |project, _| {
5771 project.languages().add(Arc::new(rust_lang()))
5772 });
5773 let workspace = add_outline_panel(&project, cx).await;
5774 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5775 let outline_panel = outline_panel(&workspace, cx);
5776 outline_panel.update_in(cx, |outline_panel, window, cx| {
5777 outline_panel.set_active(true, window, cx)
5778 });
5779
5780 workspace
5781 .update(cx, |workspace, window, cx| {
5782 ProjectSearchView::deploy_search(
5783 workspace,
5784 &workspace::DeploySearch::default(),
5785 window,
5786 cx,
5787 )
5788 })
5789 .unwrap();
5790 let search_view = workspace
5791 .update(cx, |workspace, _, cx| {
5792 workspace
5793 .active_pane()
5794 .read(cx)
5795 .items()
5796 .find_map(|item| item.downcast::<ProjectSearchView>())
5797 .expect("Project search view expected to appear after new search event trigger")
5798 })
5799 .unwrap();
5800
5801 let query = "param_names_for_lifetime_elision_hints";
5802 perform_project_search(&search_view, query, cx);
5803 search_view.update(cx, |search_view, cx| {
5804 search_view
5805 .results_editor()
5806 .update(cx, |results_editor, cx| {
5807 assert_eq!(
5808 results_editor.display_text(cx).match_indices(query).count(),
5809 9
5810 );
5811 });
5812 });
5813 let root_path = format!("{}/", path!("/rust-analyzer"));
5814 let all_matches = format!(
5815 r#"{root_path}
5816 crates/
5817 ide/src/
5818 inlay_hints/
5819 fn_lifetime_fn.rs
5820 search: match config.param_names_for_lifetime_elision_hints {{
5821 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
5822 search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
5823 search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
5824 inlay_hints.rs
5825 search: pub param_names_for_lifetime_elision_hints: bool,
5826 search: param_names_for_lifetime_elision_hints: self
5827 static_index.rs
5828 search: param_names_for_lifetime_elision_hints: false,
5829 rust-analyzer/src/
5830 cli/
5831 analysis_stats.rs
5832 search: param_names_for_lifetime_elision_hints: true,
5833 config.rs
5834 search: param_names_for_lifetime_elision_hints: self"#
5835 );
5836 let select_first_in_all_matches = |line_to_select: &str| {
5837 assert!(all_matches.contains(line_to_select));
5838 all_matches.replacen(
5839 line_to_select,
5840 &format!("{line_to_select}{SELECTED_MARKER}"),
5841 1,
5842 )
5843 };
5844 cx.executor()
5845 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5846 cx.run_until_parked();
5847
5848 let active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5849 outline_panel
5850 .active_editor()
5851 .expect("should have an active editor open")
5852 });
5853 let initial_outline_selection =
5854 "search: match config.param_names_for_lifetime_elision_hints {";
5855 outline_panel.update_in(cx, |outline_panel, window, cx| {
5856 assert_eq!(
5857 display_entries(
5858 &project,
5859 &snapshot(outline_panel, cx),
5860 &outline_panel.cached_entries,
5861 outline_panel.selected_entry(),
5862 cx,
5863 ),
5864 select_first_in_all_matches(initial_outline_selection)
5865 );
5866 assert_eq!(
5867 selected_row_text(&active_editor, cx),
5868 initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5869 "Should place the initial editor selection on the corresponding search result"
5870 );
5871
5872 outline_panel.select_next(&SelectNext, window, cx);
5873 outline_panel.select_next(&SelectNext, window, cx);
5874 });
5875
5876 let navigated_outline_selection =
5877 "search: Some(it) if config.param_names_for_lifetime_elision_hints => {";
5878 outline_panel.update(cx, |outline_panel, cx| {
5879 assert_eq!(
5880 display_entries(
5881 &project,
5882 &snapshot(outline_panel, cx),
5883 &outline_panel.cached_entries,
5884 outline_panel.selected_entry(),
5885 cx,
5886 ),
5887 select_first_in_all_matches(navigated_outline_selection)
5888 );
5889 });
5890 cx.executor()
5891 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5892 outline_panel.update(cx, |_, cx| {
5893 assert_eq!(
5894 selected_row_text(&active_editor, cx),
5895 navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5896 "Should still have the initial caret position after SelectNext calls"
5897 );
5898 });
5899
5900 outline_panel.update_in(cx, |outline_panel, window, cx| {
5901 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5902 });
5903 outline_panel.update(cx, |_outline_panel, cx| {
5904 assert_eq!(
5905 selected_row_text(&active_editor, cx),
5906 navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5907 "After opening, should move the caret to the opened outline entry's position"
5908 );
5909 });
5910
5911 outline_panel.update_in(cx, |outline_panel, window, cx| {
5912 outline_panel.select_next(&SelectNext, window, cx);
5913 });
5914 let next_navigated_outline_selection = "search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },";
5915 outline_panel.update(cx, |outline_panel, cx| {
5916 assert_eq!(
5917 display_entries(
5918 &project,
5919 &snapshot(outline_panel, cx),
5920 &outline_panel.cached_entries,
5921 outline_panel.selected_entry(),
5922 cx,
5923 ),
5924 select_first_in_all_matches(next_navigated_outline_selection)
5925 );
5926 });
5927 cx.executor()
5928 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5929 outline_panel.update(cx, |_outline_panel, cx| {
5930 assert_eq!(
5931 selected_row_text(&active_editor, cx),
5932 next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5933 "Should again preserve the selection after another SelectNext call"
5934 );
5935 });
5936
5937 outline_panel.update_in(cx, |outline_panel, window, cx| {
5938 outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx);
5939 });
5940 cx.executor()
5941 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5942 cx.run_until_parked();
5943 let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5944 outline_panel
5945 .active_editor()
5946 .expect("should have an active editor open")
5947 });
5948 outline_panel.update(cx, |outline_panel, cx| {
5949 assert_ne!(
5950 active_editor, new_active_editor,
5951 "After opening an excerpt, new editor should be open"
5952 );
5953 assert_eq!(
5954 display_entries(
5955 &project,
5956 &snapshot(outline_panel, cx),
5957 &outline_panel.cached_entries,
5958 outline_panel.selected_entry(),
5959 cx,
5960 ),
5961 "fn_lifetime_fn.rs <==== selected"
5962 );
5963 assert_eq!(
5964 selected_row_text(&new_active_editor, cx),
5965 next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5966 "When opening the excerpt, should navigate to the place corresponding the outline entry"
5967 );
5968 });
5969 }
5970
5971 #[gpui::test]
5972 async fn test_multiple_workrees(cx: &mut TestAppContext) {
5973 init_test(cx);
5974
5975 let fs = FakeFs::new(cx.background_executor.clone());
5976 fs.insert_tree(
5977 "/root",
5978 json!({
5979 "one": {
5980 "a.txt": "aaa aaa"
5981 },
5982 "two": {
5983 "b.txt": "a aaa"
5984 }
5985
5986 }),
5987 )
5988 .await;
5989 let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await;
5990 let workspace = add_outline_panel(&project, cx).await;
5991 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5992 let outline_panel = outline_panel(&workspace, cx);
5993 outline_panel.update_in(cx, |outline_panel, window, cx| {
5994 outline_panel.set_active(true, window, cx)
5995 });
5996
5997 let items = workspace
5998 .update(cx, |workspace, window, cx| {
5999 workspace.open_paths(
6000 vec![PathBuf::from("/root/two")],
6001 OpenOptions {
6002 visible: Some(OpenVisible::OnlyDirectories),
6003 ..Default::default()
6004 },
6005 None,
6006 window,
6007 cx,
6008 )
6009 })
6010 .unwrap()
6011 .await;
6012 assert_eq!(items.len(), 1, "Were opening another worktree directory");
6013 assert!(
6014 items[0].is_none(),
6015 "Directory should be opened successfully"
6016 );
6017
6018 workspace
6019 .update(cx, |workspace, window, cx| {
6020 ProjectSearchView::deploy_search(
6021 workspace,
6022 &workspace::DeploySearch::default(),
6023 window,
6024 cx,
6025 )
6026 })
6027 .unwrap();
6028 let search_view = workspace
6029 .update(cx, |workspace, _, cx| {
6030 workspace
6031 .active_pane()
6032 .read(cx)
6033 .items()
6034 .find_map(|item| item.downcast::<ProjectSearchView>())
6035 .expect("Project search view expected to appear after new search event trigger")
6036 })
6037 .unwrap();
6038
6039 let query = "aaa";
6040 perform_project_search(&search_view, query, cx);
6041 search_view.update(cx, |search_view, cx| {
6042 search_view
6043 .results_editor()
6044 .update(cx, |results_editor, cx| {
6045 assert_eq!(
6046 results_editor.display_text(cx).match_indices(query).count(),
6047 3
6048 );
6049 });
6050 });
6051
6052 cx.executor()
6053 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6054 cx.run_until_parked();
6055 outline_panel.update(cx, |outline_panel, cx| {
6056 assert_eq!(
6057 display_entries(
6058 &project,
6059 &snapshot(outline_panel, cx),
6060 &outline_panel.cached_entries,
6061 outline_panel.selected_entry(),
6062 cx,
6063 ),
6064 r#"/root/one/
6065 a.txt
6066 search: aaa aaa <==== selected
6067 search: aaa aaa
6068/root/two/
6069 b.txt
6070 search: a aaa"#
6071 );
6072 });
6073
6074 outline_panel.update_in(cx, |outline_panel, window, cx| {
6075 outline_panel.select_previous(&SelectPrevious, window, cx);
6076 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
6077 });
6078 cx.executor()
6079 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6080 cx.run_until_parked();
6081 outline_panel.update(cx, |outline_panel, cx| {
6082 assert_eq!(
6083 display_entries(
6084 &project,
6085 &snapshot(outline_panel, cx),
6086 &outline_panel.cached_entries,
6087 outline_panel.selected_entry(),
6088 cx,
6089 ),
6090 r#"/root/one/
6091 a.txt <==== selected
6092/root/two/
6093 b.txt
6094 search: a aaa"#
6095 );
6096 });
6097
6098 outline_panel.update_in(cx, |outline_panel, window, cx| {
6099 outline_panel.select_next(&SelectNext, window, cx);
6100 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
6101 });
6102 cx.executor()
6103 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6104 cx.run_until_parked();
6105 outline_panel.update(cx, |outline_panel, cx| {
6106 assert_eq!(
6107 display_entries(
6108 &project,
6109 &snapshot(outline_panel, cx),
6110 &outline_panel.cached_entries,
6111 outline_panel.selected_entry(),
6112 cx,
6113 ),
6114 r#"/root/one/
6115 a.txt
6116/root/two/ <==== selected"#
6117 );
6118 });
6119
6120 outline_panel.update_in(cx, |outline_panel, window, cx| {
6121 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
6122 });
6123 cx.executor()
6124 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6125 cx.run_until_parked();
6126 outline_panel.update(cx, |outline_panel, cx| {
6127 assert_eq!(
6128 display_entries(
6129 &project,
6130 &snapshot(outline_panel, cx),
6131 &outline_panel.cached_entries,
6132 outline_panel.selected_entry(),
6133 cx,
6134 ),
6135 r#"/root/one/
6136 a.txt
6137/root/two/ <==== selected
6138 b.txt
6139 search: a aaa"#
6140 );
6141 });
6142 }
6143
6144 #[gpui::test]
6145 async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
6146 init_test(cx);
6147
6148 let root = path!("/root");
6149 let fs = FakeFs::new(cx.background_executor.clone());
6150 fs.insert_tree(
6151 root,
6152 json!({
6153 "src": {
6154 "lib.rs": indoc!("
6155#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6156struct OutlineEntryExcerpt {
6157 id: ExcerptId,
6158 buffer_id: BufferId,
6159 range: ExcerptRange<language::Anchor>,
6160}"),
6161 }
6162 }),
6163 )
6164 .await;
6165 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
6166 project.read_with(cx, |project, _| {
6167 project.languages().add(Arc::new(
6168 rust_lang()
6169 .with_outline_query(
6170 r#"
6171 (struct_item
6172 (visibility_modifier)? @context
6173 "struct" @context
6174 name: (_) @name) @item
6175
6176 (field_declaration
6177 (visibility_modifier)? @context
6178 name: (_) @name) @item
6179"#,
6180 )
6181 .unwrap(),
6182 ))
6183 });
6184 let workspace = add_outline_panel(&project, cx).await;
6185 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6186 let outline_panel = outline_panel(&workspace, cx);
6187 cx.update(|window, cx| {
6188 outline_panel.update(cx, |outline_panel, cx| {
6189 outline_panel.set_active(true, window, cx)
6190 });
6191 });
6192
6193 let _editor = workspace
6194 .update(cx, |workspace, window, cx| {
6195 workspace.open_abs_path(
6196 PathBuf::from(path!("/root/src/lib.rs")),
6197 OpenOptions {
6198 visible: Some(OpenVisible::All),
6199 ..Default::default()
6200 },
6201 window,
6202 cx,
6203 )
6204 })
6205 .unwrap()
6206 .await
6207 .expect("Failed to open Rust source file")
6208 .downcast::<Editor>()
6209 .expect("Should open an editor for Rust source file");
6210
6211 cx.executor()
6212 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6213 cx.run_until_parked();
6214 outline_panel.update(cx, |outline_panel, cx| {
6215 assert_eq!(
6216 display_entries(
6217 &project,
6218 &snapshot(outline_panel, cx),
6219 &outline_panel.cached_entries,
6220 outline_panel.selected_entry(),
6221 cx,
6222 ),
6223 indoc!(
6224 "
6225outline: struct OutlineEntryExcerpt
6226 outline: id
6227 outline: buffer_id
6228 outline: range"
6229 )
6230 );
6231 });
6232
6233 cx.update(|window, cx| {
6234 outline_panel.update(cx, |outline_panel, cx| {
6235 outline_panel.select_next(&SelectNext, window, cx);
6236 });
6237 });
6238 cx.executor()
6239 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6240 cx.run_until_parked();
6241 outline_panel.update(cx, |outline_panel, cx| {
6242 assert_eq!(
6243 display_entries(
6244 &project,
6245 &snapshot(outline_panel, cx),
6246 &outline_panel.cached_entries,
6247 outline_panel.selected_entry(),
6248 cx,
6249 ),
6250 indoc!(
6251 "
6252outline: struct OutlineEntryExcerpt <==== selected
6253 outline: id
6254 outline: buffer_id
6255 outline: range"
6256 )
6257 );
6258 });
6259
6260 cx.update(|window, cx| {
6261 outline_panel.update(cx, |outline_panel, cx| {
6262 outline_panel.select_next(&SelectNext, window, cx);
6263 });
6264 });
6265 cx.executor()
6266 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6267 cx.run_until_parked();
6268 outline_panel.update(cx, |outline_panel, cx| {
6269 assert_eq!(
6270 display_entries(
6271 &project,
6272 &snapshot(outline_panel, cx),
6273 &outline_panel.cached_entries,
6274 outline_panel.selected_entry(),
6275 cx,
6276 ),
6277 indoc!(
6278 "
6279outline: struct OutlineEntryExcerpt
6280 outline: id <==== selected
6281 outline: buffer_id
6282 outline: range"
6283 )
6284 );
6285 });
6286
6287 cx.update(|window, cx| {
6288 outline_panel.update(cx, |outline_panel, cx| {
6289 outline_panel.select_next(&SelectNext, window, cx);
6290 });
6291 });
6292 cx.executor()
6293 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6294 cx.run_until_parked();
6295 outline_panel.update(cx, |outline_panel, cx| {
6296 assert_eq!(
6297 display_entries(
6298 &project,
6299 &snapshot(outline_panel, cx),
6300 &outline_panel.cached_entries,
6301 outline_panel.selected_entry(),
6302 cx,
6303 ),
6304 indoc!(
6305 "
6306outline: struct OutlineEntryExcerpt
6307 outline: id
6308 outline: buffer_id <==== selected
6309 outline: range"
6310 )
6311 );
6312 });
6313
6314 cx.update(|window, cx| {
6315 outline_panel.update(cx, |outline_panel, cx| {
6316 outline_panel.select_next(&SelectNext, window, cx);
6317 });
6318 });
6319 cx.executor()
6320 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6321 cx.run_until_parked();
6322 outline_panel.update(cx, |outline_panel, cx| {
6323 assert_eq!(
6324 display_entries(
6325 &project,
6326 &snapshot(outline_panel, cx),
6327 &outline_panel.cached_entries,
6328 outline_panel.selected_entry(),
6329 cx,
6330 ),
6331 indoc!(
6332 "
6333outline: struct OutlineEntryExcerpt
6334 outline: id
6335 outline: buffer_id
6336 outline: range <==== selected"
6337 )
6338 );
6339 });
6340
6341 cx.update(|window, cx| {
6342 outline_panel.update(cx, |outline_panel, cx| {
6343 outline_panel.select_next(&SelectNext, window, cx);
6344 });
6345 });
6346 cx.executor()
6347 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6348 cx.run_until_parked();
6349 outline_panel.update(cx, |outline_panel, cx| {
6350 assert_eq!(
6351 display_entries(
6352 &project,
6353 &snapshot(outline_panel, cx),
6354 &outline_panel.cached_entries,
6355 outline_panel.selected_entry(),
6356 cx,
6357 ),
6358 indoc!(
6359 "
6360outline: struct OutlineEntryExcerpt <==== selected
6361 outline: id
6362 outline: buffer_id
6363 outline: range"
6364 )
6365 );
6366 });
6367
6368 cx.update(|window, cx| {
6369 outline_panel.update(cx, |outline_panel, cx| {
6370 outline_panel.select_previous(&SelectPrevious, window, cx);
6371 });
6372 });
6373 cx.executor()
6374 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6375 cx.run_until_parked();
6376 outline_panel.update(cx, |outline_panel, cx| {
6377 assert_eq!(
6378 display_entries(
6379 &project,
6380 &snapshot(outline_panel, cx),
6381 &outline_panel.cached_entries,
6382 outline_panel.selected_entry(),
6383 cx,
6384 ),
6385 indoc!(
6386 "
6387outline: struct OutlineEntryExcerpt
6388 outline: id
6389 outline: buffer_id
6390 outline: range <==== selected"
6391 )
6392 );
6393 });
6394
6395 cx.update(|window, cx| {
6396 outline_panel.update(cx, |outline_panel, cx| {
6397 outline_panel.select_previous(&SelectPrevious, window, cx);
6398 });
6399 });
6400 cx.executor()
6401 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6402 cx.run_until_parked();
6403 outline_panel.update(cx, |outline_panel, cx| {
6404 assert_eq!(
6405 display_entries(
6406 &project,
6407 &snapshot(outline_panel, cx),
6408 &outline_panel.cached_entries,
6409 outline_panel.selected_entry(),
6410 cx,
6411 ),
6412 indoc!(
6413 "
6414outline: struct OutlineEntryExcerpt
6415 outline: id
6416 outline: buffer_id <==== selected
6417 outline: range"
6418 )
6419 );
6420 });
6421
6422 cx.update(|window, cx| {
6423 outline_panel.update(cx, |outline_panel, cx| {
6424 outline_panel.select_previous(&SelectPrevious, window, cx);
6425 });
6426 });
6427 cx.executor()
6428 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6429 cx.run_until_parked();
6430 outline_panel.update(cx, |outline_panel, cx| {
6431 assert_eq!(
6432 display_entries(
6433 &project,
6434 &snapshot(outline_panel, cx),
6435 &outline_panel.cached_entries,
6436 outline_panel.selected_entry(),
6437 cx,
6438 ),
6439 indoc!(
6440 "
6441outline: struct OutlineEntryExcerpt
6442 outline: id <==== selected
6443 outline: buffer_id
6444 outline: range"
6445 )
6446 );
6447 });
6448
6449 cx.update(|window, cx| {
6450 outline_panel.update(cx, |outline_panel, cx| {
6451 outline_panel.select_previous(&SelectPrevious, window, cx);
6452 });
6453 });
6454 cx.executor()
6455 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6456 cx.run_until_parked();
6457 outline_panel.update(cx, |outline_panel, cx| {
6458 assert_eq!(
6459 display_entries(
6460 &project,
6461 &snapshot(outline_panel, cx),
6462 &outline_panel.cached_entries,
6463 outline_panel.selected_entry(),
6464 cx,
6465 ),
6466 indoc!(
6467 "
6468outline: struct OutlineEntryExcerpt <==== selected
6469 outline: id
6470 outline: buffer_id
6471 outline: range"
6472 )
6473 );
6474 });
6475
6476 cx.update(|window, cx| {
6477 outline_panel.update(cx, |outline_panel, cx| {
6478 outline_panel.select_previous(&SelectPrevious, window, cx);
6479 });
6480 });
6481 cx.executor()
6482 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6483 cx.run_until_parked();
6484 outline_panel.update(cx, |outline_panel, cx| {
6485 assert_eq!(
6486 display_entries(
6487 &project,
6488 &snapshot(outline_panel, cx),
6489 &outline_panel.cached_entries,
6490 outline_panel.selected_entry(),
6491 cx,
6492 ),
6493 indoc!(
6494 "
6495outline: struct OutlineEntryExcerpt
6496 outline: id
6497 outline: buffer_id
6498 outline: range <==== selected"
6499 )
6500 );
6501 });
6502 }
6503
6504 #[gpui::test(iterations = 10)]
6505 async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6506 init_test(cx);
6507
6508 let root = "/frontend-project";
6509 let fs = FakeFs::new(cx.background_executor.clone());
6510 fs.insert_tree(
6511 root,
6512 json!({
6513 "public": {
6514 "lottie": {
6515 "syntax-tree.json": r#"{ "something": "static" }"#
6516 }
6517 },
6518 "src": {
6519 "app": {
6520 "(site)": {
6521 "(about)": {
6522 "jobs": {
6523 "[slug]": {
6524 "page.tsx": r#"static"#
6525 }
6526 }
6527 },
6528 "(blog)": {
6529 "post": {
6530 "[slug]": {
6531 "page.tsx": r#"static"#
6532 }
6533 }
6534 },
6535 }
6536 },
6537 "components": {
6538 "ErrorBoundary.tsx": r#"static"#,
6539 }
6540 }
6541
6542 }),
6543 )
6544 .await;
6545 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
6546 let workspace = add_outline_panel(&project, cx).await;
6547 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6548 let outline_panel = outline_panel(&workspace, cx);
6549 outline_panel.update_in(cx, |outline_panel, window, cx| {
6550 outline_panel.set_active(true, window, cx)
6551 });
6552
6553 workspace
6554 .update(cx, |workspace, window, cx| {
6555 ProjectSearchView::deploy_search(
6556 workspace,
6557 &workspace::DeploySearch::default(),
6558 window,
6559 cx,
6560 )
6561 })
6562 .unwrap();
6563 let search_view = workspace
6564 .update(cx, |workspace, _, cx| {
6565 workspace
6566 .active_pane()
6567 .read(cx)
6568 .items()
6569 .find_map(|item| item.downcast::<ProjectSearchView>())
6570 .expect("Project search view expected to appear after new search event trigger")
6571 })
6572 .unwrap();
6573
6574 let query = "static";
6575 perform_project_search(&search_view, query, cx);
6576 search_view.update(cx, |search_view, cx| {
6577 search_view
6578 .results_editor()
6579 .update(cx, |results_editor, cx| {
6580 assert_eq!(
6581 results_editor.display_text(cx).match_indices(query).count(),
6582 4
6583 );
6584 });
6585 });
6586
6587 cx.executor()
6588 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6589 cx.run_until_parked();
6590 outline_panel.update(cx, |outline_panel, cx| {
6591 assert_eq!(
6592 display_entries(
6593 &project,
6594 &snapshot(outline_panel, cx),
6595 &outline_panel.cached_entries,
6596 outline_panel.selected_entry(),
6597 cx,
6598 ),
6599 r#"/frontend-project/
6600 public/lottie/
6601 syntax-tree.json
6602 search: { "something": "static" } <==== selected
6603 src/
6604 app/(site)/
6605 (about)/jobs/[slug]/
6606 page.tsx
6607 search: static
6608 (blog)/post/[slug]/
6609 page.tsx
6610 search: static
6611 components/
6612 ErrorBoundary.tsx
6613 search: static"#
6614 );
6615 });
6616
6617 outline_panel.update_in(cx, |outline_panel, window, cx| {
6618 // Move to 5th element in the list, 3 items down.
6619 for _ in 0..2 {
6620 outline_panel.select_next(&SelectNext, window, cx);
6621 }
6622 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6623 });
6624 cx.executor()
6625 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6626 cx.run_until_parked();
6627 outline_panel.update(cx, |outline_panel, cx| {
6628 assert_eq!(
6629 display_entries(
6630 &project,
6631 &snapshot(outline_panel, cx),
6632 &outline_panel.cached_entries,
6633 outline_panel.selected_entry(),
6634 cx,
6635 ),
6636 r#"/frontend-project/
6637 public/lottie/
6638 syntax-tree.json
6639 search: { "something": "static" }
6640 src/
6641 app/(site)/ <==== selected
6642 components/
6643 ErrorBoundary.tsx
6644 search: static"#
6645 );
6646 });
6647
6648 outline_panel.update_in(cx, |outline_panel, window, cx| {
6649 // Move to the next visible non-FS entry
6650 for _ in 0..3 {
6651 outline_panel.select_next(&SelectNext, window, cx);
6652 }
6653 });
6654 cx.run_until_parked();
6655 outline_panel.update(cx, |outline_panel, cx| {
6656 assert_eq!(
6657 display_entries(
6658 &project,
6659 &snapshot(outline_panel, cx),
6660 &outline_panel.cached_entries,
6661 outline_panel.selected_entry(),
6662 cx,
6663 ),
6664 r#"/frontend-project/
6665 public/lottie/
6666 syntax-tree.json
6667 search: { "something": "static" }
6668 src/
6669 app/(site)/
6670 components/
6671 ErrorBoundary.tsx
6672 search: static <==== selected"#
6673 );
6674 });
6675
6676 outline_panel.update_in(cx, |outline_panel, window, cx| {
6677 outline_panel
6678 .active_editor()
6679 .expect("Should have an active editor")
6680 .update(cx, |editor, cx| {
6681 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6682 });
6683 });
6684 cx.executor()
6685 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6686 cx.run_until_parked();
6687 outline_panel.update(cx, |outline_panel, cx| {
6688 assert_eq!(
6689 display_entries(
6690 &project,
6691 &snapshot(outline_panel, cx),
6692 &outline_panel.cached_entries,
6693 outline_panel.selected_entry(),
6694 cx,
6695 ),
6696 r#"/frontend-project/
6697 public/lottie/
6698 syntax-tree.json
6699 search: { "something": "static" }
6700 src/
6701 app/(site)/
6702 components/
6703 ErrorBoundary.tsx <==== selected"#
6704 );
6705 });
6706
6707 outline_panel.update_in(cx, |outline_panel, window, cx| {
6708 outline_panel
6709 .active_editor()
6710 .expect("Should have an active editor")
6711 .update(cx, |editor, cx| {
6712 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6713 });
6714 });
6715 cx.executor()
6716 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6717 cx.run_until_parked();
6718 outline_panel.update(cx, |outline_panel, cx| {
6719 assert_eq!(
6720 display_entries(
6721 &project,
6722 &snapshot(outline_panel, cx),
6723 &outline_panel.cached_entries,
6724 outline_panel.selected_entry(),
6725 cx,
6726 ),
6727 r#"/frontend-project/
6728 public/lottie/
6729 syntax-tree.json
6730 search: { "something": "static" }
6731 src/
6732 app/(site)/
6733 components/
6734 ErrorBoundary.tsx <==== selected
6735 search: static"#
6736 );
6737 });
6738 }
6739
6740 async fn add_outline_panel(
6741 project: &Entity<Project>,
6742 cx: &mut TestAppContext,
6743 ) -> WindowHandle<Workspace> {
6744 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6745
6746 let outline_panel = window
6747 .update(cx, |_, window, cx| {
6748 cx.spawn_in(window, async |this, cx| {
6749 OutlinePanel::load(this, cx.clone()).await
6750 })
6751 })
6752 .unwrap()
6753 .await
6754 .expect("Failed to load outline panel");
6755
6756 window
6757 .update(cx, |workspace, window, cx| {
6758 workspace.add_panel(outline_panel, window, cx);
6759 })
6760 .unwrap();
6761 window
6762 }
6763
6764 fn outline_panel(
6765 workspace: &WindowHandle<Workspace>,
6766 cx: &mut TestAppContext,
6767 ) -> Entity<OutlinePanel> {
6768 workspace
6769 .update(cx, |workspace, _, cx| {
6770 workspace
6771 .panel::<OutlinePanel>(cx)
6772 .expect("no outline panel")
6773 })
6774 .unwrap()
6775 }
6776
6777 fn display_entries(
6778 project: &Entity<Project>,
6779 multi_buffer_snapshot: &MultiBufferSnapshot,
6780 cached_entries: &[CachedEntry],
6781 selected_entry: Option<&PanelEntry>,
6782 cx: &mut App,
6783 ) -> String {
6784 let mut display_string = String::new();
6785 for entry in cached_entries {
6786 if !display_string.is_empty() {
6787 display_string += "\n";
6788 }
6789 for _ in 0..entry.depth {
6790 display_string += " ";
6791 }
6792 display_string += &match &entry.entry {
6793 PanelEntry::Fs(entry) => match entry {
6794 FsEntry::ExternalFile(_) => {
6795 panic!("Did not cover external files with tests")
6796 }
6797 FsEntry::Directory(directory) => {
6798 match project
6799 .read(cx)
6800 .worktree_for_id(directory.worktree_id, cx)
6801 .and_then(|worktree| {
6802 if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
6803 Some(worktree.read(cx).abs_path())
6804 } else {
6805 None
6806 }
6807 }) {
6808 Some(root_path) => format!(
6809 "{}/{}",
6810 root_path.display(),
6811 directory.entry.path.display(),
6812 ),
6813 None => format!(
6814 "{}/",
6815 directory
6816 .entry
6817 .path
6818 .file_name()
6819 .unwrap_or_default()
6820 .to_string_lossy()
6821 ),
6822 }
6823 }
6824 FsEntry::File(file) => file
6825 .entry
6826 .path
6827 .file_name()
6828 .map(|name| name.to_string_lossy().to_string())
6829 .unwrap_or_default(),
6830 },
6831 PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6832 .entries
6833 .iter()
6834 .filter_map(|dir| dir.path.file_name())
6835 .map(|name| name.to_string_lossy().to_string() + "/")
6836 .collect(),
6837 PanelEntry::Outline(outline_entry) => match outline_entry {
6838 OutlineEntry::Excerpt(_) => continue,
6839 OutlineEntry::Outline(outline_entry) => {
6840 format!("outline: {}", outline_entry.outline.text)
6841 }
6842 },
6843 PanelEntry::Search(search_entry) => {
6844 format!(
6845 "search: {}",
6846 search_entry
6847 .render_data
6848 .get_or_init(|| SearchData::new(
6849 &search_entry.match_range,
6850 multi_buffer_snapshot
6851 ))
6852 .context_text
6853 )
6854 }
6855 };
6856
6857 if Some(&entry.entry) == selected_entry {
6858 display_string += SELECTED_MARKER;
6859 }
6860 }
6861 display_string
6862 }
6863
6864 fn init_test(cx: &mut TestAppContext) {
6865 cx.update(|cx| {
6866 let settings = SettingsStore::test(cx);
6867 cx.set_global(settings);
6868
6869 theme::init(theme::LoadThemes::JustBase, cx);
6870
6871 language::init(cx);
6872 editor::init(cx);
6873 workspace::init_settings(cx);
6874 Project::init_settings(cx);
6875 project_search::init(cx);
6876 super::init(cx);
6877 });
6878 }
6879
6880 // Based on https://github.com/rust-lang/rust-analyzer/
6881 async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6882 fs.insert_tree(
6883 root,
6884 json!({
6885 "crates": {
6886 "ide": {
6887 "src": {
6888 "inlay_hints": {
6889 "fn_lifetime_fn.rs": r##"
6890 pub(super) fn hints(
6891 acc: &mut Vec<InlayHint>,
6892 config: &InlayHintsConfig,
6893 func: ast::Fn,
6894 ) -> Option<()> {
6895 // ... snip
6896
6897 let mut used_names: FxHashMap<SmolStr, usize> =
6898 match config.param_names_for_lifetime_elision_hints {
6899 true => generic_param_list
6900 .iter()
6901 .flat_map(|gpl| gpl.lifetime_params())
6902 .filter_map(|param| param.lifetime())
6903 .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6904 .collect(),
6905 false => Default::default(),
6906 };
6907 {
6908 let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6909 if self_param.is_some() && potential_lt_refs.next().is_some() {
6910 allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6911 // self can't be used as a lifetime, so no need to check for collisions
6912 "'self".into()
6913 } else {
6914 gen_idx_name()
6915 });
6916 }
6917 potential_lt_refs.for_each(|(name, ..)| {
6918 let name = match name {
6919 Some(it) if config.param_names_for_lifetime_elision_hints => {
6920 if let Some(c) = used_names.get_mut(it.text().as_str()) {
6921 *c += 1;
6922 SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6923 } else {
6924 used_names.insert(it.text().as_str().into(), 0);
6925 SmolStr::from_iter(["\'", it.text().as_str()])
6926 }
6927 }
6928 _ => gen_idx_name(),
6929 };
6930 allocated_lifetimes.push(name);
6931 });
6932 }
6933
6934 // ... snip
6935 }
6936
6937 // ... snip
6938
6939 #[test]
6940 fn hints_lifetimes_named() {
6941 check_with_config(
6942 InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6943 r#"
6944 fn nested_in<'named>(named: & &X< &()>) {}
6945 // ^'named1, 'named2, 'named3, $
6946 //^'named1 ^'named2 ^'named3
6947 "#,
6948 );
6949 }
6950
6951 // ... snip
6952 "##,
6953 },
6954 "inlay_hints.rs": r#"
6955 #[derive(Clone, Debug, PartialEq, Eq)]
6956 pub struct InlayHintsConfig {
6957 // ... snip
6958 pub param_names_for_lifetime_elision_hints: bool,
6959 pub max_length: Option<usize>,
6960 // ... snip
6961 }
6962
6963 impl Config {
6964 pub fn inlay_hints(&self) -> InlayHintsConfig {
6965 InlayHintsConfig {
6966 // ... snip
6967 param_names_for_lifetime_elision_hints: self
6968 .inlayHints_lifetimeElisionHints_useParameterNames()
6969 .to_owned(),
6970 max_length: self.inlayHints_maxLength().to_owned(),
6971 // ... snip
6972 }
6973 }
6974 }
6975 "#,
6976 "static_index.rs": r#"
6977// ... snip
6978 fn add_file(&mut self, file_id: FileId) {
6979 let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6980 let folds = self.analysis.folding_ranges(file_id).unwrap();
6981 let inlay_hints = self
6982 .analysis
6983 .inlay_hints(
6984 &InlayHintsConfig {
6985 // ... snip
6986 closure_style: hir::ClosureStyle::ImplFn,
6987 param_names_for_lifetime_elision_hints: false,
6988 binding_mode_hints: false,
6989 max_length: Some(25),
6990 closure_capture_hints: false,
6991 // ... snip
6992 },
6993 file_id,
6994 None,
6995 )
6996 .unwrap();
6997 // ... snip
6998 }
6999// ... snip
7000 "#
7001 }
7002 },
7003 "rust-analyzer": {
7004 "src": {
7005 "cli": {
7006 "analysis_stats.rs": r#"
7007 // ... snip
7008 for &file_id in &file_ids {
7009 _ = analysis.inlay_hints(
7010 &InlayHintsConfig {
7011 // ... snip
7012 implicit_drop_hints: true,
7013 lifetime_elision_hints: ide::LifetimeElisionHints::Always,
7014 param_names_for_lifetime_elision_hints: true,
7015 hide_named_constructor_hints: false,
7016 hide_closure_initialization_hints: false,
7017 closure_style: hir::ClosureStyle::ImplFn,
7018 max_length: Some(25),
7019 closing_brace_hints_min_lines: Some(20),
7020 fields_to_resolve: InlayFieldsToResolve::empty(),
7021 range_exclusive_hints: true,
7022 },
7023 file_id.into(),
7024 None,
7025 );
7026 }
7027 // ... snip
7028 "#,
7029 },
7030 "config.rs": r#"
7031 config_data! {
7032 /// Configs that only make sense when they are set by a client. As such they can only be defined
7033 /// by setting them using client's settings (e.g `settings.json` on VS Code).
7034 client: struct ClientDefaultConfigData <- ClientConfigInput -> {
7035 // ... snip
7036 /// Maximum length for inlay hints. Set to null to have an unlimited length.
7037 inlayHints_maxLength: Option<usize> = Some(25),
7038 // ... snip
7039 /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
7040 inlayHints_lifetimeElisionHints_useParameterNames: bool = false,
7041 // ... snip
7042 }
7043 }
7044
7045 impl Config {
7046 // ... snip
7047 pub fn inlay_hints(&self) -> InlayHintsConfig {
7048 InlayHintsConfig {
7049 // ... snip
7050 param_names_for_lifetime_elision_hints: self
7051 .inlayHints_lifetimeElisionHints_useParameterNames()
7052 .to_owned(),
7053 max_length: self.inlayHints_maxLength().to_owned(),
7054 // ... snip
7055 }
7056 }
7057 // ... snip
7058 }
7059 "#
7060 }
7061 }
7062 }
7063 }),
7064 )
7065 .await;
7066 }
7067
7068 fn rust_lang() -> Language {
7069 Language::new(
7070 LanguageConfig {
7071 name: "Rust".into(),
7072 matcher: LanguageMatcher {
7073 path_suffixes: vec!["rs".to_string()],
7074 ..Default::default()
7075 },
7076 ..Default::default()
7077 },
7078 Some(tree_sitter_rust::LANGUAGE.into()),
7079 )
7080 .with_highlights_query(
7081 r#"
7082 (field_identifier) @field
7083 (struct_expression) @struct
7084 "#,
7085 )
7086 .unwrap()
7087 .with_injection_query(
7088 r#"
7089 (macro_invocation
7090 (token_tree) @injection.content
7091 (#set! injection.language "rust"))
7092 "#,
7093 )
7094 .unwrap()
7095 }
7096
7097 fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
7098 outline_panel
7099 .active_editor()
7100 .unwrap()
7101 .read(cx)
7102 .buffer()
7103 .read(cx)
7104 .snapshot(cx)
7105 }
7106
7107 fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
7108 editor.update(cx, |editor, cx| {
7109 let selections = editor.selections.all::<language::Point>(cx);
7110 assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
7111 let selection = selections.first().unwrap();
7112 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
7113 let line_start = language::Point::new(selection.start.row, 0);
7114 let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
7115 multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
7116 })
7117 }
7118
7119 #[gpui::test]
7120 async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) {
7121 init_test(cx);
7122
7123 let fs = FakeFs::new(cx.background_executor.clone());
7124 fs.insert_tree(
7125 "/test",
7126 json!({
7127 "src": {
7128 "lib.rs": indoc!("
7129 mod outer {
7130 pub struct OuterStruct {
7131 field: String,
7132 }
7133 impl OuterStruct {
7134 pub fn new() -> Self {
7135 Self { field: String::new() }
7136 }
7137 pub fn method(&self) {
7138 println!(\"{}\", self.field);
7139 }
7140 }
7141 mod inner {
7142 pub fn inner_function() {
7143 let x = 42;
7144 println!(\"{}\", x);
7145 }
7146 pub struct InnerStruct {
7147 value: i32,
7148 }
7149 }
7150 }
7151 fn main() {
7152 let s = outer::OuterStruct::new();
7153 s.method();
7154 }
7155 "),
7156 }
7157 }),
7158 )
7159 .await;
7160
7161 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7162 project.read_with(cx, |project, _| {
7163 project.languages().add(Arc::new(
7164 rust_lang()
7165 .with_outline_query(
7166 r#"
7167 (struct_item
7168 (visibility_modifier)? @context
7169 "struct" @context
7170 name: (_) @name) @item
7171 (impl_item
7172 "impl" @context
7173 trait: (_)? @context
7174 "for"? @context
7175 type: (_) @context
7176 body: (_)) @item
7177 (function_item
7178 (visibility_modifier)? @context
7179 "fn" @context
7180 name: (_) @name
7181 parameters: (_) @context) @item
7182 (mod_item
7183 (visibility_modifier)? @context
7184 "mod" @context
7185 name: (_) @name) @item
7186 (enum_item
7187 (visibility_modifier)? @context
7188 "enum" @context
7189 name: (_) @name) @item
7190 (field_declaration
7191 (visibility_modifier)? @context
7192 name: (_) @name
7193 ":" @context
7194 type: (_) @context) @item
7195 "#,
7196 )
7197 .unwrap(),
7198 ))
7199 });
7200 let workspace = add_outline_panel(&project, cx).await;
7201 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7202 let outline_panel = outline_panel(&workspace, cx);
7203
7204 outline_panel.update_in(cx, |outline_panel, window, cx| {
7205 outline_panel.set_active(true, window, cx)
7206 });
7207
7208 workspace
7209 .update(cx, |workspace, window, cx| {
7210 workspace.open_abs_path(
7211 PathBuf::from("/test/src/lib.rs"),
7212 OpenOptions {
7213 visible: Some(OpenVisible::All),
7214 ..Default::default()
7215 },
7216 window,
7217 cx,
7218 )
7219 })
7220 .unwrap()
7221 .await
7222 .unwrap();
7223
7224 cx.executor()
7225 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7226 cx.run_until_parked();
7227
7228 // Force another update cycle to ensure outlines are fetched
7229 outline_panel.update_in(cx, |panel, window, cx| {
7230 panel.update_non_fs_items(window, cx);
7231 panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7232 });
7233 cx.executor()
7234 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7235 cx.run_until_parked();
7236
7237 outline_panel.update(cx, |outline_panel, cx| {
7238 assert_eq!(
7239 display_entries(
7240 &project,
7241 &snapshot(outline_panel, cx),
7242 &outline_panel.cached_entries,
7243 outline_panel.selected_entry(),
7244 cx,
7245 ),
7246 indoc!(
7247 "
7248outline: mod outer <==== selected
7249 outline: pub struct OuterStruct
7250 outline: field: String
7251 outline: impl OuterStruct
7252 outline: pub fn new()
7253 outline: pub fn method(&self)
7254 outline: mod inner
7255 outline: pub fn inner_function()
7256 outline: pub struct InnerStruct
7257 outline: value: i32
7258outline: fn main()"
7259 )
7260 );
7261 });
7262
7263 let parent_outline = outline_panel
7264 .read_with(cx, |panel, _cx| {
7265 panel
7266 .cached_entries
7267 .iter()
7268 .find_map(|entry| match &entry.entry {
7269 PanelEntry::Outline(OutlineEntry::Outline(outline))
7270 if panel
7271 .outline_children_cache
7272 .get(&outline.buffer_id)
7273 .and_then(|children_map| {
7274 let key =
7275 (outline.outline.range.clone(), outline.outline.depth);
7276 children_map.get(&key)
7277 })
7278 .copied()
7279 .unwrap_or(false) =>
7280 {
7281 Some(entry.entry.clone())
7282 }
7283 _ => None,
7284 })
7285 })
7286 .expect("Should find an outline with children");
7287
7288 outline_panel.update_in(cx, |panel, window, cx| {
7289 panel.select_entry(parent_outline.clone(), true, window, cx);
7290 panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7291 });
7292 cx.executor()
7293 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7294 cx.run_until_parked();
7295
7296 outline_panel.update(cx, |outline_panel, cx| {
7297 assert_eq!(
7298 display_entries(
7299 &project,
7300 &snapshot(outline_panel, cx),
7301 &outline_panel.cached_entries,
7302 outline_panel.selected_entry(),
7303 cx,
7304 ),
7305 indoc!(
7306 "
7307outline: mod outer <==== selected
7308outline: fn main()"
7309 )
7310 );
7311 });
7312
7313 outline_panel.update_in(cx, |panel, window, cx| {
7314 panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7315 });
7316 cx.executor()
7317 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7318 cx.run_until_parked();
7319
7320 outline_panel.update(cx, |outline_panel, cx| {
7321 assert_eq!(
7322 display_entries(
7323 &project,
7324 &snapshot(outline_panel, cx),
7325 &outline_panel.cached_entries,
7326 outline_panel.selected_entry(),
7327 cx,
7328 ),
7329 indoc!(
7330 "
7331outline: mod outer <==== selected
7332 outline: pub struct OuterStruct
7333 outline: field: String
7334 outline: impl OuterStruct
7335 outline: pub fn new()
7336 outline: pub fn method(&self)
7337 outline: mod inner
7338 outline: pub fn inner_function()
7339 outline: pub struct InnerStruct
7340 outline: value: i32
7341outline: fn main()"
7342 )
7343 );
7344 });
7345
7346 outline_panel.update_in(cx, |panel, window, cx| {
7347 panel.collapsed_entries.clear();
7348 panel.update_cached_entries(None, window, cx);
7349 });
7350 cx.executor()
7351 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7352 cx.run_until_parked();
7353
7354 outline_panel.update_in(cx, |panel, window, cx| {
7355 let outlines_with_children: Vec<_> = panel
7356 .cached_entries
7357 .iter()
7358 .filter_map(|entry| match &entry.entry {
7359 PanelEntry::Outline(OutlineEntry::Outline(outline))
7360 if panel
7361 .outline_children_cache
7362 .get(&outline.buffer_id)
7363 .and_then(|children_map| {
7364 let key = (outline.outline.range.clone(), outline.outline.depth);
7365 children_map.get(&key)
7366 })
7367 .copied()
7368 .unwrap_or(false) =>
7369 {
7370 Some(entry.entry.clone())
7371 }
7372 _ => None,
7373 })
7374 .collect();
7375
7376 for outline in outlines_with_children {
7377 panel.select_entry(outline, false, window, cx);
7378 panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7379 }
7380 });
7381 cx.executor()
7382 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7383 cx.run_until_parked();
7384
7385 outline_panel.update(cx, |outline_panel, cx| {
7386 assert_eq!(
7387 display_entries(
7388 &project,
7389 &snapshot(outline_panel, cx),
7390 &outline_panel.cached_entries,
7391 outline_panel.selected_entry(),
7392 cx,
7393 ),
7394 indoc!(
7395 "
7396outline: mod outer
7397outline: fn main()"
7398 )
7399 );
7400 });
7401
7402 let collapsed_entries_count =
7403 outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len());
7404 assert!(
7405 collapsed_entries_count > 0,
7406 "Should have collapsed entries tracked"
7407 );
7408 }
7409
7410 #[gpui::test]
7411 async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) {
7412 init_test(cx);
7413
7414 let fs = FakeFs::new(cx.background_executor.clone());
7415 fs.insert_tree(
7416 "/test",
7417 json!({
7418 "src": {
7419 "main.rs": indoc!("
7420 struct Config {
7421 name: String,
7422 value: i32,
7423 }
7424 impl Config {
7425 fn new(name: String) -> Self {
7426 Self { name, value: 0 }
7427 }
7428 fn get_value(&self) -> i32 {
7429 self.value
7430 }
7431 }
7432 enum Status {
7433 Active,
7434 Inactive,
7435 }
7436 fn process_config(config: Config) -> Status {
7437 if config.get_value() > 0 {
7438 Status::Active
7439 } else {
7440 Status::Inactive
7441 }
7442 }
7443 fn main() {
7444 let config = Config::new(\"test\".to_string());
7445 let status = process_config(config);
7446 }
7447 "),
7448 }
7449 }),
7450 )
7451 .await;
7452
7453 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7454 project.read_with(cx, |project, _| {
7455 project.languages().add(Arc::new(
7456 rust_lang()
7457 .with_outline_query(
7458 r#"
7459 (struct_item
7460 (visibility_modifier)? @context
7461 "struct" @context
7462 name: (_) @name) @item
7463 (impl_item
7464 "impl" @context
7465 trait: (_)? @context
7466 "for"? @context
7467 type: (_) @context
7468 body: (_)) @item
7469 (function_item
7470 (visibility_modifier)? @context
7471 "fn" @context
7472 name: (_) @name
7473 parameters: (_) @context) @item
7474 (mod_item
7475 (visibility_modifier)? @context
7476 "mod" @context
7477 name: (_) @name) @item
7478 (enum_item
7479 (visibility_modifier)? @context
7480 "enum" @context
7481 name: (_) @name) @item
7482 (field_declaration
7483 (visibility_modifier)? @context
7484 name: (_) @name
7485 ":" @context
7486 type: (_) @context) @item
7487 "#,
7488 )
7489 .unwrap(),
7490 ))
7491 });
7492
7493 let workspace = add_outline_panel(&project, cx).await;
7494 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7495 let outline_panel = outline_panel(&workspace, cx);
7496
7497 outline_panel.update_in(cx, |outline_panel, window, cx| {
7498 outline_panel.set_active(true, window, cx)
7499 });
7500
7501 let _editor = workspace
7502 .update(cx, |workspace, window, cx| {
7503 workspace.open_abs_path(
7504 PathBuf::from("/test/src/main.rs"),
7505 OpenOptions {
7506 visible: Some(OpenVisible::All),
7507 ..Default::default()
7508 },
7509 window,
7510 cx,
7511 )
7512 })
7513 .unwrap()
7514 .await
7515 .unwrap();
7516
7517 cx.executor()
7518 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7519 cx.run_until_parked();
7520
7521 outline_panel.update(cx, |outline_panel, _cx| {
7522 outline_panel.selected_entry = SelectedEntry::None;
7523 });
7524
7525 // Check initial state - all entries should be expanded by default
7526 outline_panel.update(cx, |outline_panel, cx| {
7527 assert_eq!(
7528 display_entries(
7529 &project,
7530 &snapshot(outline_panel, cx),
7531 &outline_panel.cached_entries,
7532 outline_panel.selected_entry(),
7533 cx,
7534 ),
7535 indoc!(
7536 "
7537outline: struct Config
7538 outline: name: String
7539 outline: value: i32
7540outline: impl Config
7541 outline: fn new(name: String)
7542 outline: fn get_value(&self)
7543outline: enum Status
7544outline: fn process_config(config: Config)
7545outline: fn main()"
7546 )
7547 );
7548 });
7549
7550 outline_panel.update(cx, |outline_panel, _cx| {
7551 outline_panel.selected_entry = SelectedEntry::None;
7552 });
7553
7554 cx.update(|window, cx| {
7555 outline_panel.update(cx, |outline_panel, cx| {
7556 outline_panel.select_first(&SelectFirst, window, cx);
7557 });
7558 });
7559
7560 cx.executor()
7561 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7562 cx.run_until_parked();
7563
7564 outline_panel.update(cx, |outline_panel, cx| {
7565 assert_eq!(
7566 display_entries(
7567 &project,
7568 &snapshot(outline_panel, cx),
7569 &outline_panel.cached_entries,
7570 outline_panel.selected_entry(),
7571 cx,
7572 ),
7573 indoc!(
7574 "
7575outline: struct Config <==== selected
7576 outline: name: String
7577 outline: value: i32
7578outline: impl Config
7579 outline: fn new(name: String)
7580 outline: fn get_value(&self)
7581outline: enum Status
7582outline: fn process_config(config: Config)
7583outline: fn main()"
7584 )
7585 );
7586 });
7587
7588 cx.update(|window, cx| {
7589 outline_panel.update(cx, |outline_panel, cx| {
7590 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
7591 });
7592 });
7593
7594 cx.executor()
7595 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7596 cx.run_until_parked();
7597
7598 outline_panel.update(cx, |outline_panel, cx| {
7599 assert_eq!(
7600 display_entries(
7601 &project,
7602 &snapshot(outline_panel, cx),
7603 &outline_panel.cached_entries,
7604 outline_panel.selected_entry(),
7605 cx,
7606 ),
7607 indoc!(
7608 "
7609outline: struct Config <==== selected
7610outline: impl Config
7611 outline: fn new(name: String)
7612 outline: fn get_value(&self)
7613outline: enum Status
7614outline: fn process_config(config: Config)
7615outline: fn main()"
7616 )
7617 );
7618 });
7619
7620 cx.update(|window, cx| {
7621 outline_panel.update(cx, |outline_panel, cx| {
7622 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
7623 });
7624 });
7625
7626 cx.executor()
7627 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7628 cx.run_until_parked();
7629
7630 outline_panel.update(cx, |outline_panel, cx| {
7631 assert_eq!(
7632 display_entries(
7633 &project,
7634 &snapshot(outline_panel, cx),
7635 &outline_panel.cached_entries,
7636 outline_panel.selected_entry(),
7637 cx,
7638 ),
7639 indoc!(
7640 "
7641outline: struct Config <==== selected
7642 outline: name: String
7643 outline: value: i32
7644outline: impl Config
7645 outline: fn new(name: String)
7646 outline: fn get_value(&self)
7647outline: enum Status
7648outline: fn process_config(config: Config)
7649outline: fn main()"
7650 )
7651 );
7652 });
7653 }
7654}