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