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