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