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