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