1mod outline_panel_settings;
2
3use std::{
4 cmp,
5 collections::BTreeMap,
6 hash::Hash,
7 ops::Range,
8 path::{Path, PathBuf, MAIN_SEPARATOR_STR},
9 sync::{
10 atomic::{self, AtomicBool},
11 Arc, OnceLock,
12 },
13 time::Duration,
14 u32,
15};
16
17use anyhow::Context as _;
18use collections::{hash_map, BTreeSet, HashMap, HashSet};
19use db::kvp::KEY_VALUE_STORE;
20use editor::{
21 display_map::ToDisplayPoint,
22 items::{entry_git_aware_label_color, entry_label_color},
23 scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor, ScrollbarAutoHide},
24 AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorMode, EditorSettings, ExcerptId,
25 ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, ShowScrollbar,
26};
27use file_icons::FileIcons;
28use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
29use gpui::{
30 actions, anchored, deferred, div, point, px, size, uniform_list, Action, AnyElement, App,
31 AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context, DismissEvent, Div,
32 ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, InteractiveElement,
33 IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton,
34 MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy, SharedString, Stateful,
35 StatefulInteractiveElement as _, Styled, Subscription, Task, UniformListScrollHandle,
36 WeakEntity, Window,
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::{debug_panic, RangeExt, ResultExt, TryFutureExt};
51use workspace::{
52 dock::{DockPosition, Panel, PanelEvent},
53 item::ItemHandle,
54 searchable::{SearchEvent, SearchableItem},
55 ui::{
56 h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder,
57 HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label,
58 LabelCommon, ListItem, Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable,
59 Tooltip,
60 },
61 OpenInTerminal, WeakItemHandle, Workspace,
62};
63use worktree::{Entry, ProjectEntryId, WorktreeId};
64
65actions!(
66 outline_panel,
67 [
68 CollapseAllEntries,
69 CollapseSelectedEntry,
70 ExpandAllEntries,
71 ExpandSelectedEntry,
72 FoldDirectory,
73 Open,
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_subscrpiption: 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 = cx
684 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(OUTLINE_PANEL_KEY) })
685 .await
686 .context("loading outline panel")
687 .log_err()
688 .flatten()
689 .map(|panel| serde_json::from_str::<SerializedOutlinePanel>(&panel))
690 .transpose()
691 .log_err()
692 .flatten();
693
694 workspace.update_in(&mut cx, |workspace, window, cx| {
695 let panel = Self::new(workspace, window, cx);
696 if let Some(serialized_panel) = serialized_panel {
697 panel.update(cx, |panel, cx| {
698 panel.width = serialized_panel.width.map(|px| px.round());
699 panel.active = serialized_panel.active.unwrap_or(false);
700 cx.notify();
701 });
702 }
703 panel
704 })
705 }
706
707 fn new(
708 workspace: &mut Workspace,
709 window: &mut Window,
710 cx: &mut Context<Workspace>,
711 ) -> Entity<Self> {
712 let project = workspace.project().clone();
713 let workspace_handle = cx.entity().downgrade();
714 let outline_panel = cx.new(|cx| {
715 let filter_editor = cx.new(|cx| {
716 let mut editor = Editor::single_line(window, cx);
717 editor.set_placeholder_text("Filter...", cx);
718 editor
719 });
720 let filter_update_subscription = cx.subscribe_in(
721 &filter_editor,
722 window,
723 |outline_panel: &mut Self, _, event, window, cx| {
724 if let editor::EditorEvent::BufferEdited = event {
725 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
726 }
727 },
728 );
729
730 let focus_handle = cx.focus_handle();
731 let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in);
732 let focus_out_subscription =
733 cx.on_focus_out(&focus_handle, window, |outline_panel, _, window, cx| {
734 outline_panel.hide_scrollbar(window, cx);
735 });
736 let workspace_subscription = cx.subscribe_in(
737 &workspace
738 .weak_handle()
739 .upgrade()
740 .expect("have a &mut Workspace"),
741 window,
742 move |outline_panel, workspace, event, window, cx| {
743 if let workspace::Event::ActiveItemChanged = event {
744 if let Some((new_active_item, new_active_editor)) =
745 workspace_active_editor(workspace.read(cx), cx)
746 {
747 if outline_panel.should_replace_active_item(new_active_item.as_ref()) {
748 outline_panel.replace_active_editor(
749 new_active_item,
750 new_active_editor,
751 window,
752 cx,
753 );
754 }
755 } else {
756 outline_panel.clear_previous(window, cx);
757 cx.notify();
758 }
759 }
760 },
761 );
762
763 let icons_subscription = cx.observe_global::<FileIcons>(|_, cx| {
764 cx.notify();
765 });
766
767 let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx);
768 let mut current_theme = ThemeSettings::get_global(cx).clone();
769 let settings_subscription =
770 cx.observe_global_in::<SettingsStore>(window, move |outline_panel, window, cx| {
771 let new_settings = OutlinePanelSettings::get_global(cx);
772 let new_theme = ThemeSettings::get_global(cx);
773 if ¤t_theme != new_theme {
774 outline_panel_settings = *new_settings;
775 current_theme = new_theme.clone();
776 for excerpts in outline_panel.excerpts.values_mut() {
777 for excerpt in excerpts.values_mut() {
778 excerpt.invalidate_outlines();
779 }
780 }
781 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
782 if update_cached_items {
783 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
784 }
785 } else if &outline_panel_settings != new_settings {
786 outline_panel_settings = *new_settings;
787 cx.notify();
788 }
789 });
790
791 let scroll_handle = UniformListScrollHandle::new();
792
793 let mut outline_panel = Self {
794 mode: ItemsDisplayMode::Outline,
795 active: false,
796 pinned: false,
797 workspace: workspace_handle,
798 project,
799 fs: workspace.app_state().fs.clone(),
800 show_scrollbar: !Self::should_autohide_scrollbar(cx),
801 hide_scrollbar_task: None,
802 vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
803 .parent_entity(&cx.entity()),
804 horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
805 .parent_entity(&cx.entity()),
806 max_width_item_index: None,
807 scroll_handle,
808 focus_handle,
809 filter_editor,
810 fs_entries: Vec::new(),
811 fs_entries_depth: HashMap::default(),
812 fs_children_count: HashMap::default(),
813 collapsed_entries: HashSet::default(),
814 unfolded_dirs: HashMap::default(),
815 selected_entry: SelectedEntry::None,
816 context_menu: None,
817 width: None,
818 active_item: None,
819 pending_serialization: Task::ready(None),
820 updating_fs_entries: false,
821 updating_cached_entries: false,
822 new_entries_for_fs_update: HashSet::default(),
823 preserve_selection_on_buffer_fold_toggles: HashSet::default(),
824 fs_entries_update_task: Task::ready(()),
825 cached_entries_update_task: Task::ready(()),
826 reveal_selection_task: Task::ready(Ok(())),
827 outline_fetch_tasks: HashMap::default(),
828 excerpts: HashMap::default(),
829 cached_entries: Vec::new(),
830 _subscriptions: vec![
831 settings_subscription,
832 icons_subscription,
833 focus_subscription,
834 focus_out_subscription,
835 workspace_subscription,
836 filter_update_subscription,
837 ],
838 };
839 if let Some((item, editor)) = workspace_active_editor(workspace, cx) {
840 outline_panel.replace_active_editor(item, editor, window, cx);
841 }
842 outline_panel
843 });
844
845 outline_panel
846 }
847
848 fn serialize(&mut self, cx: &mut Context<Self>) {
849 let width = self.width;
850 let active = Some(self.active);
851 self.pending_serialization = cx.background_spawn(
852 async move {
853 KEY_VALUE_STORE
854 .write_kvp(
855 OUTLINE_PANEL_KEY.into(),
856 serde_json::to_string(&SerializedOutlinePanel { width, active })?,
857 )
858 .await?;
859 anyhow::Ok(())
860 }
861 .log_err(),
862 );
863 }
864
865 fn dispatch_context(&self, window: &mut Window, cx: &mut Context<Self>) -> KeyContext {
866 let mut dispatch_context = KeyContext::new_with_defaults();
867 dispatch_context.add("OutlinePanel");
868 dispatch_context.add("menu");
869 let identifier = if self.filter_editor.focus_handle(cx).is_focused(window) {
870 "editing"
871 } else {
872 "not_editing"
873 };
874 dispatch_context.add(identifier);
875 dispatch_context
876 }
877
878 fn unfold_directory(
879 &mut self,
880 _: &UnfoldDirectory,
881 window: &mut Window,
882 cx: &mut Context<Self>,
883 ) {
884 if let Some(PanelEntry::FoldedDirs(FoldedDirsEntry {
885 worktree_id,
886 entries,
887 ..
888 })) = self.selected_entry().cloned()
889 {
890 self.unfolded_dirs
891 .entry(worktree_id)
892 .or_default()
893 .extend(entries.iter().map(|entry| entry.id));
894 self.update_cached_entries(None, window, cx);
895 }
896 }
897
898 fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
899 let (worktree_id, entry) = match self.selected_entry().cloned() {
900 Some(PanelEntry::Fs(FsEntry::Directory(directory))) => {
901 (directory.worktree_id, Some(directory.entry))
902 }
903 Some(PanelEntry::FoldedDirs(folded_dirs)) => {
904 (folded_dirs.worktree_id, folded_dirs.entries.last().cloned())
905 }
906 _ => return,
907 };
908 let Some(entry) = entry else {
909 return;
910 };
911 let unfolded_dirs = self.unfolded_dirs.get_mut(&worktree_id);
912 let worktree = self
913 .project
914 .read(cx)
915 .worktree_for_id(worktree_id, cx)
916 .map(|w| w.read(cx).snapshot());
917 let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
918 return;
919 };
920
921 unfolded_dirs.remove(&entry.id);
922 self.update_cached_entries(None, window, cx);
923 }
924
925 fn open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
926 if self.filter_editor.focus_handle(cx).is_focused(window) {
927 cx.propagate()
928 } else if let Some(selected_entry) = self.selected_entry().cloned() {
929 self.toggle_expanded(&selected_entry, window, cx);
930 self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
931 }
932 }
933
934 fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
935 if self.filter_editor.focus_handle(cx).is_focused(window) {
936 self.focus_handle.focus(window);
937 } else {
938 self.filter_editor.focus_handle(cx).focus(window);
939 }
940
941 if self.context_menu.is_some() {
942 self.context_menu.take();
943 cx.notify();
944 }
945 }
946
947 fn open_excerpts(
948 &mut self,
949 action: &editor::OpenExcerpts,
950 window: &mut Window,
951 cx: &mut Context<Self>,
952 ) {
953 if self.filter_editor.focus_handle(cx).is_focused(window) {
954 cx.propagate()
955 } else if let Some((active_editor, selected_entry)) =
956 self.active_editor().zip(self.selected_entry().cloned())
957 {
958 self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
959 active_editor.update(cx, |editor, cx| editor.open_excerpts(action, window, cx));
960 }
961 }
962
963 fn open_excerpts_split(
964 &mut self,
965 action: &editor::OpenExcerptsSplit,
966 window: &mut Window,
967 cx: &mut Context<Self>,
968 ) {
969 if self.filter_editor.focus_handle(cx).is_focused(window) {
970 cx.propagate()
971 } else if let Some((active_editor, selected_entry)) =
972 self.active_editor().zip(self.selected_entry().cloned())
973 {
974 self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
975 active_editor.update(cx, |editor, cx| {
976 editor.open_excerpts_in_split(action, window, cx)
977 });
978 }
979 }
980
981 fn scroll_editor_to_entry(
982 &mut self,
983 entry: &PanelEntry,
984 prefer_selection_change: bool,
985 prefer_focus_change: bool,
986 window: &mut Window,
987 cx: &mut Context<OutlinePanel>,
988 ) {
989 let Some(active_editor) = self.active_editor() else {
990 return;
991 };
992 let active_multi_buffer = active_editor.read(cx).buffer().clone();
993 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
994 let mut change_selection = prefer_selection_change;
995 let mut change_focus = prefer_focus_change;
996 let mut scroll_to_buffer = None;
997 let scroll_target = match entry {
998 PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => {
999 change_focus = false;
1000 None
1001 }
1002 PanelEntry::Fs(FsEntry::ExternalFile(file)) => {
1003 change_selection = false;
1004 scroll_to_buffer = Some(file.buffer_id);
1005 multi_buffer_snapshot.excerpts().find_map(
1006 |(excerpt_id, buffer_snapshot, excerpt_range)| {
1007 if buffer_snapshot.remote_id() == file.buffer_id {
1008 multi_buffer_snapshot
1009 .anchor_in_excerpt(excerpt_id, excerpt_range.context.start)
1010 } else {
1011 None
1012 }
1013 },
1014 )
1015 }
1016
1017 PanelEntry::Fs(FsEntry::File(file)) => {
1018 change_selection = false;
1019 scroll_to_buffer = Some(file.buffer_id);
1020 self.project
1021 .update(cx, |project, cx| {
1022 project
1023 .path_for_entry(file.entry.id, cx)
1024 .and_then(|path| project.get_open_buffer(&path, cx))
1025 })
1026 .map(|buffer| {
1027 active_multi_buffer
1028 .read(cx)
1029 .excerpts_for_buffer(buffer.read(cx).remote_id(), cx)
1030 })
1031 .and_then(|excerpts| {
1032 let (excerpt_id, excerpt_range) = excerpts.first()?;
1033 multi_buffer_snapshot
1034 .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
1035 })
1036 }
1037 PanelEntry::Outline(OutlineEntry::Outline(outline)) => multi_buffer_snapshot
1038 .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.start)
1039 .or_else(|| {
1040 multi_buffer_snapshot
1041 .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.end)
1042 }),
1043 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1044 change_selection = false;
1045 change_focus = false;
1046 multi_buffer_snapshot.anchor_in_excerpt(excerpt.id, excerpt.range.context.start)
1047 }
1048 PanelEntry::Search(search_entry) => Some(search_entry.match_range.start),
1049 };
1050
1051 if let Some(anchor) = scroll_target {
1052 let activate = self
1053 .workspace
1054 .update(cx, |workspace, cx| match self.active_item() {
1055 Some(active_item) => workspace.activate_item(
1056 active_item.as_ref(),
1057 true,
1058 change_focus,
1059 window,
1060 cx,
1061 ),
1062 None => workspace.activate_item(&active_editor, true, change_focus, window, cx),
1063 });
1064
1065 if activate.is_ok() {
1066 self.select_entry(entry.clone(), true, window, cx);
1067 if change_selection {
1068 active_editor.update(cx, |editor, cx| {
1069 editor.change_selections(
1070 Some(Autoscroll::Strategy(AutoscrollStrategy::Center, None)),
1071 window,
1072 cx,
1073 |s| s.select_ranges(Some(anchor..anchor)),
1074 );
1075 });
1076 } else {
1077 let mut offset = Point::default();
1078 let expand_excerpt_control_height = 1.0;
1079 if let Some(buffer_id) = scroll_to_buffer {
1080 let current_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
1081 if current_folded {
1082 let previous_buffer_id = self
1083 .fs_entries
1084 .iter()
1085 .rev()
1086 .filter_map(|entry| match entry {
1087 FsEntry::File(file) => Some(file.buffer_id),
1088 FsEntry::ExternalFile(external_file) => {
1089 Some(external_file.buffer_id)
1090 }
1091 FsEntry::Directory(..) => None,
1092 })
1093 .skip_while(|id| *id != buffer_id)
1094 .nth(1);
1095 if let Some(previous_buffer_id) = previous_buffer_id {
1096 if !active_editor
1097 .read(cx)
1098 .is_buffer_folded(previous_buffer_id, cx)
1099 {
1100 offset.y += expand_excerpt_control_height;
1101 }
1102 }
1103 } else {
1104 if multi_buffer_snapshot.as_singleton().is_none() {
1105 offset.y = -(active_editor.read(cx).file_header_size() as f32);
1106 }
1107 offset.y -= expand_excerpt_control_height;
1108 }
1109 }
1110 active_editor.update(cx, |editor, cx| {
1111 editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, window, cx);
1112 });
1113 }
1114
1115 if change_focus {
1116 active_editor.focus_handle(cx).focus(window);
1117 } else {
1118 self.focus_handle.focus(window);
1119 }
1120 }
1121 }
1122 }
1123
1124 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1125 if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1126 self.cached_entries
1127 .iter()
1128 .map(|cached_entry| &cached_entry.entry)
1129 .skip_while(|entry| entry != &selected_entry)
1130 .nth(1)
1131 .cloned()
1132 }) {
1133 self.select_entry(entry_to_select, true, window, cx);
1134 } else {
1135 self.select_first(&SelectFirst {}, window, cx)
1136 }
1137 if let Some(selected_entry) = self.selected_entry().cloned() {
1138 self.scroll_editor_to_entry(&selected_entry, true, false, window, cx);
1139 }
1140 }
1141
1142 fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
1143 if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1144 self.cached_entries
1145 .iter()
1146 .rev()
1147 .map(|cached_entry| &cached_entry.entry)
1148 .skip_while(|entry| entry != &selected_entry)
1149 .nth(1)
1150 .cloned()
1151 }) {
1152 self.select_entry(entry_to_select, true, window, cx);
1153 } else {
1154 self.select_last(&SelectLast, window, cx)
1155 }
1156 if let Some(selected_entry) = self.selected_entry().cloned() {
1157 self.scroll_editor_to_entry(&selected_entry, true, false, window, cx);
1158 }
1159 }
1160
1161 fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
1162 if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1163 let mut previous_entries = self
1164 .cached_entries
1165 .iter()
1166 .rev()
1167 .map(|cached_entry| &cached_entry.entry)
1168 .skip_while(|entry| entry != &selected_entry)
1169 .skip(1);
1170 match &selected_entry {
1171 PanelEntry::Fs(fs_entry) => match fs_entry {
1172 FsEntry::ExternalFile(..) => None,
1173 FsEntry::File(FsEntryFile {
1174 worktree_id, entry, ..
1175 })
1176 | FsEntry::Directory(FsEntryDirectory {
1177 worktree_id, entry, ..
1178 }) => entry.path.parent().and_then(|parent_path| {
1179 previous_entries.find(|entry| match entry {
1180 PanelEntry::Fs(FsEntry::Directory(directory)) => {
1181 directory.worktree_id == *worktree_id
1182 && directory.entry.path.as_ref() == parent_path
1183 }
1184 PanelEntry::FoldedDirs(FoldedDirsEntry {
1185 worktree_id: dirs_worktree_id,
1186 entries: dirs,
1187 ..
1188 }) => {
1189 dirs_worktree_id == worktree_id
1190 && dirs
1191 .last()
1192 .map_or(false, |dir| dir.path.as_ref() == parent_path)
1193 }
1194 _ => false,
1195 })
1196 }),
1197 },
1198 PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
1199 .entries
1200 .first()
1201 .and_then(|entry| entry.path.parent())
1202 .and_then(|parent_path| {
1203 previous_entries.find(|entry| {
1204 if let PanelEntry::Fs(FsEntry::Directory(directory)) = entry {
1205 directory.worktree_id == folded_dirs.worktree_id
1206 && directory.entry.path.as_ref() == parent_path
1207 } else {
1208 false
1209 }
1210 })
1211 }),
1212 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1213 previous_entries.find(|entry| match entry {
1214 PanelEntry::Fs(FsEntry::File(file)) => {
1215 file.buffer_id == excerpt.buffer_id
1216 && file.excerpts.contains(&excerpt.id)
1217 }
1218 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1219 external_file.buffer_id == excerpt.buffer_id
1220 && external_file.excerpts.contains(&excerpt.id)
1221 }
1222 _ => false,
1223 })
1224 }
1225 PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1226 previous_entries.find(|entry| {
1227 if let PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) = entry {
1228 outline.buffer_id == excerpt.buffer_id
1229 && outline.excerpt_id == excerpt.id
1230 } else {
1231 false
1232 }
1233 })
1234 }
1235 PanelEntry::Search(_) => {
1236 previous_entries.find(|entry| !matches!(entry, PanelEntry::Search(_)))
1237 }
1238 }
1239 }) {
1240 self.select_entry(entry_to_select.clone(), true, window, cx);
1241 } else {
1242 self.select_first(&SelectFirst {}, window, cx);
1243 }
1244 }
1245
1246 fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1247 if let Some(first_entry) = self.cached_entries.first() {
1248 self.select_entry(first_entry.entry.clone(), true, window, cx);
1249 }
1250 }
1251
1252 fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
1253 if let Some(new_selection) = self
1254 .cached_entries
1255 .iter()
1256 .rev()
1257 .map(|cached_entry| &cached_entry.entry)
1258 .next()
1259 {
1260 self.select_entry(new_selection.clone(), true, window, cx);
1261 }
1262 }
1263
1264 fn autoscroll(&mut self, cx: &mut Context<Self>) {
1265 if let Some(selected_entry) = self.selected_entry() {
1266 let index = self
1267 .cached_entries
1268 .iter()
1269 .position(|cached_entry| &cached_entry.entry == selected_entry);
1270 if let Some(index) = index {
1271 self.scroll_handle
1272 .scroll_to_item(index, ScrollStrategy::Center);
1273 cx.notify();
1274 }
1275 }
1276 }
1277
1278 fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1279 if !self.focus_handle.contains_focused(window, cx) {
1280 cx.emit(Event::Focus);
1281 }
1282 }
1283
1284 fn deploy_context_menu(
1285 &mut self,
1286 position: Point<Pixels>,
1287 entry: PanelEntry,
1288 window: &mut Window,
1289 cx: &mut Context<Self>,
1290 ) {
1291 self.select_entry(entry.clone(), true, window, cx);
1292 let is_root = match &entry {
1293 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1294 worktree_id, entry, ..
1295 }))
1296 | PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1297 worktree_id, entry, ..
1298 })) => self
1299 .project
1300 .read(cx)
1301 .worktree_for_id(*worktree_id, cx)
1302 .map(|worktree| {
1303 worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1304 })
1305 .unwrap_or(false),
1306 PanelEntry::FoldedDirs(FoldedDirsEntry {
1307 worktree_id,
1308 entries,
1309 ..
1310 }) => entries
1311 .first()
1312 .and_then(|entry| {
1313 self.project
1314 .read(cx)
1315 .worktree_for_id(*worktree_id, cx)
1316 .map(|worktree| {
1317 worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1318 })
1319 })
1320 .unwrap_or(false),
1321 PanelEntry::Fs(FsEntry::ExternalFile(..)) => false,
1322 PanelEntry::Outline(..) => {
1323 cx.notify();
1324 return;
1325 }
1326 PanelEntry::Search(_) => {
1327 cx.notify();
1328 return;
1329 }
1330 };
1331 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
1332 let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(&entry);
1333 let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(&entry);
1334
1335 let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
1336 menu.context(self.focus_handle.clone())
1337 .when(cfg!(target_os = "macos"), |menu| {
1338 menu.action("Reveal in Finder", Box::new(RevealInFileManager))
1339 })
1340 .when(cfg!(not(target_os = "macos")), |menu| {
1341 menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
1342 })
1343 .action("Open in Terminal", Box::new(OpenInTerminal))
1344 .when(is_unfoldable, |menu| {
1345 menu.action("Unfold Directory", Box::new(UnfoldDirectory))
1346 })
1347 .when(is_foldable, |menu| {
1348 menu.action("Fold Directory", Box::new(FoldDirectory))
1349 })
1350 .separator()
1351 .action("Copy Path", Box::new(zed_actions::workspace::CopyPath))
1352 .action(
1353 "Copy Relative Path",
1354 Box::new(zed_actions::workspace::CopyRelativePath),
1355 )
1356 });
1357 window.focus(&context_menu.focus_handle(cx));
1358 let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
1359 outline_panel.context_menu.take();
1360 cx.notify();
1361 });
1362 self.context_menu = Some((context_menu, position, subscription));
1363 cx.notify();
1364 }
1365
1366 fn is_unfoldable(&self, entry: &PanelEntry) -> bool {
1367 matches!(entry, PanelEntry::FoldedDirs(..))
1368 }
1369
1370 fn is_foldable(&self, entry: &PanelEntry) -> bool {
1371 let (directory_worktree, directory_entry) = match entry {
1372 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1373 worktree_id,
1374 entry: directory_entry,
1375 ..
1376 })) => (*worktree_id, Some(directory_entry)),
1377 _ => return false,
1378 };
1379 let Some(directory_entry) = directory_entry else {
1380 return false;
1381 };
1382
1383 if self
1384 .unfolded_dirs
1385 .get(&directory_worktree)
1386 .map_or(true, |unfolded_dirs| {
1387 !unfolded_dirs.contains(&directory_entry.id)
1388 })
1389 {
1390 return false;
1391 }
1392
1393 let children = self
1394 .fs_children_count
1395 .get(&directory_worktree)
1396 .and_then(|entries| entries.get(&directory_entry.path))
1397 .copied()
1398 .unwrap_or_default();
1399
1400 children.may_be_fold_part() && children.dirs > 0
1401 }
1402
1403 fn expand_selected_entry(
1404 &mut self,
1405 _: &ExpandSelectedEntry,
1406 window: &mut Window,
1407 cx: &mut Context<Self>,
1408 ) {
1409 let Some(active_editor) = self.active_editor() else {
1410 return;
1411 };
1412 let Some(selected_entry) = self.selected_entry().cloned() else {
1413 return;
1414 };
1415 let mut buffers_to_unfold = HashSet::default();
1416 let entry_to_expand = match &selected_entry {
1417 PanelEntry::FoldedDirs(FoldedDirsEntry {
1418 entries: dir_entries,
1419 worktree_id,
1420 ..
1421 }) => dir_entries.last().map(|entry| {
1422 buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry));
1423 CollapsedEntry::Dir(*worktree_id, entry.id)
1424 }),
1425 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1426 worktree_id, entry, ..
1427 })) => {
1428 buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry));
1429 Some(CollapsedEntry::Dir(*worktree_id, entry.id))
1430 }
1431 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1432 worktree_id,
1433 buffer_id,
1434 ..
1435 })) => {
1436 buffers_to_unfold.insert(*buffer_id);
1437 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1438 }
1439 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1440 buffers_to_unfold.insert(external_file.buffer_id);
1441 Some(CollapsedEntry::ExternalFile(external_file.buffer_id))
1442 }
1443 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1444 Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id))
1445 }
1446 PanelEntry::Search(_) | PanelEntry::Outline(..) => return,
1447 };
1448 let Some(collapsed_entry) = entry_to_expand else {
1449 return;
1450 };
1451 let expanded = self.collapsed_entries.remove(&collapsed_entry);
1452 if expanded {
1453 if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
1454 let task = self.project.update(cx, |project, cx| {
1455 project.expand_entry(worktree_id, dir_entry_id, cx)
1456 });
1457 if let Some(task) = task {
1458 task.detach_and_log_err(cx);
1459 }
1460 };
1461
1462 active_editor.update(cx, |editor, cx| {
1463 buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
1464 });
1465 self.select_entry(selected_entry, true, window, cx);
1466 if buffers_to_unfold.is_empty() {
1467 self.update_cached_entries(None, window, cx);
1468 } else {
1469 self.toggle_buffers_fold(buffers_to_unfold, false, window, cx)
1470 .detach();
1471 }
1472 } else {
1473 self.select_next(&SelectNext, window, cx)
1474 }
1475 }
1476
1477 fn collapse_selected_entry(
1478 &mut self,
1479 _: &CollapseSelectedEntry,
1480 window: &mut Window,
1481 cx: &mut Context<Self>,
1482 ) {
1483 let Some(active_editor) = self.active_editor() else {
1484 return;
1485 };
1486 let Some(selected_entry) = self.selected_entry().cloned() else {
1487 return;
1488 };
1489
1490 let mut buffers_to_fold = HashSet::default();
1491 let collapsed = match &selected_entry {
1492 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1493 worktree_id, entry, ..
1494 })) => {
1495 if self
1496 .collapsed_entries
1497 .insert(CollapsedEntry::Dir(*worktree_id, entry.id))
1498 {
1499 buffers_to_fold.extend(self.buffers_inside_directory(*worktree_id, entry));
1500 true
1501 } else {
1502 false
1503 }
1504 }
1505 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1506 worktree_id,
1507 buffer_id,
1508 ..
1509 })) => {
1510 if self
1511 .collapsed_entries
1512 .insert(CollapsedEntry::File(*worktree_id, *buffer_id))
1513 {
1514 buffers_to_fold.insert(*buffer_id);
1515 true
1516 } else {
1517 false
1518 }
1519 }
1520 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1521 if self
1522 .collapsed_entries
1523 .insert(CollapsedEntry::ExternalFile(external_file.buffer_id))
1524 {
1525 buffers_to_fold.insert(external_file.buffer_id);
1526 true
1527 } else {
1528 false
1529 }
1530 }
1531 PanelEntry::FoldedDirs(folded_dirs) => {
1532 let mut folded = false;
1533 if let Some(dir_entry) = folded_dirs.entries.last() {
1534 if self
1535 .collapsed_entries
1536 .insert(CollapsedEntry::Dir(folded_dirs.worktree_id, dir_entry.id))
1537 {
1538 folded = true;
1539 buffers_to_fold.extend(
1540 self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry),
1541 );
1542 }
1543 }
1544 folded
1545 }
1546 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
1547 .collapsed_entries
1548 .insert(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)),
1549 PanelEntry::Search(_) | PanelEntry::Outline(..) => false,
1550 };
1551
1552 if collapsed {
1553 active_editor.update(cx, |editor, cx| {
1554 buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
1555 });
1556 self.select_entry(selected_entry, true, window, cx);
1557 if buffers_to_fold.is_empty() {
1558 self.update_cached_entries(None, window, cx);
1559 } else {
1560 self.toggle_buffers_fold(buffers_to_fold, true, window, cx)
1561 .detach();
1562 }
1563 } else {
1564 self.select_parent(&SelectParent, window, cx);
1565 }
1566 }
1567
1568 pub fn expand_all_entries(
1569 &mut self,
1570 _: &ExpandAllEntries,
1571 window: &mut Window,
1572 cx: &mut Context<Self>,
1573 ) {
1574 let Some(active_editor) = self.active_editor() else {
1575 return;
1576 };
1577 let mut buffers_to_unfold = HashSet::default();
1578 let expanded_entries =
1579 self.fs_entries
1580 .iter()
1581 .fold(HashSet::default(), |mut entries, fs_entry| {
1582 match fs_entry {
1583 FsEntry::ExternalFile(external_file) => {
1584 buffers_to_unfold.insert(external_file.buffer_id);
1585 entries.insert(CollapsedEntry::ExternalFile(external_file.buffer_id));
1586 entries.extend(
1587 self.excerpts
1588 .get(&external_file.buffer_id)
1589 .into_iter()
1590 .flat_map(|excerpts| {
1591 excerpts.iter().map(|(excerpt_id, _)| {
1592 CollapsedEntry::Excerpt(
1593 external_file.buffer_id,
1594 *excerpt_id,
1595 )
1596 })
1597 }),
1598 );
1599 }
1600 FsEntry::Directory(directory) => {
1601 entries.insert(CollapsedEntry::Dir(
1602 directory.worktree_id,
1603 directory.entry.id,
1604 ));
1605 }
1606 FsEntry::File(file) => {
1607 buffers_to_unfold.insert(file.buffer_id);
1608 entries.insert(CollapsedEntry::File(file.worktree_id, file.buffer_id));
1609 entries.extend(
1610 self.excerpts.get(&file.buffer_id).into_iter().flat_map(
1611 |excerpts| {
1612 excerpts.iter().map(|(excerpt_id, _)| {
1613 CollapsedEntry::Excerpt(file.buffer_id, *excerpt_id)
1614 })
1615 },
1616 ),
1617 );
1618 }
1619 };
1620 entries
1621 });
1622 self.collapsed_entries
1623 .retain(|entry| !expanded_entries.contains(entry));
1624 active_editor.update(cx, |editor, cx| {
1625 buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
1626 });
1627 if buffers_to_unfold.is_empty() {
1628 self.update_cached_entries(None, window, cx);
1629 } else {
1630 self.toggle_buffers_fold(buffers_to_unfold, false, window, cx)
1631 .detach();
1632 }
1633 }
1634
1635 pub fn collapse_all_entries(
1636 &mut self,
1637 _: &CollapseAllEntries,
1638 window: &mut Window,
1639 cx: &mut Context<Self>,
1640 ) {
1641 let Some(active_editor) = self.active_editor() else {
1642 return;
1643 };
1644 let mut buffers_to_fold = HashSet::default();
1645 let new_entries = self
1646 .cached_entries
1647 .iter()
1648 .flat_map(|cached_entry| match &cached_entry.entry {
1649 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1650 worktree_id, entry, ..
1651 })) => Some(CollapsedEntry::Dir(*worktree_id, entry.id)),
1652 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1653 worktree_id,
1654 buffer_id,
1655 ..
1656 })) => {
1657 buffers_to_fold.insert(*buffer_id);
1658 Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1659 }
1660 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1661 buffers_to_fold.insert(external_file.buffer_id);
1662 Some(CollapsedEntry::ExternalFile(external_file.buffer_id))
1663 }
1664 PanelEntry::FoldedDirs(FoldedDirsEntry {
1665 worktree_id,
1666 entries,
1667 ..
1668 }) => Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)),
1669 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1670 Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id))
1671 }
1672 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1673 })
1674 .collect::<Vec<_>>();
1675 self.collapsed_entries.extend(new_entries);
1676
1677 active_editor.update(cx, |editor, cx| {
1678 buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
1679 });
1680 if buffers_to_fold.is_empty() {
1681 self.update_cached_entries(None, window, cx);
1682 } else {
1683 self.toggle_buffers_fold(buffers_to_fold, true, window, cx)
1684 .detach();
1685 }
1686 }
1687
1688 fn toggle_expanded(&mut self, entry: &PanelEntry, window: &mut Window, cx: &mut Context<Self>) {
1689 let Some(active_editor) = self.active_editor() else {
1690 return;
1691 };
1692 let mut fold = false;
1693 let mut buffers_to_toggle = HashSet::default();
1694 match entry {
1695 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1696 worktree_id,
1697 entry: dir_entry,
1698 ..
1699 })) => {
1700 let entry_id = dir_entry.id;
1701 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1702 buffers_to_toggle.extend(self.buffers_inside_directory(*worktree_id, dir_entry));
1703 if self.collapsed_entries.remove(&collapsed_entry) {
1704 self.project
1705 .update(cx, |project, cx| {
1706 project.expand_entry(*worktree_id, entry_id, cx)
1707 })
1708 .unwrap_or_else(|| Task::ready(Ok(())))
1709 .detach_and_log_err(cx);
1710 } else {
1711 self.collapsed_entries.insert(collapsed_entry);
1712 fold = true;
1713 }
1714 }
1715 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1716 worktree_id,
1717 buffer_id,
1718 ..
1719 })) => {
1720 let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1721 buffers_to_toggle.insert(*buffer_id);
1722 if !self.collapsed_entries.remove(&collapsed_entry) {
1723 self.collapsed_entries.insert(collapsed_entry);
1724 fold = true;
1725 }
1726 }
1727 PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1728 let collapsed_entry = CollapsedEntry::ExternalFile(external_file.buffer_id);
1729 buffers_to_toggle.insert(external_file.buffer_id);
1730 if !self.collapsed_entries.remove(&collapsed_entry) {
1731 self.collapsed_entries.insert(collapsed_entry);
1732 fold = true;
1733 }
1734 }
1735 PanelEntry::FoldedDirs(FoldedDirsEntry {
1736 worktree_id,
1737 entries: dir_entries,
1738 ..
1739 }) => {
1740 if let Some(dir_entry) = dir_entries.first() {
1741 let entry_id = dir_entry.id;
1742 let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1743 buffers_to_toggle
1744 .extend(self.buffers_inside_directory(*worktree_id, dir_entry));
1745 if self.collapsed_entries.remove(&collapsed_entry) {
1746 self.project
1747 .update(cx, |project, cx| {
1748 project.expand_entry(*worktree_id, entry_id, cx)
1749 })
1750 .unwrap_or_else(|| Task::ready(Ok(())))
1751 .detach_and_log_err(cx);
1752 } else {
1753 self.collapsed_entries.insert(collapsed_entry);
1754 fold = true;
1755 }
1756 }
1757 }
1758 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1759 let collapsed_entry = CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id);
1760 if !self.collapsed_entries.remove(&collapsed_entry) {
1761 self.collapsed_entries.insert(collapsed_entry);
1762 }
1763 }
1764 PanelEntry::Search(_) | PanelEntry::Outline(..) => return,
1765 }
1766
1767 active_editor.update(cx, |editor, cx| {
1768 buffers_to_toggle.retain(|buffer_id| {
1769 let folded = editor.is_buffer_folded(*buffer_id, cx);
1770 if fold {
1771 !folded
1772 } else {
1773 folded
1774 }
1775 });
1776 });
1777
1778 self.select_entry(entry.clone(), true, window, cx);
1779 if buffers_to_toggle.is_empty() {
1780 self.update_cached_entries(None, window, cx);
1781 } else {
1782 self.toggle_buffers_fold(buffers_to_toggle, fold, window, cx)
1783 .detach();
1784 }
1785 }
1786
1787 fn toggle_buffers_fold(
1788 &self,
1789 buffers: HashSet<BufferId>,
1790 fold: bool,
1791 window: &mut Window,
1792 cx: &mut Context<Self>,
1793 ) -> Task<()> {
1794 let Some(active_editor) = self.active_editor() else {
1795 return Task::ready(());
1796 };
1797 cx.spawn_in(window, async move |outline_panel, cx| {
1798 outline_panel
1799 .update_in(cx, |outline_panel, window, cx| {
1800 active_editor.update(cx, |editor, cx| {
1801 for buffer_id in buffers {
1802 outline_panel
1803 .preserve_selection_on_buffer_fold_toggles
1804 .insert(buffer_id);
1805 if fold {
1806 editor.fold_buffer(buffer_id, cx);
1807 } else {
1808 editor.unfold_buffer(buffer_id, cx);
1809 }
1810 }
1811 });
1812 if let Some(selection) = outline_panel.selected_entry().cloned() {
1813 outline_panel.scroll_editor_to_entry(&selection, false, false, window, cx);
1814 }
1815 })
1816 .ok();
1817 })
1818 }
1819
1820 fn copy_path(
1821 &mut self,
1822 _: &zed_actions::workspace::CopyPath,
1823 _: &mut Window,
1824 cx: &mut Context<Self>,
1825 ) {
1826 if let Some(clipboard_text) = self
1827 .selected_entry()
1828 .and_then(|entry| self.abs_path(entry, cx))
1829 .map(|p| p.to_string_lossy().to_string())
1830 {
1831 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1832 }
1833 }
1834
1835 fn copy_relative_path(
1836 &mut self,
1837 _: &zed_actions::workspace::CopyRelativePath,
1838 _: &mut Window,
1839 cx: &mut Context<Self>,
1840 ) {
1841 if let Some(clipboard_text) = self
1842 .selected_entry()
1843 .and_then(|entry| match entry {
1844 PanelEntry::Fs(entry) => self.relative_path(entry, cx),
1845 PanelEntry::FoldedDirs(folded_dirs) => {
1846 folded_dirs.entries.last().map(|entry| entry.path.clone())
1847 }
1848 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1849 })
1850 .map(|p| p.to_string_lossy().to_string())
1851 {
1852 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1853 }
1854 }
1855
1856 fn reveal_in_finder(
1857 &mut self,
1858 _: &RevealInFileManager,
1859 _: &mut Window,
1860 cx: &mut Context<Self>,
1861 ) {
1862 if let Some(abs_path) = self
1863 .selected_entry()
1864 .and_then(|entry| self.abs_path(entry, cx))
1865 {
1866 cx.reveal_path(&abs_path);
1867 }
1868 }
1869
1870 fn open_in_terminal(
1871 &mut self,
1872 _: &OpenInTerminal,
1873 window: &mut Window,
1874 cx: &mut Context<Self>,
1875 ) {
1876 let selected_entry = self.selected_entry();
1877 let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx));
1878 let working_directory = if let (
1879 Some(abs_path),
1880 Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1881 ) = (&abs_path, selected_entry)
1882 {
1883 abs_path.parent().map(|p| p.to_owned())
1884 } else {
1885 abs_path
1886 };
1887
1888 if let Some(working_directory) = working_directory {
1889 window.dispatch_action(
1890 workspace::OpenTerminal { working_directory }.boxed_clone(),
1891 cx,
1892 )
1893 }
1894 }
1895
1896 fn reveal_entry_for_selection(
1897 &mut self,
1898 editor: Entity<Editor>,
1899 window: &mut Window,
1900 cx: &mut Context<Self>,
1901 ) {
1902 if !self.active
1903 || !OutlinePanelSettings::get_global(cx).auto_reveal_entries
1904 || self.focus_handle.contains_focused(window, cx)
1905 {
1906 return;
1907 }
1908 let project = self.project.clone();
1909 self.reveal_selection_task = cx.spawn_in(window, async move |outline_panel, cx| {
1910 cx.background_executor().timer(UPDATE_DEBOUNCE).await;
1911 let entry_with_selection =
1912 outline_panel.update_in(cx, |outline_panel, window, cx| {
1913 outline_panel.location_for_editor_selection(&editor, window, cx)
1914 })?;
1915 let Some(entry_with_selection) = entry_with_selection else {
1916 outline_panel.update(cx, |outline_panel, cx| {
1917 outline_panel.selected_entry = SelectedEntry::None;
1918 cx.notify();
1919 })?;
1920 return Ok(());
1921 };
1922 let related_buffer_entry = match &entry_with_selection {
1923 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1924 worktree_id,
1925 buffer_id,
1926 ..
1927 })) => project.update(cx, |project, cx| {
1928 let entry_id = project
1929 .buffer_for_id(*buffer_id, cx)
1930 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1931 project
1932 .worktree_for_id(*worktree_id, cx)
1933 .zip(entry_id)
1934 .and_then(|(worktree, entry_id)| {
1935 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1936 Some((worktree, entry))
1937 })
1938 })?,
1939 PanelEntry::Outline(outline_entry) => {
1940 let (buffer_id, excerpt_id) = outline_entry.ids();
1941 outline_panel.update(cx, |outline_panel, cx| {
1942 outline_panel
1943 .collapsed_entries
1944 .remove(&CollapsedEntry::ExternalFile(buffer_id));
1945 outline_panel
1946 .collapsed_entries
1947 .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1948 let project = outline_panel.project.read(cx);
1949 let entry_id = project
1950 .buffer_for_id(buffer_id, cx)
1951 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1952
1953 entry_id.and_then(|entry_id| {
1954 project
1955 .worktree_for_entry(entry_id, cx)
1956 .and_then(|worktree| {
1957 let worktree_id = worktree.read(cx).id();
1958 outline_panel
1959 .collapsed_entries
1960 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1961 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1962 Some((worktree, entry))
1963 })
1964 })
1965 })?
1966 }
1967 PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
1968 PanelEntry::Search(SearchEntry { match_range, .. }) => match_range
1969 .start
1970 .buffer_id
1971 .or(match_range.end.buffer_id)
1972 .map(|buffer_id| {
1973 outline_panel.update(cx, |outline_panel, cx| {
1974 outline_panel
1975 .collapsed_entries
1976 .remove(&CollapsedEntry::ExternalFile(buffer_id));
1977 let project = project.read(cx);
1978 let entry_id = project
1979 .buffer_for_id(buffer_id, cx)
1980 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1981
1982 entry_id.and_then(|entry_id| {
1983 project
1984 .worktree_for_entry(entry_id, cx)
1985 .and_then(|worktree| {
1986 let worktree_id = worktree.read(cx).id();
1987 outline_panel
1988 .collapsed_entries
1989 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1990 let entry =
1991 worktree.read(cx).entry_for_id(entry_id)?.clone();
1992 Some((worktree, entry))
1993 })
1994 })
1995 })
1996 })
1997 .transpose()?
1998 .flatten(),
1999 _ => return anyhow::Ok(()),
2000 };
2001 if let Some((worktree, buffer_entry)) = related_buffer_entry {
2002 outline_panel.update(cx, |outline_panel, cx| {
2003 let worktree_id = worktree.read(cx).id();
2004 let mut dirs_to_expand = Vec::new();
2005 {
2006 let mut traversal = worktree.read(cx).traverse_from_path(
2007 true,
2008 true,
2009 true,
2010 buffer_entry.path.as_ref(),
2011 );
2012 let mut current_entry = buffer_entry;
2013 loop {
2014 if current_entry.is_dir()
2015 && outline_panel
2016 .collapsed_entries
2017 .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
2018 {
2019 dirs_to_expand.push(current_entry.id);
2020 }
2021
2022 if traversal.back_to_parent() {
2023 if let Some(parent_entry) = traversal.entry() {
2024 current_entry = parent_entry.clone();
2025 continue;
2026 }
2027 }
2028 break;
2029 }
2030 }
2031 for dir_to_expand in dirs_to_expand {
2032 project
2033 .update(cx, |project, cx| {
2034 project.expand_entry(worktree_id, dir_to_expand, cx)
2035 })
2036 .unwrap_or_else(|| Task::ready(Ok(())))
2037 .detach_and_log_err(cx)
2038 }
2039 })?
2040 }
2041
2042 outline_panel.update_in(cx, |outline_panel, window, cx| {
2043 outline_panel.select_entry(entry_with_selection, false, window, cx);
2044 outline_panel.update_cached_entries(None, window, cx);
2045 })?;
2046
2047 anyhow::Ok(())
2048 });
2049 }
2050
2051 fn render_excerpt(
2052 &self,
2053 excerpt: &OutlineEntryExcerpt,
2054 depth: usize,
2055 window: &mut Window,
2056 cx: &mut Context<OutlinePanel>,
2057 ) -> Option<Stateful<Div>> {
2058 let item_id = ElementId::from(excerpt.id.to_proto() as usize);
2059 let is_active = match self.selected_entry() {
2060 Some(PanelEntry::Outline(OutlineEntry::Excerpt(selected_excerpt))) => {
2061 selected_excerpt.buffer_id == excerpt.buffer_id && selected_excerpt.id == excerpt.id
2062 }
2063 _ => false,
2064 };
2065 let has_outlines = self
2066 .excerpts
2067 .get(&excerpt.buffer_id)
2068 .and_then(|excerpts| match &excerpts.get(&excerpt.id)?.outlines {
2069 ExcerptOutlines::Outlines(outlines) => Some(outlines),
2070 ExcerptOutlines::Invalidated(outlines) => Some(outlines),
2071 ExcerptOutlines::NotFetched => None,
2072 })
2073 .map_or(false, |outlines| !outlines.is_empty());
2074 let is_expanded = !self
2075 .collapsed_entries
2076 .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id));
2077 let color = entry_label_color(is_active);
2078 let icon = if has_outlines {
2079 FileIcons::get_chevron_icon(is_expanded, cx)
2080 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2081 } else {
2082 None
2083 }
2084 .unwrap_or_else(empty_icon);
2085
2086 let label = self.excerpt_label(excerpt.buffer_id, &excerpt.range, cx)?;
2087 let label_element = Label::new(label)
2088 .single_line()
2089 .color(color)
2090 .into_any_element();
2091
2092 Some(self.entry_element(
2093 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())),
2094 item_id,
2095 depth,
2096 Some(icon),
2097 is_active,
2098 label_element,
2099 window,
2100 cx,
2101 ))
2102 }
2103
2104 fn excerpt_label(
2105 &self,
2106 buffer_id: BufferId,
2107 range: &ExcerptRange<language::Anchor>,
2108 cx: &App,
2109 ) -> Option<String> {
2110 let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
2111 let excerpt_range = range.context.to_point(&buffer_snapshot);
2112 Some(format!(
2113 "Lines {}- {}",
2114 excerpt_range.start.row + 1,
2115 excerpt_range.end.row + 1,
2116 ))
2117 }
2118
2119 fn render_outline(
2120 &self,
2121 outline: &OutlineEntryOutline,
2122 depth: usize,
2123 string_match: Option<&StringMatch>,
2124 window: &mut Window,
2125 cx: &mut Context<Self>,
2126 ) -> Stateful<Div> {
2127 let item_id = ElementId::from(SharedString::from(format!(
2128 "{:?}|{:?}{:?}|{:?}",
2129 outline.buffer_id, outline.excerpt_id, outline.outline.range, &outline.outline.text,
2130 )));
2131
2132 let label_element = outline::render_item(
2133 &outline.outline,
2134 string_match
2135 .map(|string_match| string_match.ranges().collect::<Vec<_>>())
2136 .unwrap_or_default(),
2137 cx,
2138 )
2139 .into_any_element();
2140
2141 let is_active = match self.selected_entry() {
2142 Some(PanelEntry::Outline(OutlineEntry::Outline(selected))) => {
2143 outline == selected && outline.outline == selected.outline
2144 }
2145 _ => false,
2146 };
2147
2148 let icon = if self.is_singleton_active(cx) {
2149 None
2150 } else {
2151 Some(empty_icon())
2152 };
2153
2154 self.entry_element(
2155 PanelEntry::Outline(OutlineEntry::Outline(outline.clone())),
2156 item_id,
2157 depth,
2158 icon,
2159 is_active,
2160 label_element,
2161 window,
2162 cx,
2163 )
2164 }
2165
2166 fn render_entry(
2167 &self,
2168 rendered_entry: &FsEntry,
2169 depth: usize,
2170 string_match: Option<&StringMatch>,
2171 window: &mut Window,
2172 cx: &mut Context<Self>,
2173 ) -> Stateful<Div> {
2174 let settings = OutlinePanelSettings::get_global(cx);
2175 let is_active = match self.selected_entry() {
2176 Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry,
2177 _ => false,
2178 };
2179 let (item_id, label_element, icon) = match rendered_entry {
2180 FsEntry::File(FsEntryFile {
2181 worktree_id, entry, ..
2182 }) => {
2183 let name = self.entry_name(worktree_id, entry, cx);
2184 let color =
2185 entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
2186 let icon = if settings.file_icons {
2187 FileIcons::get_icon(&entry.path, cx)
2188 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2189 } else {
2190 None
2191 };
2192 (
2193 ElementId::from(entry.id.to_proto() as usize),
2194 HighlightedLabel::new(
2195 name,
2196 string_match
2197 .map(|string_match| string_match.positions.clone())
2198 .unwrap_or_default(),
2199 )
2200 .color(color)
2201 .into_any_element(),
2202 icon.unwrap_or_else(empty_icon),
2203 )
2204 }
2205 FsEntry::Directory(directory) => {
2206 let name = self.entry_name(&directory.worktree_id, &directory.entry, cx);
2207
2208 let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Dir(
2209 directory.worktree_id,
2210 directory.entry.id,
2211 ));
2212 let color = entry_git_aware_label_color(
2213 directory.entry.git_summary,
2214 directory.entry.is_ignored,
2215 is_active,
2216 );
2217 let icon = if settings.folder_icons {
2218 FileIcons::get_folder_icon(is_expanded, cx)
2219 } else {
2220 FileIcons::get_chevron_icon(is_expanded, cx)
2221 }
2222 .map(Icon::from_path)
2223 .map(|icon| icon.color(color).into_any_element());
2224 (
2225 ElementId::from(directory.entry.id.to_proto() as usize),
2226 HighlightedLabel::new(
2227 name,
2228 string_match
2229 .map(|string_match| string_match.positions.clone())
2230 .unwrap_or_default(),
2231 )
2232 .color(color)
2233 .into_any_element(),
2234 icon.unwrap_or_else(empty_icon),
2235 )
2236 }
2237 FsEntry::ExternalFile(external_file) => {
2238 let color = entry_label_color(is_active);
2239 let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) {
2240 Some(buffer_snapshot) => match buffer_snapshot.file() {
2241 Some(file) => {
2242 let path = file.path();
2243 let icon = if settings.file_icons {
2244 FileIcons::get_icon(path.as_ref(), cx)
2245 } else {
2246 None
2247 }
2248 .map(Icon::from_path)
2249 .map(|icon| icon.color(color).into_any_element());
2250 (icon, file_name(path.as_ref()))
2251 }
2252 None => (None, "Untitled".to_string()),
2253 },
2254 None => (None, "Unknown buffer".to_string()),
2255 };
2256 (
2257 ElementId::from(external_file.buffer_id.to_proto() as usize),
2258 HighlightedLabel::new(
2259 name,
2260 string_match
2261 .map(|string_match| string_match.positions.clone())
2262 .unwrap_or_default(),
2263 )
2264 .color(color)
2265 .into_any_element(),
2266 icon.unwrap_or_else(empty_icon),
2267 )
2268 }
2269 };
2270
2271 self.entry_element(
2272 PanelEntry::Fs(rendered_entry.clone()),
2273 item_id,
2274 depth,
2275 Some(icon),
2276 is_active,
2277 label_element,
2278 window,
2279 cx,
2280 )
2281 }
2282
2283 fn render_folded_dirs(
2284 &self,
2285 folded_dir: &FoldedDirsEntry,
2286 depth: usize,
2287 string_match: Option<&StringMatch>,
2288 window: &mut Window,
2289 cx: &mut Context<OutlinePanel>,
2290 ) -> Stateful<Div> {
2291 let settings = OutlinePanelSettings::get_global(cx);
2292 let is_active = match self.selected_entry() {
2293 Some(PanelEntry::FoldedDirs(selected_dirs)) => {
2294 selected_dirs.worktree_id == folded_dir.worktree_id
2295 && selected_dirs.entries == folded_dir.entries
2296 }
2297 _ => false,
2298 };
2299 let (item_id, label_element, icon) = {
2300 let name = self.dir_names_string(&folded_dir.entries, folded_dir.worktree_id, cx);
2301
2302 let is_expanded = folded_dir.entries.iter().all(|dir| {
2303 !self
2304 .collapsed_entries
2305 .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id))
2306 });
2307 let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored);
2308 let git_status = folded_dir
2309 .entries
2310 .first()
2311 .map(|entry| entry.git_summary)
2312 .unwrap_or_default();
2313 let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
2314 let icon = if settings.folder_icons {
2315 FileIcons::get_folder_icon(is_expanded, cx)
2316 } else {
2317 FileIcons::get_chevron_icon(is_expanded, cx)
2318 }
2319 .map(Icon::from_path)
2320 .map(|icon| icon.color(color).into_any_element());
2321 (
2322 ElementId::from(
2323 folded_dir
2324 .entries
2325 .last()
2326 .map(|entry| entry.id.to_proto())
2327 .unwrap_or_else(|| folded_dir.worktree_id.to_proto())
2328 as usize,
2329 ),
2330 HighlightedLabel::new(
2331 name,
2332 string_match
2333 .map(|string_match| string_match.positions.clone())
2334 .unwrap_or_default(),
2335 )
2336 .color(color)
2337 .into_any_element(),
2338 icon.unwrap_or_else(empty_icon),
2339 )
2340 };
2341
2342 self.entry_element(
2343 PanelEntry::FoldedDirs(folded_dir.clone()),
2344 item_id,
2345 depth,
2346 Some(icon),
2347 is_active,
2348 label_element,
2349 window,
2350 cx,
2351 )
2352 }
2353
2354 fn render_search_match(
2355 &mut self,
2356 multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
2357 match_range: &Range<editor::Anchor>,
2358 render_data: &Arc<OnceLock<SearchData>>,
2359 kind: SearchKind,
2360 depth: usize,
2361 string_match: Option<&StringMatch>,
2362 window: &mut Window,
2363 cx: &mut Context<Self>,
2364 ) -> Option<Stateful<Div>> {
2365 let search_data = match render_data.get() {
2366 Some(search_data) => search_data,
2367 None => {
2368 if let ItemsDisplayMode::Search(search_state) = &mut self.mode {
2369 if let Some(multi_buffer_snapshot) = multi_buffer_snapshot {
2370 search_state
2371 .highlight_search_match_tx
2372 .try_send(HighlightArguments {
2373 multi_buffer_snapshot: multi_buffer_snapshot.clone(),
2374 match_range: match_range.clone(),
2375 search_data: Arc::clone(render_data),
2376 })
2377 .ok();
2378 }
2379 }
2380 return None;
2381 }
2382 };
2383 let search_matches = string_match
2384 .iter()
2385 .flat_map(|string_match| string_match.ranges())
2386 .collect::<Vec<_>>();
2387 let match_ranges = if search_matches.is_empty() {
2388 &search_data.search_match_indices
2389 } else {
2390 &search_matches
2391 };
2392 let label_element = outline::render_item(
2393 &OutlineItem {
2394 depth,
2395 annotation_range: None,
2396 range: search_data.context_range.clone(),
2397 text: search_data.context_text.clone(),
2398 highlight_ranges: search_data
2399 .highlights_data
2400 .get()
2401 .cloned()
2402 .unwrap_or_default(),
2403 name_ranges: search_data.search_match_indices.clone(),
2404 body_range: Some(search_data.context_range.clone()),
2405 },
2406 match_ranges.iter().cloned(),
2407 cx,
2408 );
2409 let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
2410 let entire_label = h_flex()
2411 .justify_center()
2412 .p_0()
2413 .when(search_data.truncated_left, |parent| {
2414 parent.child(truncated_contents_label())
2415 })
2416 .child(label_element)
2417 .when(search_data.truncated_right, |parent| {
2418 parent.child(truncated_contents_label())
2419 })
2420 .into_any_element();
2421
2422 let is_active = match self.selected_entry() {
2423 Some(PanelEntry::Search(SearchEntry {
2424 match_range: selected_match_range,
2425 ..
2426 })) => match_range == selected_match_range,
2427 _ => false,
2428 };
2429 Some(self.entry_element(
2430 PanelEntry::Search(SearchEntry {
2431 kind,
2432 match_range: match_range.clone(),
2433 render_data: render_data.clone(),
2434 }),
2435 ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
2436 depth,
2437 None,
2438 is_active,
2439 entire_label,
2440 window,
2441 cx,
2442 ))
2443 }
2444
2445 fn entry_element(
2446 &self,
2447 rendered_entry: PanelEntry,
2448 item_id: ElementId,
2449 depth: usize,
2450 icon_element: Option<AnyElement>,
2451 is_active: bool,
2452 label_element: gpui::AnyElement,
2453 window: &mut Window,
2454 cx: &mut Context<OutlinePanel>,
2455 ) -> Stateful<Div> {
2456 let settings = OutlinePanelSettings::get_global(cx);
2457 div()
2458 .text_ui(cx)
2459 .id(item_id.clone())
2460 .on_click({
2461 let clicked_entry = rendered_entry.clone();
2462 cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| {
2463 if event.down.button == MouseButton::Right || event.down.first_mouse {
2464 return;
2465 }
2466 let change_focus = event.down.click_count > 1;
2467 outline_panel.toggle_expanded(&clicked_entry, window, cx);
2468 outline_panel.scroll_editor_to_entry(
2469 &clicked_entry,
2470 true,
2471 change_focus,
2472 window,
2473 cx,
2474 );
2475 })
2476 })
2477 .cursor_pointer()
2478 .child(
2479 ListItem::new(item_id)
2480 .indent_level(depth)
2481 .indent_step_size(px(settings.indent_size))
2482 .toggle_state(is_active)
2483 .when_some(icon_element, |list_item, icon_element| {
2484 list_item.child(h_flex().child(icon_element))
2485 })
2486 .child(h_flex().h_6().child(label_element).ml_1())
2487 .on_secondary_mouse_down(cx.listener(
2488 move |outline_panel, event: &MouseDownEvent, window, cx| {
2489 // Stop propagation to prevent the catch-all context menu for the project
2490 // panel from being deployed.
2491 cx.stop_propagation();
2492 outline_panel.deploy_context_menu(
2493 event.position,
2494 rendered_entry.clone(),
2495 window,
2496 cx,
2497 )
2498 },
2499 )),
2500 )
2501 .border_1()
2502 .border_r_2()
2503 .rounded_none()
2504 .hover(|style| {
2505 if is_active {
2506 style
2507 } else {
2508 let hover_color = cx.theme().colors().ghost_element_hover;
2509 style.bg(hover_color).border_color(hover_color)
2510 }
2511 })
2512 .when(
2513 is_active && self.focus_handle.contains_focused(window, cx),
2514 |div| div.border_color(Color::Selected.color(cx)),
2515 )
2516 }
2517
2518 fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String {
2519 let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2520 Some(worktree) => {
2521 let worktree = worktree.read(cx);
2522 match worktree.snapshot().root_entry() {
2523 Some(root_entry) => {
2524 if root_entry.id == entry.id {
2525 file_name(worktree.abs_path().as_ref())
2526 } else {
2527 let path = worktree.absolutize(entry.path.as_ref()).ok();
2528 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2529 file_name(path)
2530 }
2531 }
2532 None => {
2533 let path = worktree.absolutize(entry.path.as_ref()).ok();
2534 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2535 file_name(path)
2536 }
2537 }
2538 }
2539 None => file_name(entry.path.as_ref()),
2540 };
2541 name
2542 }
2543
2544 fn update_fs_entries(
2545 &mut self,
2546 active_editor: Entity<Editor>,
2547 debounce: Option<Duration>,
2548 window: &mut Window,
2549 cx: &mut Context<Self>,
2550 ) {
2551 if !self.active {
2552 return;
2553 }
2554
2555 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2556 let active_multi_buffer = active_editor.read(cx).buffer().clone();
2557 let new_entries = self.new_entries_for_fs_update.clone();
2558 let repo_snapshots = self.project.update(cx, |project, cx| {
2559 project.git_store().read(cx).repo_snapshots(cx)
2560 });
2561 self.updating_fs_entries = true;
2562 self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
2563 if let Some(debounce) = debounce {
2564 cx.background_executor().timer(debounce).await;
2565 }
2566
2567 let mut new_collapsed_entries = HashSet::default();
2568 let mut new_unfolded_dirs = HashMap::default();
2569 let mut root_entries = HashSet::default();
2570 let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
2571 let Ok(buffer_excerpts) = outline_panel.update(cx, |outline_panel, cx| {
2572 let git_store = outline_panel.project.read(cx).git_store().clone();
2573 new_collapsed_entries = outline_panel.collapsed_entries.clone();
2574 new_unfolded_dirs = outline_panel.unfolded_dirs.clone();
2575 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
2576 let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
2577 HashMap::default(),
2578 |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
2579 let buffer_id = buffer_snapshot.remote_id();
2580 let file = File::from_dyn(buffer_snapshot.file());
2581 let entry_id = file.and_then(|file| file.project_entry_id(cx));
2582 let worktree = file.map(|file| file.worktree.read(cx).snapshot());
2583 let is_new = new_entries.contains(&excerpt_id)
2584 || !outline_panel.excerpts.contains_key(&buffer_id);
2585 let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
2586 let status = git_store
2587 .read(cx)
2588 .repository_and_path_for_buffer_id(buffer_id, cx)
2589 .and_then(|(repo, path)| {
2590 Some(repo.read(cx).status_for_path(&path)?.status)
2591 });
2592 buffer_excerpts
2593 .entry(buffer_id)
2594 .or_insert_with(|| {
2595 (is_new, is_folded, Vec::new(), entry_id, worktree, status)
2596 })
2597 .2
2598 .push(excerpt_id);
2599
2600 let outlines = match outline_panel
2601 .excerpts
2602 .get(&buffer_id)
2603 .and_then(|excerpts| excerpts.get(&excerpt_id))
2604 {
2605 Some(old_excerpt) => match &old_excerpt.outlines {
2606 ExcerptOutlines::Outlines(outlines) => {
2607 ExcerptOutlines::Outlines(outlines.clone())
2608 }
2609 ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
2610 ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
2611 },
2612 None => ExcerptOutlines::NotFetched,
2613 };
2614 new_excerpts.entry(buffer_id).or_default().insert(
2615 excerpt_id,
2616 Excerpt {
2617 range: excerpt_range,
2618 outlines,
2619 },
2620 );
2621 buffer_excerpts
2622 },
2623 );
2624 buffer_excerpts
2625 }) else {
2626 return;
2627 };
2628
2629 let Some((
2630 new_collapsed_entries,
2631 new_unfolded_dirs,
2632 new_fs_entries,
2633 new_depth_map,
2634 new_children_count,
2635 )) = cx
2636 .background_spawn(async move {
2637 let mut processed_external_buffers = HashSet::default();
2638 let mut new_worktree_entries =
2639 BTreeMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
2640 let mut worktree_excerpts = HashMap::<
2641 WorktreeId,
2642 HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
2643 >::default();
2644 let mut external_excerpts = HashMap::default();
2645
2646 for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree, status)) in
2647 buffer_excerpts
2648 {
2649 if is_folded {
2650 match &worktree {
2651 Some(worktree) => {
2652 new_collapsed_entries
2653 .insert(CollapsedEntry::File(worktree.id(), buffer_id));
2654 }
2655 None => {
2656 new_collapsed_entries
2657 .insert(CollapsedEntry::ExternalFile(buffer_id));
2658 }
2659 }
2660 } else if is_new {
2661 match &worktree {
2662 Some(worktree) => {
2663 new_collapsed_entries
2664 .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
2665 }
2666 None => {
2667 new_collapsed_entries
2668 .remove(&CollapsedEntry::ExternalFile(buffer_id));
2669 }
2670 }
2671 }
2672
2673 if let Some(worktree) = worktree {
2674 let worktree_id = worktree.id();
2675 let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
2676
2677 match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
2678 Some(entry) => {
2679 let entry = GitEntry {
2680 git_summary: status
2681 .map(|status| status.summary())
2682 .unwrap_or_default(),
2683 entry,
2684 };
2685 let mut traversal = GitTraversal::new(
2686 &repo_snapshots,
2687 worktree.traverse_from_path(
2688 true,
2689 true,
2690 true,
2691 entry.path.as_ref(),
2692 ),
2693 );
2694
2695 let mut entries_to_add = HashMap::default();
2696 worktree_excerpts
2697 .entry(worktree_id)
2698 .or_default()
2699 .insert(entry.id, (buffer_id, excerpts));
2700 let mut current_entry = entry;
2701 loop {
2702 if current_entry.is_dir() {
2703 let is_root =
2704 worktree.root_entry().map(|entry| entry.id)
2705 == Some(current_entry.id);
2706 if is_root {
2707 root_entries.insert(current_entry.id);
2708 if auto_fold_dirs {
2709 unfolded_dirs.insert(current_entry.id);
2710 }
2711 }
2712 if is_new {
2713 new_collapsed_entries.remove(&CollapsedEntry::Dir(
2714 worktree_id,
2715 current_entry.id,
2716 ));
2717 }
2718 }
2719
2720 let new_entry_added = entries_to_add
2721 .insert(current_entry.id, current_entry)
2722 .is_none();
2723 if new_entry_added && traversal.back_to_parent() {
2724 if let Some(parent_entry) = traversal.entry() {
2725 current_entry = parent_entry.to_owned();
2726 continue;
2727 }
2728 }
2729 break;
2730 }
2731 new_worktree_entries
2732 .entry(worktree_id)
2733 .or_insert_with(HashMap::default)
2734 .extend(entries_to_add);
2735 }
2736 None => {
2737 if processed_external_buffers.insert(buffer_id) {
2738 external_excerpts
2739 .entry(buffer_id)
2740 .or_insert_with(Vec::new)
2741 .extend(excerpts);
2742 }
2743 }
2744 }
2745 } else if processed_external_buffers.insert(buffer_id) {
2746 external_excerpts
2747 .entry(buffer_id)
2748 .or_insert_with(Vec::new)
2749 .extend(excerpts);
2750 }
2751 }
2752
2753 let mut new_children_count =
2754 HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
2755
2756 let worktree_entries = new_worktree_entries
2757 .into_iter()
2758 .map(|(worktree_id, entries)| {
2759 let mut entries = entries.into_values().collect::<Vec<_>>();
2760 entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
2761 (worktree_id, entries)
2762 })
2763 .flat_map(|(worktree_id, entries)| {
2764 {
2765 entries
2766 .into_iter()
2767 .filter_map(|entry| {
2768 if auto_fold_dirs {
2769 if let Some(parent) = entry.path.parent() {
2770 let children = new_children_count
2771 .entry(worktree_id)
2772 .or_default()
2773 .entry(Arc::from(parent))
2774 .or_default();
2775 if entry.is_dir() {
2776 children.dirs += 1;
2777 } else {
2778 children.files += 1;
2779 }
2780 }
2781 }
2782
2783 if entry.is_dir() {
2784 Some(FsEntry::Directory(FsEntryDirectory {
2785 worktree_id,
2786 entry,
2787 }))
2788 } else {
2789 let (buffer_id, excerpts) = worktree_excerpts
2790 .get_mut(&worktree_id)
2791 .and_then(|worktree_excerpts| {
2792 worktree_excerpts.remove(&entry.id)
2793 })?;
2794 Some(FsEntry::File(FsEntryFile {
2795 worktree_id,
2796 buffer_id,
2797 entry,
2798 excerpts,
2799 }))
2800 }
2801 })
2802 .collect::<Vec<_>>()
2803 }
2804 })
2805 .collect::<Vec<_>>();
2806
2807 let mut visited_dirs = Vec::new();
2808 let mut new_depth_map = HashMap::default();
2809 let new_visible_entries = external_excerpts
2810 .into_iter()
2811 .sorted_by_key(|(id, _)| *id)
2812 .map(|(buffer_id, excerpts)| {
2813 FsEntry::ExternalFile(FsEntryExternalFile {
2814 buffer_id,
2815 excerpts,
2816 })
2817 })
2818 .chain(worktree_entries)
2819 .filter(|visible_item| {
2820 match visible_item {
2821 FsEntry::Directory(directory) => {
2822 let parent_id = back_to_common_visited_parent(
2823 &mut visited_dirs,
2824 &directory.worktree_id,
2825 &directory.entry,
2826 );
2827
2828 let mut depth = 0;
2829 if !root_entries.contains(&directory.entry.id) {
2830 if auto_fold_dirs {
2831 let children = new_children_count
2832 .get(&directory.worktree_id)
2833 .and_then(|children_count| {
2834 children_count.get(&directory.entry.path)
2835 })
2836 .copied()
2837 .unwrap_or_default();
2838
2839 if !children.may_be_fold_part()
2840 || (children.dirs == 0
2841 && visited_dirs
2842 .last()
2843 .map(|(parent_dir_id, _)| {
2844 new_unfolded_dirs
2845 .get(&directory.worktree_id)
2846 .map_or(true, |unfolded_dirs| {
2847 unfolded_dirs
2848 .contains(parent_dir_id)
2849 })
2850 })
2851 .unwrap_or(true))
2852 {
2853 new_unfolded_dirs
2854 .entry(directory.worktree_id)
2855 .or_default()
2856 .insert(directory.entry.id);
2857 }
2858 }
2859
2860 depth = parent_id
2861 .and_then(|(worktree_id, id)| {
2862 new_depth_map.get(&(worktree_id, id)).copied()
2863 })
2864 .unwrap_or(0)
2865 + 1;
2866 };
2867 visited_dirs
2868 .push((directory.entry.id, directory.entry.path.clone()));
2869 new_depth_map
2870 .insert((directory.worktree_id, directory.entry.id), depth);
2871 }
2872 FsEntry::File(FsEntryFile {
2873 worktree_id,
2874 entry: file_entry,
2875 ..
2876 }) => {
2877 let parent_id = back_to_common_visited_parent(
2878 &mut visited_dirs,
2879 worktree_id,
2880 file_entry,
2881 );
2882 let depth = if root_entries.contains(&file_entry.id) {
2883 0
2884 } else {
2885 parent_id
2886 .and_then(|(worktree_id, id)| {
2887 new_depth_map.get(&(worktree_id, id)).copied()
2888 })
2889 .unwrap_or(0)
2890 + 1
2891 };
2892 new_depth_map.insert((*worktree_id, file_entry.id), depth);
2893 }
2894 FsEntry::ExternalFile(..) => {
2895 visited_dirs.clear();
2896 }
2897 }
2898
2899 true
2900 })
2901 .collect::<Vec<_>>();
2902
2903 anyhow::Ok((
2904 new_collapsed_entries,
2905 new_unfolded_dirs,
2906 new_visible_entries,
2907 new_depth_map,
2908 new_children_count,
2909 ))
2910 })
2911 .await
2912 .log_err()
2913 else {
2914 return;
2915 };
2916
2917 outline_panel
2918 .update_in(cx, |outline_panel, window, cx| {
2919 outline_panel.updating_fs_entries = false;
2920 outline_panel.new_entries_for_fs_update.clear();
2921 outline_panel.excerpts = new_excerpts;
2922 outline_panel.collapsed_entries = new_collapsed_entries;
2923 outline_panel.unfolded_dirs = new_unfolded_dirs;
2924 outline_panel.fs_entries = new_fs_entries;
2925 outline_panel.fs_entries_depth = new_depth_map;
2926 outline_panel.fs_children_count = new_children_count;
2927 outline_panel.update_non_fs_items(window, cx);
2928 outline_panel.update_cached_entries(debounce, window, cx);
2929
2930 cx.notify();
2931 })
2932 .ok();
2933 });
2934 }
2935
2936 fn replace_active_editor(
2937 &mut self,
2938 new_active_item: Box<dyn ItemHandle>,
2939 new_active_editor: Entity<Editor>,
2940 window: &mut Window,
2941 cx: &mut Context<Self>,
2942 ) {
2943 self.clear_previous(window, cx);
2944 let buffer_search_subscription = cx.subscribe_in(
2945 &new_active_editor,
2946 window,
2947 |outline_panel: &mut Self,
2948 _,
2949 e: &SearchEvent,
2950 window: &mut Window,
2951 cx: &mut Context<Self>| {
2952 if matches!(e, SearchEvent::MatchesInvalidated) {
2953 let update_cached_items = outline_panel.update_search_matches(window, cx);
2954 if update_cached_items {
2955 outline_panel.selected_entry.invalidate();
2956 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
2957 }
2958 };
2959 outline_panel.autoscroll(cx);
2960 },
2961 );
2962 self.active_item = Some(ActiveItem {
2963 _buffer_search_subscription: buffer_search_subscription,
2964 _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, window, cx),
2965 item_handle: new_active_item.downgrade_item(),
2966 active_editor: new_active_editor.downgrade(),
2967 });
2968 self.new_entries_for_fs_update
2969 .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
2970 self.selected_entry.invalidate();
2971 self.update_fs_entries(new_active_editor, None, window, cx);
2972 }
2973
2974 fn clear_previous(&mut self, window: &mut Window, cx: &mut App) {
2975 self.fs_entries_update_task = Task::ready(());
2976 self.outline_fetch_tasks.clear();
2977 self.cached_entries_update_task = Task::ready(());
2978 self.reveal_selection_task = Task::ready(Ok(()));
2979 self.filter_editor
2980 .update(cx, |editor, cx| editor.clear(window, cx));
2981 self.collapsed_entries.clear();
2982 self.unfolded_dirs.clear();
2983 self.active_item = None;
2984 self.fs_entries.clear();
2985 self.fs_entries_depth.clear();
2986 self.fs_children_count.clear();
2987 self.excerpts.clear();
2988 self.cached_entries = Vec::new();
2989 self.selected_entry = SelectedEntry::None;
2990 self.pinned = false;
2991 self.mode = ItemsDisplayMode::Outline;
2992 }
2993
2994 fn location_for_editor_selection(
2995 &self,
2996 editor: &Entity<Editor>,
2997 window: &mut Window,
2998 cx: &mut Context<Self>,
2999 ) -> Option<PanelEntry> {
3000 let selection = editor.update(cx, |editor, cx| {
3001 editor.selections.newest::<language::Point>(cx).head()
3002 });
3003 let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
3004 let multi_buffer = editor.read(cx).buffer();
3005 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
3006 let (excerpt_id, buffer, _) = editor
3007 .read(cx)
3008 .buffer()
3009 .read(cx)
3010 .excerpt_containing(selection, cx)?;
3011 let buffer_id = buffer.read(cx).remote_id();
3012
3013 if editor.read(cx).is_buffer_folded(buffer_id, cx) {
3014 return self
3015 .fs_entries
3016 .iter()
3017 .find(|fs_entry| match fs_entry {
3018 FsEntry::Directory(..) => false,
3019 FsEntry::File(FsEntryFile {
3020 buffer_id: other_buffer_id,
3021 ..
3022 })
3023 | FsEntry::ExternalFile(FsEntryExternalFile {
3024 buffer_id: other_buffer_id,
3025 ..
3026 }) => buffer_id == *other_buffer_id,
3027 })
3028 .cloned()
3029 .map(PanelEntry::Fs);
3030 }
3031
3032 let selection_display_point = selection.to_display_point(&editor_snapshot);
3033
3034 match &self.mode {
3035 ItemsDisplayMode::Search(search_state) => search_state
3036 .matches
3037 .iter()
3038 .rev()
3039 .min_by_key(|&(match_range, _)| {
3040 let match_display_range =
3041 match_range.clone().to_display_points(&editor_snapshot);
3042 let start_distance = if selection_display_point < match_display_range.start {
3043 match_display_range.start - selection_display_point
3044 } else {
3045 selection_display_point - match_display_range.start
3046 };
3047 let end_distance = if selection_display_point < match_display_range.end {
3048 match_display_range.end - selection_display_point
3049 } else {
3050 selection_display_point - match_display_range.end
3051 };
3052 start_distance + end_distance
3053 })
3054 .and_then(|(closest_range, _)| {
3055 self.cached_entries.iter().find_map(|cached_entry| {
3056 if let PanelEntry::Search(SearchEntry { match_range, .. }) =
3057 &cached_entry.entry
3058 {
3059 if match_range == closest_range {
3060 Some(cached_entry.entry.clone())
3061 } else {
3062 None
3063 }
3064 } else {
3065 None
3066 }
3067 })
3068 }),
3069 ItemsDisplayMode::Outline => self.outline_location(
3070 buffer_id,
3071 excerpt_id,
3072 multi_buffer_snapshot,
3073 editor_snapshot,
3074 selection_display_point,
3075 ),
3076 }
3077 }
3078
3079 fn outline_location(
3080 &self,
3081 buffer_id: BufferId,
3082 excerpt_id: ExcerptId,
3083 multi_buffer_snapshot: editor::MultiBufferSnapshot,
3084 editor_snapshot: editor::EditorSnapshot,
3085 selection_display_point: DisplayPoint,
3086 ) -> Option<PanelEntry> {
3087 let excerpt_outlines = self
3088 .excerpts
3089 .get(&buffer_id)
3090 .and_then(|excerpts| excerpts.get(&excerpt_id))
3091 .into_iter()
3092 .flat_map(|excerpt| excerpt.iter_outlines())
3093 .flat_map(|outline| {
3094 let start = multi_buffer_snapshot
3095 .anchor_in_excerpt(excerpt_id, outline.range.start)?
3096 .to_display_point(&editor_snapshot);
3097 let end = multi_buffer_snapshot
3098 .anchor_in_excerpt(excerpt_id, outline.range.end)?
3099 .to_display_point(&editor_snapshot);
3100 Some((start..end, outline))
3101 })
3102 .collect::<Vec<_>>();
3103
3104 let mut matching_outline_indices = Vec::new();
3105 let mut children = HashMap::default();
3106 let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
3107
3108 for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
3109 if outline_range
3110 .to_inclusive()
3111 .contains(&selection_display_point)
3112 {
3113 matching_outline_indices.push(i);
3114 } else if (outline_range.start.row()..outline_range.end.row())
3115 .to_inclusive()
3116 .contains(&selection_display_point.row())
3117 {
3118 matching_outline_indices.push(i);
3119 }
3120
3121 while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
3122 if parent_outline.depth >= outline.depth
3123 || !parent_range.contains(&outline_range.start)
3124 {
3125 parents_stack.pop();
3126 } else {
3127 break;
3128 }
3129 }
3130 if let Some((_, _, parent_index)) = parents_stack.last_mut() {
3131 children
3132 .entry(*parent_index)
3133 .or_insert_with(Vec::new)
3134 .push(i);
3135 }
3136 parents_stack.push((outline_range, outline, i));
3137 }
3138
3139 let outline_item = matching_outline_indices
3140 .into_iter()
3141 .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
3142 .filter(|(i, _)| {
3143 children
3144 .get(i)
3145 .map(|children| {
3146 children.iter().all(|child_index| {
3147 excerpt_outlines
3148 .get(*child_index)
3149 .map(|(child_range, _)| child_range.start > selection_display_point)
3150 .unwrap_or(false)
3151 })
3152 })
3153 .unwrap_or(true)
3154 })
3155 .min_by_key(|(_, (outline_range, outline))| {
3156 let distance_from_start = if outline_range.start > selection_display_point {
3157 outline_range.start - selection_display_point
3158 } else {
3159 selection_display_point - outline_range.start
3160 };
3161 let distance_from_end = if outline_range.end > selection_display_point {
3162 outline_range.end - selection_display_point
3163 } else {
3164 selection_display_point - outline_range.end
3165 };
3166
3167 (
3168 cmp::Reverse(outline.depth),
3169 distance_from_start + distance_from_end,
3170 )
3171 })
3172 .map(|(_, (_, outline))| *outline)
3173 .cloned();
3174
3175 let closest_container = match outline_item {
3176 Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
3177 buffer_id,
3178 excerpt_id,
3179 outline,
3180 })),
3181 None => {
3182 self.cached_entries.iter().rev().find_map(|cached_entry| {
3183 match &cached_entry.entry {
3184 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
3185 if excerpt.buffer_id == buffer_id && excerpt.id == excerpt_id {
3186 Some(cached_entry.entry.clone())
3187 } else {
3188 None
3189 }
3190 }
3191 PanelEntry::Fs(
3192 FsEntry::ExternalFile(FsEntryExternalFile {
3193 buffer_id: file_buffer_id,
3194 excerpts: file_excerpts,
3195 })
3196 | FsEntry::File(FsEntryFile {
3197 buffer_id: file_buffer_id,
3198 excerpts: file_excerpts,
3199 ..
3200 }),
3201 ) => {
3202 if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
3203 Some(cached_entry.entry.clone())
3204 } else {
3205 None
3206 }
3207 }
3208 _ => None,
3209 }
3210 })?
3211 }
3212 };
3213 Some(closest_container)
3214 }
3215
3216 fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3217 let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
3218 if excerpt_fetch_ranges.is_empty() {
3219 return;
3220 }
3221
3222 let syntax_theme = cx.theme().syntax().clone();
3223 let first_update = Arc::new(AtomicBool::new(true));
3224 for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
3225 for (excerpt_id, excerpt_range) in excerpt_ranges {
3226 let syntax_theme = syntax_theme.clone();
3227 let buffer_snapshot = buffer_snapshot.clone();
3228 let first_update = first_update.clone();
3229 self.outline_fetch_tasks.insert(
3230 (buffer_id, excerpt_id),
3231 cx.spawn_in(window, async move |outline_panel, cx| {
3232 let fetched_outlines = cx
3233 .background_spawn(async move {
3234 buffer_snapshot
3235 .outline_items_containing(
3236 excerpt_range.context,
3237 false,
3238 Some(&syntax_theme),
3239 )
3240 .unwrap_or_default()
3241 })
3242 .await;
3243 outline_panel
3244 .update_in(cx, |outline_panel, window, cx| {
3245 if let Some(excerpt) = outline_panel
3246 .excerpts
3247 .entry(buffer_id)
3248 .or_default()
3249 .get_mut(&excerpt_id)
3250 {
3251 let debounce = if first_update
3252 .fetch_and(false, atomic::Ordering::AcqRel)
3253 {
3254 None
3255 } else {
3256 Some(UPDATE_DEBOUNCE)
3257 };
3258 excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
3259 outline_panel.update_cached_entries(debounce, window, cx);
3260 }
3261 })
3262 .ok();
3263 }),
3264 );
3265 }
3266 }
3267 }
3268
3269 fn is_singleton_active(&self, cx: &App) -> bool {
3270 self.active_editor().map_or(false, |active_editor| {
3271 active_editor.read(cx).buffer().read(cx).is_singleton()
3272 })
3273 }
3274
3275 fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
3276 self.outline_fetch_tasks.clear();
3277 let mut ids = ids.iter().collect::<HashSet<_>>();
3278 for excerpts in self.excerpts.values_mut() {
3279 ids.retain(|id| {
3280 if let Some(excerpt) = excerpts.get_mut(id) {
3281 excerpt.invalidate_outlines();
3282 false
3283 } else {
3284 true
3285 }
3286 });
3287 if ids.is_empty() {
3288 break;
3289 }
3290 }
3291 }
3292
3293 fn excerpt_fetch_ranges(
3294 &self,
3295 cx: &App,
3296 ) -> HashMap<
3297 BufferId,
3298 (
3299 BufferSnapshot,
3300 HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
3301 ),
3302 > {
3303 self.fs_entries
3304 .iter()
3305 .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
3306 match fs_entry {
3307 FsEntry::File(FsEntryFile {
3308 buffer_id,
3309 excerpts: file_excerpts,
3310 ..
3311 })
3312 | FsEntry::ExternalFile(FsEntryExternalFile {
3313 buffer_id,
3314 excerpts: file_excerpts,
3315 }) => {
3316 let excerpts = self.excerpts.get(buffer_id);
3317 for &file_excerpt in file_excerpts {
3318 if let Some(excerpt) = excerpts
3319 .and_then(|excerpts| excerpts.get(&file_excerpt))
3320 .filter(|excerpt| excerpt.should_fetch_outlines())
3321 {
3322 match excerpts_to_fetch.entry(*buffer_id) {
3323 hash_map::Entry::Occupied(mut o) => {
3324 o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
3325 }
3326 hash_map::Entry::Vacant(v) => {
3327 if let Some(buffer_snapshot) =
3328 self.buffer_snapshot_for_id(*buffer_id, cx)
3329 {
3330 v.insert((buffer_snapshot, HashMap::default()))
3331 .1
3332 .insert(file_excerpt, excerpt.range.clone());
3333 }
3334 }
3335 }
3336 }
3337 }
3338 }
3339 FsEntry::Directory(..) => {}
3340 }
3341 excerpts_to_fetch
3342 })
3343 }
3344
3345 fn buffer_snapshot_for_id(&self, buffer_id: BufferId, cx: &App) -> Option<BufferSnapshot> {
3346 let editor = self.active_editor()?;
3347 Some(
3348 editor
3349 .read(cx)
3350 .buffer()
3351 .read(cx)
3352 .buffer(buffer_id)?
3353 .read(cx)
3354 .snapshot(),
3355 )
3356 }
3357
3358 fn abs_path(&self, entry: &PanelEntry, cx: &App) -> Option<PathBuf> {
3359 match entry {
3360 PanelEntry::Fs(
3361 FsEntry::File(FsEntryFile { buffer_id, .. })
3362 | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }),
3363 ) => self
3364 .buffer_snapshot_for_id(*buffer_id, cx)
3365 .and_then(|buffer_snapshot| {
3366 let file = File::from_dyn(buffer_snapshot.file())?;
3367 file.worktree.read(cx).absolutize(&file.path).ok()
3368 }),
3369 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3370 worktree_id, entry, ..
3371 })) => self
3372 .project
3373 .read(cx)
3374 .worktree_for_id(*worktree_id, cx)?
3375 .read(cx)
3376 .absolutize(&entry.path)
3377 .ok(),
3378 PanelEntry::FoldedDirs(FoldedDirsEntry {
3379 worktree_id,
3380 entries: dirs,
3381 ..
3382 }) => dirs.last().and_then(|entry| {
3383 self.project
3384 .read(cx)
3385 .worktree_for_id(*worktree_id, cx)
3386 .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
3387 }),
3388 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
3389 }
3390 }
3391
3392 fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<Path>> {
3393 match entry {
3394 FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3395 let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
3396 Some(buffer_snapshot.file()?.path().clone())
3397 }
3398 FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()),
3399 FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()),
3400 }
3401 }
3402
3403 fn update_cached_entries(
3404 &mut self,
3405 debounce: Option<Duration>,
3406 window: &mut Window,
3407 cx: &mut Context<OutlinePanel>,
3408 ) {
3409 if !self.active {
3410 return;
3411 }
3412
3413 let is_singleton = self.is_singleton_active(cx);
3414 let query = self.query(cx);
3415 self.updating_cached_entries = true;
3416 self.cached_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
3417 if let Some(debounce) = debounce {
3418 cx.background_executor().timer(debounce).await;
3419 }
3420 let Some(new_cached_entries) = outline_panel
3421 .update_in(cx, |outline_panel, window, cx| {
3422 outline_panel.generate_cached_entries(is_singleton, query, window, cx)
3423 })
3424 .ok()
3425 else {
3426 return;
3427 };
3428 let (new_cached_entries, max_width_item_index) = new_cached_entries.await;
3429 outline_panel
3430 .update_in(cx, |outline_panel, window, cx| {
3431 outline_panel.cached_entries = new_cached_entries;
3432 outline_panel.max_width_item_index = max_width_item_index;
3433 if outline_panel.selected_entry.is_invalidated()
3434 || matches!(outline_panel.selected_entry, SelectedEntry::None)
3435 {
3436 if let Some(new_selected_entry) =
3437 outline_panel.active_editor().and_then(|active_editor| {
3438 outline_panel.location_for_editor_selection(
3439 &active_editor,
3440 window,
3441 cx,
3442 )
3443 })
3444 {
3445 outline_panel.select_entry(new_selected_entry, false, window, cx);
3446 }
3447 }
3448
3449 outline_panel.autoscroll(cx);
3450 outline_panel.updating_cached_entries = false;
3451 cx.notify();
3452 })
3453 .ok();
3454 });
3455 }
3456
3457 fn generate_cached_entries(
3458 &self,
3459 is_singleton: bool,
3460 query: Option<String>,
3461 window: &mut Window,
3462 cx: &mut Context<Self>,
3463 ) -> Task<(Vec<CachedEntry>, Option<usize>)> {
3464 let project = self.project.clone();
3465 let Some(active_editor) = self.active_editor() else {
3466 return Task::ready((Vec::new(), None));
3467 };
3468 cx.spawn_in(window, async move |outline_panel, cx| {
3469 let mut generation_state = GenerationState::default();
3470
3471 let Ok(()) = outline_panel.update(cx, |outline_panel, cx| {
3472 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
3473 let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>;
3474 let track_matches = query.is_some();
3475
3476 #[derive(Debug)]
3477 struct ParentStats {
3478 path: Arc<Path>,
3479 folded: bool,
3480 expanded: bool,
3481 depth: usize,
3482 }
3483 let mut parent_dirs = Vec::<ParentStats>::new();
3484 for entry in outline_panel.fs_entries.clone() {
3485 let is_expanded = outline_panel.is_expanded(&entry);
3486 let (depth, should_add) = match &entry {
3487 FsEntry::Directory(directory_entry) => {
3488 let mut should_add = true;
3489 let is_root = project
3490 .read(cx)
3491 .worktree_for_id(directory_entry.worktree_id, cx)
3492 .map_or(false, |worktree| {
3493 worktree.read(cx).root_entry() == Some(&directory_entry.entry)
3494 });
3495 let folded = auto_fold_dirs
3496 && !is_root
3497 && outline_panel
3498 .unfolded_dirs
3499 .get(&directory_entry.worktree_id)
3500 .map_or(true, |unfolded_dirs| {
3501 !unfolded_dirs.contains(&directory_entry.entry.id)
3502 });
3503 let fs_depth = outline_panel
3504 .fs_entries_depth
3505 .get(&(directory_entry.worktree_id, directory_entry.entry.id))
3506 .copied()
3507 .unwrap_or(0);
3508 while let Some(parent) = parent_dirs.last() {
3509 if !is_root && directory_entry.entry.path.starts_with(&parent.path)
3510 {
3511 break;
3512 }
3513 parent_dirs.pop();
3514 }
3515 let auto_fold = match parent_dirs.last() {
3516 Some(parent) => {
3517 parent.folded
3518 && Some(parent.path.as_ref())
3519 == directory_entry.entry.path.parent()
3520 && outline_panel
3521 .fs_children_count
3522 .get(&directory_entry.worktree_id)
3523 .and_then(|entries| {
3524 entries.get(&directory_entry.entry.path)
3525 })
3526 .copied()
3527 .unwrap_or_default()
3528 .may_be_fold_part()
3529 }
3530 None => false,
3531 };
3532 let folded = folded || auto_fold;
3533 let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
3534 Some(parent) => {
3535 let parent_folded = parent.folded;
3536 let parent_expanded = parent.expanded;
3537 let new_depth = if parent_folded {
3538 parent.depth
3539 } else {
3540 parent.depth + 1
3541 };
3542 parent_dirs.push(ParentStats {
3543 path: directory_entry.entry.path.clone(),
3544 folded,
3545 expanded: parent_expanded && is_expanded,
3546 depth: new_depth,
3547 });
3548 (new_depth, parent_expanded, parent_folded)
3549 }
3550 None => {
3551 parent_dirs.push(ParentStats {
3552 path: directory_entry.entry.path.clone(),
3553 folded,
3554 expanded: is_expanded,
3555 depth: fs_depth,
3556 });
3557 (fs_depth, true, false)
3558 }
3559 };
3560
3561 if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take()
3562 {
3563 if folded
3564 && directory_entry.worktree_id == folded_dirs.worktree_id
3565 && directory_entry.entry.path.parent()
3566 == folded_dirs
3567 .entries
3568 .last()
3569 .map(|entry| entry.path.as_ref())
3570 {
3571 folded_dirs.entries.push(directory_entry.entry.clone());
3572 folded_dirs_entry = Some((folded_depth, folded_dirs))
3573 } else {
3574 if !is_singleton {
3575 let start_of_collapsed_dir_sequence = !parent_expanded
3576 && parent_dirs
3577 .iter()
3578 .rev()
3579 .nth(folded_dirs.entries.len() + 1)
3580 .map_or(true, |parent| parent.expanded);
3581 if start_of_collapsed_dir_sequence
3582 || parent_expanded
3583 || query.is_some()
3584 {
3585 if parent_folded {
3586 folded_dirs
3587 .entries
3588 .push(directory_entry.entry.clone());
3589 should_add = false;
3590 }
3591 let new_folded_dirs =
3592 PanelEntry::FoldedDirs(folded_dirs.clone());
3593 outline_panel.push_entry(
3594 &mut generation_state,
3595 track_matches,
3596 new_folded_dirs,
3597 folded_depth,
3598 cx,
3599 );
3600 }
3601 }
3602
3603 folded_dirs_entry = if parent_folded {
3604 None
3605 } else {
3606 Some((
3607 depth,
3608 FoldedDirsEntry {
3609 worktree_id: directory_entry.worktree_id,
3610 entries: vec![directory_entry.entry.clone()],
3611 },
3612 ))
3613 };
3614 }
3615 } else if folded {
3616 folded_dirs_entry = Some((
3617 depth,
3618 FoldedDirsEntry {
3619 worktree_id: directory_entry.worktree_id,
3620 entries: vec![directory_entry.entry.clone()],
3621 },
3622 ));
3623 }
3624
3625 let should_add =
3626 should_add && parent_expanded && folded_dirs_entry.is_none();
3627 (depth, should_add)
3628 }
3629 FsEntry::ExternalFile(..) => {
3630 if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() {
3631 let parent_expanded = parent_dirs
3632 .iter()
3633 .rev()
3634 .find(|parent| {
3635 folded_dir
3636 .entries
3637 .iter()
3638 .all(|entry| entry.path != parent.path)
3639 })
3640 .map_or(true, |parent| parent.expanded);
3641 if !is_singleton && (parent_expanded || query.is_some()) {
3642 outline_panel.push_entry(
3643 &mut generation_state,
3644 track_matches,
3645 PanelEntry::FoldedDirs(folded_dir),
3646 folded_depth,
3647 cx,
3648 );
3649 }
3650 }
3651 parent_dirs.clear();
3652 (0, true)
3653 }
3654 FsEntry::File(file) => {
3655 if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3656 let parent_expanded = parent_dirs
3657 .iter()
3658 .rev()
3659 .find(|parent| {
3660 folded_dirs
3661 .entries
3662 .iter()
3663 .all(|entry| entry.path != parent.path)
3664 })
3665 .map_or(true, |parent| parent.expanded);
3666 if !is_singleton && (parent_expanded || query.is_some()) {
3667 outline_panel.push_entry(
3668 &mut generation_state,
3669 track_matches,
3670 PanelEntry::FoldedDirs(folded_dirs),
3671 folded_depth,
3672 cx,
3673 );
3674 }
3675 }
3676
3677 let fs_depth = outline_panel
3678 .fs_entries_depth
3679 .get(&(file.worktree_id, file.entry.id))
3680 .copied()
3681 .unwrap_or(0);
3682 while let Some(parent) = parent_dirs.last() {
3683 if file.entry.path.starts_with(&parent.path) {
3684 break;
3685 }
3686 parent_dirs.pop();
3687 }
3688 match parent_dirs.last() {
3689 Some(parent) => {
3690 let new_depth = parent.depth + 1;
3691 (new_depth, parent.expanded)
3692 }
3693 None => (fs_depth, true),
3694 }
3695 }
3696 };
3697
3698 if !is_singleton
3699 && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3700 {
3701 outline_panel.push_entry(
3702 &mut generation_state,
3703 track_matches,
3704 PanelEntry::Fs(entry.clone()),
3705 depth,
3706 cx,
3707 );
3708 }
3709
3710 match outline_panel.mode {
3711 ItemsDisplayMode::Search(_) => {
3712 if is_singleton || query.is_some() || (should_add && is_expanded) {
3713 outline_panel.add_search_entries(
3714 &mut generation_state,
3715 &active_editor,
3716 entry.clone(),
3717 depth,
3718 query.clone(),
3719 is_singleton,
3720 cx,
3721 );
3722 }
3723 }
3724 ItemsDisplayMode::Outline => {
3725 let excerpts_to_consider =
3726 if is_singleton || query.is_some() || (should_add && is_expanded) {
3727 match &entry {
3728 FsEntry::File(FsEntryFile {
3729 buffer_id,
3730 excerpts,
3731 ..
3732 })
3733 | FsEntry::ExternalFile(FsEntryExternalFile {
3734 buffer_id,
3735 excerpts,
3736 ..
3737 }) => Some((*buffer_id, excerpts)),
3738 _ => None,
3739 }
3740 } else {
3741 None
3742 };
3743 if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
3744 if !active_editor.read(cx).is_buffer_folded(buffer_id, cx) {
3745 outline_panel.add_excerpt_entries(
3746 &mut generation_state,
3747 buffer_id,
3748 entry_excerpts,
3749 depth,
3750 track_matches,
3751 is_singleton,
3752 query.as_deref(),
3753 cx,
3754 );
3755 }
3756 }
3757 }
3758 }
3759
3760 if is_singleton
3761 && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
3762 && !generation_state.entries.iter().any(|item| {
3763 matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
3764 })
3765 {
3766 outline_panel.push_entry(
3767 &mut generation_state,
3768 track_matches,
3769 PanelEntry::Fs(entry.clone()),
3770 0,
3771 cx,
3772 );
3773 }
3774 }
3775
3776 if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3777 let parent_expanded = parent_dirs
3778 .iter()
3779 .rev()
3780 .find(|parent| {
3781 folded_dirs
3782 .entries
3783 .iter()
3784 .all(|entry| entry.path != parent.path)
3785 })
3786 .map_or(true, |parent| parent.expanded);
3787 if parent_expanded || query.is_some() {
3788 outline_panel.push_entry(
3789 &mut generation_state,
3790 track_matches,
3791 PanelEntry::FoldedDirs(folded_dirs),
3792 folded_depth,
3793 cx,
3794 );
3795 }
3796 }
3797 }) else {
3798 return (Vec::new(), None);
3799 };
3800
3801 let Some(query) = query else {
3802 return (
3803 generation_state.entries,
3804 generation_state
3805 .max_width_estimate_and_index
3806 .map(|(_, index)| index),
3807 );
3808 };
3809
3810 let mut matched_ids = match_strings(
3811 &generation_state.match_candidates,
3812 &query,
3813 true,
3814 usize::MAX,
3815 &AtomicBool::default(),
3816 cx.background_executor().clone(),
3817 )
3818 .await
3819 .into_iter()
3820 .map(|string_match| (string_match.candidate_id, string_match))
3821 .collect::<HashMap<_, _>>();
3822
3823 let mut id = 0;
3824 generation_state.entries.retain_mut(|cached_entry| {
3825 let retain = match matched_ids.remove(&id) {
3826 Some(string_match) => {
3827 cached_entry.string_match = Some(string_match);
3828 true
3829 }
3830 None => false,
3831 };
3832 id += 1;
3833 retain
3834 });
3835
3836 (
3837 generation_state.entries,
3838 generation_state
3839 .max_width_estimate_and_index
3840 .map(|(_, index)| index),
3841 )
3842 })
3843 }
3844
3845 fn push_entry(
3846 &self,
3847 state: &mut GenerationState,
3848 track_matches: bool,
3849 entry: PanelEntry,
3850 depth: usize,
3851 cx: &mut App,
3852 ) {
3853 let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry {
3854 match folded_dirs_entry.entries.len() {
3855 0 => {
3856 debug_panic!("Empty folded dirs receiver");
3857 return;
3858 }
3859 1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3860 worktree_id: folded_dirs_entry.worktree_id,
3861 entry: folded_dirs_entry.entries[0].clone(),
3862 })),
3863 _ => entry,
3864 }
3865 } else {
3866 entry
3867 };
3868
3869 if track_matches {
3870 let id = state.entries.len();
3871 match &entry {
3872 PanelEntry::Fs(fs_entry) => {
3873 if let Some(file_name) =
3874 self.relative_path(fs_entry, cx).as_deref().map(file_name)
3875 {
3876 state
3877 .match_candidates
3878 .push(StringMatchCandidate::new(id, &file_name));
3879 }
3880 }
3881 PanelEntry::FoldedDirs(folded_dir_entry) => {
3882 let dir_names = self.dir_names_string(
3883 &folded_dir_entry.entries,
3884 folded_dir_entry.worktree_id,
3885 cx,
3886 );
3887 {
3888 state
3889 .match_candidates
3890 .push(StringMatchCandidate::new(id, &dir_names));
3891 }
3892 }
3893 PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state
3894 .match_candidates
3895 .push(StringMatchCandidate::new(id, &outline_entry.outline.text)),
3896 PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {}
3897 PanelEntry::Search(new_search_entry) => {
3898 if let Some(search_data) = new_search_entry.render_data.get() {
3899 state
3900 .match_candidates
3901 .push(StringMatchCandidate::new(id, &search_data.context_text));
3902 }
3903 }
3904 }
3905 }
3906
3907 let width_estimate = self.width_estimate(depth, &entry, cx);
3908 if Some(width_estimate)
3909 > state
3910 .max_width_estimate_and_index
3911 .map(|(estimate, _)| estimate)
3912 {
3913 state.max_width_estimate_and_index = Some((width_estimate, state.entries.len()));
3914 }
3915 state.entries.push(CachedEntry {
3916 depth,
3917 entry,
3918 string_match: None,
3919 });
3920 }
3921
3922 fn dir_names_string(&self, entries: &[GitEntry], worktree_id: WorktreeId, cx: &App) -> String {
3923 let dir_names_segment = entries
3924 .iter()
3925 .map(|entry| self.entry_name(&worktree_id, entry, cx))
3926 .collect::<PathBuf>();
3927 dir_names_segment.to_string_lossy().to_string()
3928 }
3929
3930 fn query(&self, cx: &App) -> Option<String> {
3931 let query = self.filter_editor.read(cx).text(cx);
3932 if query.trim().is_empty() {
3933 None
3934 } else {
3935 Some(query)
3936 }
3937 }
3938
3939 fn is_expanded(&self, entry: &FsEntry) -> bool {
3940 let entry_to_check = match entry {
3941 FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3942 CollapsedEntry::ExternalFile(*buffer_id)
3943 }
3944 FsEntry::File(FsEntryFile {
3945 worktree_id,
3946 buffer_id,
3947 ..
3948 }) => CollapsedEntry::File(*worktree_id, *buffer_id),
3949 FsEntry::Directory(FsEntryDirectory {
3950 worktree_id, entry, ..
3951 }) => CollapsedEntry::Dir(*worktree_id, entry.id),
3952 };
3953 !self.collapsed_entries.contains(&entry_to_check)
3954 }
3955
3956 fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) -> bool {
3957 if !self.active {
3958 return false;
3959 }
3960
3961 let mut update_cached_items = false;
3962 update_cached_items |= self.update_search_matches(window, cx);
3963 self.fetch_outdated_outlines(window, cx);
3964 if update_cached_items {
3965 self.selected_entry.invalidate();
3966 }
3967 update_cached_items
3968 }
3969
3970 fn update_search_matches(
3971 &mut self,
3972 window: &mut Window,
3973 cx: &mut Context<OutlinePanel>,
3974 ) -> bool {
3975 if !self.active {
3976 return false;
3977 }
3978
3979 let project_search = self
3980 .active_item()
3981 .and_then(|item| item.downcast::<ProjectSearchView>());
3982 let project_search_matches = project_search
3983 .as_ref()
3984 .map(|project_search| project_search.read(cx).get_matches(cx))
3985 .unwrap_or_default();
3986
3987 let buffer_search = self
3988 .active_item()
3989 .as_deref()
3990 .and_then(|active_item| {
3991 self.workspace
3992 .upgrade()
3993 .and_then(|workspace| workspace.read(cx).pane_for(active_item))
3994 })
3995 .and_then(|pane| {
3996 pane.read(cx)
3997 .toolbar()
3998 .read(cx)
3999 .item_of_type::<BufferSearchBar>()
4000 });
4001 let buffer_search_matches = self
4002 .active_editor()
4003 .map(|active_editor| {
4004 active_editor.update(cx, |editor, cx| editor.get_matches(window, cx))
4005 })
4006 .unwrap_or_default();
4007
4008 let mut update_cached_entries = false;
4009 if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
4010 if matches!(self.mode, ItemsDisplayMode::Search(_)) {
4011 self.mode = ItemsDisplayMode::Outline;
4012 update_cached_entries = true;
4013 }
4014 } else {
4015 let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
4016 (
4017 SearchKind::Project,
4018 project_search_matches,
4019 project_search
4020 .map(|project_search| project_search.read(cx).search_query_text(cx))
4021 .unwrap_or_default(),
4022 )
4023 } else {
4024 (
4025 SearchKind::Buffer,
4026 buffer_search_matches,
4027 buffer_search
4028 .map(|buffer_search| buffer_search.read(cx).query(cx))
4029 .unwrap_or_default(),
4030 )
4031 };
4032
4033 let mut previous_matches = HashMap::default();
4034 update_cached_entries = match &mut self.mode {
4035 ItemsDisplayMode::Search(current_search_state) => {
4036 let update = current_search_state.query != new_search_query
4037 || current_search_state.kind != kind
4038 || current_search_state.matches.is_empty()
4039 || current_search_state.matches.iter().enumerate().any(
4040 |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
4041 );
4042 if current_search_state.kind == kind {
4043 previous_matches.extend(current_search_state.matches.drain(..));
4044 }
4045 update
4046 }
4047 ItemsDisplayMode::Outline => true,
4048 };
4049 self.mode = ItemsDisplayMode::Search(SearchState::new(
4050 kind,
4051 new_search_query,
4052 previous_matches,
4053 new_search_matches,
4054 cx.theme().syntax().clone(),
4055 window,
4056 cx,
4057 ));
4058 }
4059 update_cached_entries
4060 }
4061
4062 fn add_excerpt_entries(
4063 &self,
4064 state: &mut GenerationState,
4065 buffer_id: BufferId,
4066 entries_to_add: &[ExcerptId],
4067 parent_depth: usize,
4068 track_matches: bool,
4069 is_singleton: bool,
4070 query: Option<&str>,
4071 cx: &mut Context<Self>,
4072 ) {
4073 if let Some(excerpts) = self.excerpts.get(&buffer_id) {
4074 for &excerpt_id in entries_to_add {
4075 let Some(excerpt) = excerpts.get(&excerpt_id) else {
4076 continue;
4077 };
4078 let excerpt_depth = parent_depth + 1;
4079 self.push_entry(
4080 state,
4081 track_matches,
4082 PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
4083 buffer_id,
4084 id: excerpt_id,
4085 range: excerpt.range.clone(),
4086 })),
4087 excerpt_depth,
4088 cx,
4089 );
4090
4091 let mut outline_base_depth = excerpt_depth + 1;
4092 if is_singleton {
4093 outline_base_depth = 0;
4094 state.clear();
4095 } else if query.is_none()
4096 && self
4097 .collapsed_entries
4098 .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
4099 {
4100 continue;
4101 }
4102
4103 for outline in excerpt.iter_outlines() {
4104 self.push_entry(
4105 state,
4106 track_matches,
4107 PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
4108 buffer_id,
4109 excerpt_id,
4110 outline: outline.clone(),
4111 })),
4112 outline_base_depth + outline.depth,
4113 cx,
4114 );
4115 }
4116 }
4117 }
4118 }
4119
4120 fn add_search_entries(
4121 &mut self,
4122 state: &mut GenerationState,
4123 active_editor: &Entity<Editor>,
4124 parent_entry: FsEntry,
4125 parent_depth: usize,
4126 filter_query: Option<String>,
4127 is_singleton: bool,
4128 cx: &mut Context<Self>,
4129 ) {
4130 let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
4131 return;
4132 };
4133
4134 let kind = search_state.kind;
4135 let related_excerpts = match &parent_entry {
4136 FsEntry::Directory(_) => return,
4137 FsEntry::ExternalFile(external) => &external.excerpts,
4138 FsEntry::File(file) => &file.excerpts,
4139 }
4140 .iter()
4141 .copied()
4142 .collect::<HashSet<_>>();
4143
4144 let depth = if is_singleton { 0 } else { parent_depth + 1 };
4145 let new_search_matches = search_state
4146 .matches
4147 .iter()
4148 .filter(|(match_range, _)| {
4149 related_excerpts.contains(&match_range.start.excerpt_id)
4150 || related_excerpts.contains(&match_range.end.excerpt_id)
4151 })
4152 .filter(|(match_range, _)| {
4153 let editor = active_editor.read(cx);
4154 if let Some(buffer_id) = match_range.start.buffer_id {
4155 if editor.is_buffer_folded(buffer_id, cx) {
4156 return false;
4157 }
4158 }
4159 if let Some(buffer_id) = match_range.start.buffer_id {
4160 if editor.is_buffer_folded(buffer_id, cx) {
4161 return false;
4162 }
4163 }
4164 true
4165 });
4166
4167 let new_search_entries = new_search_matches
4168 .map(|(match_range, search_data)| SearchEntry {
4169 match_range: match_range.clone(),
4170 kind,
4171 render_data: Arc::clone(search_data),
4172 })
4173 .collect::<Vec<_>>();
4174 for new_search_entry in new_search_entries {
4175 self.push_entry(
4176 state,
4177 filter_query.is_some(),
4178 PanelEntry::Search(new_search_entry),
4179 depth,
4180 cx,
4181 );
4182 }
4183 }
4184
4185 fn active_editor(&self) -> Option<Entity<Editor>> {
4186 self.active_item.as_ref()?.active_editor.upgrade()
4187 }
4188
4189 fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
4190 self.active_item.as_ref()?.item_handle.upgrade()
4191 }
4192
4193 fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
4194 self.active_item().map_or(true, |active_item| {
4195 !self.pinned && active_item.item_id() != new_active_item.item_id()
4196 })
4197 }
4198
4199 pub fn toggle_active_editor_pin(
4200 &mut self,
4201 _: &ToggleActiveEditorPin,
4202 window: &mut Window,
4203 cx: &mut Context<Self>,
4204 ) {
4205 self.pinned = !self.pinned;
4206 if !self.pinned {
4207 if let Some((active_item, active_editor)) = self
4208 .workspace
4209 .upgrade()
4210 .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
4211 {
4212 if self.should_replace_active_item(active_item.as_ref()) {
4213 self.replace_active_editor(active_item, active_editor, window, cx);
4214 }
4215 }
4216 }
4217
4218 cx.notify();
4219 }
4220
4221 fn selected_entry(&self) -> Option<&PanelEntry> {
4222 match &self.selected_entry {
4223 SelectedEntry::Invalidated(entry) => entry.as_ref(),
4224 SelectedEntry::Valid(entry, _) => Some(entry),
4225 SelectedEntry::None => None,
4226 }
4227 }
4228
4229 fn select_entry(
4230 &mut self,
4231 entry: PanelEntry,
4232 focus: bool,
4233 window: &mut Window,
4234 cx: &mut Context<Self>,
4235 ) {
4236 if focus {
4237 self.focus_handle.focus(window);
4238 }
4239 let ix = self
4240 .cached_entries
4241 .iter()
4242 .enumerate()
4243 .find(|(_, cached_entry)| &cached_entry.entry == &entry)
4244 .map(|(i, _)| i)
4245 .unwrap_or_default();
4246
4247 self.selected_entry = SelectedEntry::Valid(entry, ix);
4248
4249 self.autoscroll(cx);
4250 cx.notify();
4251 }
4252
4253 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4254 if !Self::should_show_scrollbar(cx)
4255 || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4256 {
4257 return None;
4258 }
4259 Some(
4260 div()
4261 .occlude()
4262 .id("project-panel-vertical-scroll")
4263 .on_mouse_move(cx.listener(|_, _, _, cx| {
4264 cx.notify();
4265 cx.stop_propagation()
4266 }))
4267 .on_hover(|_, _, cx| {
4268 cx.stop_propagation();
4269 })
4270 .on_any_mouse_down(|_, _, cx| {
4271 cx.stop_propagation();
4272 })
4273 .on_mouse_up(
4274 MouseButton::Left,
4275 cx.listener(|outline_panel, _, window, cx| {
4276 if !outline_panel.vertical_scrollbar_state.is_dragging()
4277 && !outline_panel.focus_handle.contains_focused(window, cx)
4278 {
4279 outline_panel.hide_scrollbar(window, cx);
4280 cx.notify();
4281 }
4282
4283 cx.stop_propagation();
4284 }),
4285 )
4286 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4287 cx.notify();
4288 }))
4289 .h_full()
4290 .absolute()
4291 .right_1()
4292 .top_1()
4293 .bottom_0()
4294 .w(px(12.))
4295 .cursor_default()
4296 .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone())),
4297 )
4298 }
4299
4300 fn render_horizontal_scrollbar(
4301 &self,
4302 _: &mut Window,
4303 cx: &mut Context<Self>,
4304 ) -> Option<Stateful<Div>> {
4305 if !Self::should_show_scrollbar(cx)
4306 || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4307 {
4308 return None;
4309 }
4310
4311 let scroll_handle = self.scroll_handle.0.borrow();
4312 let longest_item_width = scroll_handle
4313 .last_item_size
4314 .filter(|size| size.contents.width > size.item.width)?
4315 .contents
4316 .width
4317 .0 as f64;
4318 if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
4319 return None;
4320 }
4321
4322 Some(
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 .when(self.width.is_some(), |this| {
4360 this.children(Scrollbar::horizontal(
4361 self.horizontal_scrollbar_state.clone(),
4362 ))
4363 }),
4364 )
4365 }
4366
4367 fn should_show_scrollbar(cx: &App) -> bool {
4368 let show = OutlinePanelSettings::get_global(cx)
4369 .scrollbar
4370 .show
4371 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4372 match show {
4373 ShowScrollbar::Auto => true,
4374 ShowScrollbar::System => true,
4375 ShowScrollbar::Always => true,
4376 ShowScrollbar::Never => false,
4377 }
4378 }
4379
4380 fn should_autohide_scrollbar(cx: &App) -> bool {
4381 let show = OutlinePanelSettings::get_global(cx)
4382 .scrollbar
4383 .show
4384 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4385 match show {
4386 ShowScrollbar::Auto => true,
4387 ShowScrollbar::System => cx
4388 .try_global::<ScrollbarAutoHide>()
4389 .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4390 ShowScrollbar::Always => false,
4391 ShowScrollbar::Never => true,
4392 }
4393 }
4394
4395 fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4396 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4397 if !Self::should_autohide_scrollbar(cx) {
4398 return;
4399 }
4400 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4401 cx.background_executor()
4402 .timer(SCROLLBAR_SHOW_INTERVAL)
4403 .await;
4404 panel
4405 .update(cx, |panel, cx| {
4406 panel.show_scrollbar = false;
4407 cx.notify();
4408 })
4409 .log_err();
4410 }))
4411 }
4412
4413 fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
4414 let item_text_chars = match entry {
4415 PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
4416 .buffer_snapshot_for_id(external.buffer_id, cx)
4417 .and_then(|snapshot| {
4418 Some(snapshot.file()?.path().file_name()?.to_string_lossy().len())
4419 })
4420 .unwrap_or_default(),
4421 PanelEntry::Fs(FsEntry::Directory(directory)) => directory
4422 .entry
4423 .path
4424 .file_name()
4425 .map(|name| name.to_string_lossy().len())
4426 .unwrap_or_default(),
4427 PanelEntry::Fs(FsEntry::File(file)) => file
4428 .entry
4429 .path
4430 .file_name()
4431 .map(|name| name.to_string_lossy().len())
4432 .unwrap_or_default(),
4433 PanelEntry::FoldedDirs(folded_dirs) => {
4434 folded_dirs
4435 .entries
4436 .iter()
4437 .map(|dir| {
4438 dir.path
4439 .file_name()
4440 .map(|name| name.to_string_lossy().len())
4441 .unwrap_or_default()
4442 })
4443 .sum::<usize>()
4444 + folded_dirs.entries.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len()
4445 }
4446 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
4447 .excerpt_label(excerpt.buffer_id, &excerpt.range, cx)
4448 .map(|label| label.len())
4449 .unwrap_or_default(),
4450 PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(),
4451 PanelEntry::Search(search) => search
4452 .render_data
4453 .get()
4454 .map(|data| data.context_text.len())
4455 .unwrap_or_default(),
4456 };
4457
4458 (item_text_chars + depth) as u64
4459 }
4460
4461 fn render_main_contents(
4462 &mut self,
4463 query: Option<String>,
4464 show_indent_guides: bool,
4465 indent_size: f32,
4466 window: &mut Window,
4467 cx: &mut Context<Self>,
4468 ) -> Div {
4469 let contents = if self.cached_entries.is_empty() {
4470 let header = if self.updating_fs_entries || self.updating_cached_entries {
4471 None
4472 } else if query.is_some() {
4473 Some("No matches for query")
4474 } else {
4475 Some("No outlines available")
4476 };
4477
4478 v_flex()
4479 .flex_1()
4480 .justify_center()
4481 .size_full()
4482 .when_some(header, |panel, header| {
4483 panel
4484 .child(h_flex().justify_center().child(Label::new(header)))
4485 .when_some(query.clone(), |panel, query| {
4486 panel.child(h_flex().justify_center().child(Label::new(query)))
4487 })
4488 .child(
4489 h_flex()
4490 .pt(DynamicSpacing::Base04.rems(cx))
4491 .justify_center()
4492 .child({
4493 let keystroke =
4494 match self.position(window, cx) {
4495 DockPosition::Left => window
4496 .keystroke_text_for(&workspace::ToggleLeftDock),
4497 DockPosition::Bottom => window
4498 .keystroke_text_for(&workspace::ToggleBottomDock),
4499 DockPosition::Right => window
4500 .keystroke_text_for(&workspace::ToggleRightDock),
4501 };
4502 Label::new(format!("Toggle this panel with {keystroke}"))
4503 }),
4504 )
4505 })
4506 } else {
4507 let list_contents = {
4508 let items_len = self.cached_entries.len();
4509 let multi_buffer_snapshot = self
4510 .active_editor()
4511 .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
4512 uniform_list(cx.entity().clone(), "entries", items_len, {
4513 move |outline_panel, range, window, cx| {
4514 let entries = outline_panel.cached_entries.get(range);
4515 entries
4516 .map(|entries| entries.to_vec())
4517 .unwrap_or_default()
4518 .into_iter()
4519 .filter_map(|cached_entry| match cached_entry.entry {
4520 PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4521 &entry,
4522 cached_entry.depth,
4523 cached_entry.string_match.as_ref(),
4524 window,
4525 cx,
4526 )),
4527 PanelEntry::FoldedDirs(folded_dirs_entry) => {
4528 Some(outline_panel.render_folded_dirs(
4529 &folded_dirs_entry,
4530 cached_entry.depth,
4531 cached_entry.string_match.as_ref(),
4532 window,
4533 cx,
4534 ))
4535 }
4536 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4537 outline_panel.render_excerpt(
4538 &excerpt,
4539 cached_entry.depth,
4540 window,
4541 cx,
4542 )
4543 }
4544 PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4545 Some(outline_panel.render_outline(
4546 &entry,
4547 cached_entry.depth,
4548 cached_entry.string_match.as_ref(),
4549 window,
4550 cx,
4551 ))
4552 }
4553 PanelEntry::Search(SearchEntry {
4554 match_range,
4555 render_data,
4556 kind,
4557 ..
4558 }) => outline_panel.render_search_match(
4559 multi_buffer_snapshot.as_ref(),
4560 &match_range,
4561 &render_data,
4562 kind,
4563 cached_entry.depth,
4564 cached_entry.string_match.as_ref(),
4565 window,
4566 cx,
4567 ),
4568 })
4569 .collect()
4570 }
4571 })
4572 .with_sizing_behavior(ListSizingBehavior::Infer)
4573 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4574 .with_width_from_item(self.max_width_item_index)
4575 .track_scroll(self.scroll_handle.clone())
4576 .when(show_indent_guides, |list| {
4577 list.with_decoration(
4578 ui::indent_guides(
4579 cx.entity().clone(),
4580 px(indent_size),
4581 IndentGuideColors::panel(cx),
4582 |outline_panel, range, _, _| {
4583 let entries = outline_panel.cached_entries.get(range);
4584 if let Some(entries) = entries {
4585 entries.into_iter().map(|item| item.depth).collect()
4586 } else {
4587 smallvec::SmallVec::new()
4588 }
4589 },
4590 )
4591 .with_render_fn(
4592 cx.entity().clone(),
4593 move |outline_panel, params, _, _| {
4594 const LEFT_OFFSET: Pixels = px(14.);
4595
4596 let indent_size = params.indent_size;
4597 let item_height = params.item_height;
4598 let active_indent_guide_ix = find_active_indent_guide_ix(
4599 outline_panel,
4600 ¶ms.indent_guides,
4601 );
4602
4603 params
4604 .indent_guides
4605 .into_iter()
4606 .enumerate()
4607 .map(|(ix, layout)| {
4608 let bounds = Bounds::new(
4609 point(
4610 layout.offset.x * indent_size + LEFT_OFFSET,
4611 layout.offset.y * item_height,
4612 ),
4613 size(px(1.), layout.length * item_height),
4614 );
4615 ui::RenderedIndentGuide {
4616 bounds,
4617 layout,
4618 is_active: active_indent_guide_ix == Some(ix),
4619 hitbox: None,
4620 }
4621 })
4622 .collect()
4623 },
4624 ),
4625 )
4626 })
4627 };
4628
4629 v_flex()
4630 .flex_shrink()
4631 .size_full()
4632 .child(list_contents.size_full().flex_shrink())
4633 .children(self.render_vertical_scrollbar(cx))
4634 .when_some(
4635 self.render_horizontal_scrollbar(window, cx),
4636 |this, scrollbar| this.pb_4().child(scrollbar),
4637 )
4638 }
4639 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4640 deferred(
4641 anchored()
4642 .position(*position)
4643 .anchor(gpui::Corner::TopLeft)
4644 .child(menu.clone()),
4645 )
4646 .with_priority(1)
4647 }));
4648
4649 v_flex().w_full().flex_1().overflow_hidden().child(contents)
4650 }
4651
4652 fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4653 v_flex().flex_none().child(horizontal_separator(cx)).child(
4654 h_flex()
4655 .p_2()
4656 .w_full()
4657 .child(self.filter_editor.clone())
4658 .child(
4659 div().child(
4660 IconButton::new(
4661 "outline-panel-menu",
4662 if pinned {
4663 IconName::Unpin
4664 } else {
4665 IconName::Pin
4666 },
4667 )
4668 .tooltip(Tooltip::text(if pinned {
4669 "Unpin Outline"
4670 } else {
4671 "Pin Active Outline"
4672 }))
4673 .shape(IconButtonShape::Square)
4674 .on_click(cx.listener(
4675 |outline_panel, _, window, cx| {
4676 outline_panel.toggle_active_editor_pin(
4677 &ToggleActiveEditorPin,
4678 window,
4679 cx,
4680 );
4681 },
4682 )),
4683 ),
4684 ),
4685 )
4686 }
4687
4688 fn buffers_inside_directory(
4689 &self,
4690 dir_worktree: WorktreeId,
4691 dir_entry: &GitEntry,
4692 ) -> HashSet<BufferId> {
4693 if !dir_entry.is_dir() {
4694 debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4695 return HashSet::default();
4696 }
4697
4698 self.fs_entries
4699 .iter()
4700 .skip_while(|fs_entry| match fs_entry {
4701 FsEntry::Directory(directory) => {
4702 directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4703 }
4704 _ => true,
4705 })
4706 .skip(1)
4707 .take_while(|fs_entry| match fs_entry {
4708 FsEntry::ExternalFile(..) => false,
4709 FsEntry::Directory(directory) => {
4710 directory.worktree_id == dir_worktree
4711 && directory.entry.path.starts_with(&dir_entry.path)
4712 }
4713 FsEntry::File(file) => {
4714 file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4715 }
4716 })
4717 .filter_map(|fs_entry| match fs_entry {
4718 FsEntry::File(file) => Some(file.buffer_id),
4719 _ => None,
4720 })
4721 .collect()
4722 }
4723}
4724
4725fn workspace_active_editor(
4726 workspace: &Workspace,
4727 cx: &App,
4728) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4729 let active_item = workspace.active_item(cx)?;
4730 let active_editor = active_item
4731 .act_as::<Editor>(cx)
4732 .filter(|editor| editor.read(cx).mode() == EditorMode::Full)?;
4733 Some((active_item, active_editor))
4734}
4735
4736fn back_to_common_visited_parent(
4737 visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
4738 worktree_id: &WorktreeId,
4739 new_entry: &Entry,
4740) -> Option<(WorktreeId, ProjectEntryId)> {
4741 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4742 match new_entry.path.parent() {
4743 Some(parent_path) => {
4744 if parent_path == visited_path.as_ref() {
4745 return Some((*worktree_id, *visited_dir_id));
4746 }
4747 }
4748 None => {
4749 break;
4750 }
4751 }
4752 visited_dirs.pop();
4753 }
4754 None
4755}
4756
4757fn file_name(path: &Path) -> String {
4758 let mut current_path = path;
4759 loop {
4760 if let Some(file_name) = current_path.file_name() {
4761 return file_name.to_string_lossy().into_owned();
4762 }
4763 match current_path.parent() {
4764 Some(parent) => current_path = parent,
4765 None => return path.to_string_lossy().into_owned(),
4766 }
4767 }
4768}
4769
4770impl Panel for OutlinePanel {
4771 fn persistent_name() -> &'static str {
4772 "Outline Panel"
4773 }
4774
4775 fn position(&self, _: &Window, cx: &App) -> DockPosition {
4776 match OutlinePanelSettings::get_global(cx).dock {
4777 OutlinePanelDockPosition::Left => DockPosition::Left,
4778 OutlinePanelDockPosition::Right => DockPosition::Right,
4779 }
4780 }
4781
4782 fn position_is_valid(&self, position: DockPosition) -> bool {
4783 matches!(position, DockPosition::Left | DockPosition::Right)
4784 }
4785
4786 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4787 settings::update_settings_file::<OutlinePanelSettings>(
4788 self.fs.clone(),
4789 cx,
4790 move |settings, _| {
4791 let dock = match position {
4792 DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
4793 DockPosition::Right => OutlinePanelDockPosition::Right,
4794 };
4795 settings.dock = Some(dock);
4796 },
4797 );
4798 }
4799
4800 fn size(&self, _: &Window, cx: &App) -> Pixels {
4801 self.width
4802 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
4803 }
4804
4805 fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
4806 self.width = size;
4807 self.serialize(cx);
4808 cx.notify();
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))
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(format!("'{}'", search_state.query))),
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::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
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.update(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(&Open, 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 =
5698 "search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },";
5699 outline_panel.update(cx, |outline_panel, cx| {
5700 assert_eq!(
5701 display_entries(
5702 &project,
5703 &snapshot(&outline_panel, cx),
5704 &outline_panel.cached_entries,
5705 outline_panel.selected_entry(),
5706 cx,
5707 ),
5708 select_first_in_all_matches(next_navigated_outline_selection)
5709 );
5710 });
5711 cx.executor()
5712 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5713 outline_panel.update(cx, |_outline_panel, cx| {
5714 assert_eq!(
5715 selected_row_text(&active_editor, cx),
5716 next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5717 "Should again preserve the selection after another SelectNext call"
5718 );
5719 });
5720
5721 outline_panel.update_in(cx, |outline_panel, window, cx| {
5722 outline_panel.open_excerpts(&editor::OpenExcerpts, window, cx);
5723 });
5724 cx.executor()
5725 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5726 cx.run_until_parked();
5727 let new_active_editor = outline_panel.update(cx, |outline_panel, _| {
5728 outline_panel
5729 .active_editor()
5730 .expect("should have an active editor open")
5731 });
5732 outline_panel.update(cx, |outline_panel, cx| {
5733 assert_ne!(
5734 active_editor, new_active_editor,
5735 "After opening an excerpt, new editor should be open"
5736 );
5737 assert_eq!(
5738 display_entries(
5739 &project,
5740 &snapshot(&outline_panel, cx),
5741 &outline_panel.cached_entries,
5742 outline_panel.selected_entry(),
5743 cx,
5744 ),
5745 "fn_lifetime_fn.rs <==== selected"
5746 );
5747 assert_eq!(
5748 selected_row_text(&new_active_editor, cx),
5749 next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5750 "When opening the excerpt, should navigate to the place corresponding the outline entry"
5751 );
5752 });
5753 }
5754
5755 #[gpui::test]
5756 async fn test_multiple_workrees(cx: &mut TestAppContext) {
5757 init_test(cx);
5758
5759 let fs = FakeFs::new(cx.background_executor.clone());
5760 fs.insert_tree(
5761 "/root",
5762 json!({
5763 "one": {
5764 "a.txt": "aaa aaa"
5765 },
5766 "two": {
5767 "b.txt": "a aaa"
5768 }
5769
5770 }),
5771 )
5772 .await;
5773 let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await;
5774 let workspace = add_outline_panel(&project, cx).await;
5775 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5776 let outline_panel = outline_panel(&workspace, cx);
5777 outline_panel.update_in(cx, |outline_panel, window, cx| {
5778 outline_panel.set_active(true, window, cx)
5779 });
5780
5781 let items = workspace
5782 .update(cx, |workspace, window, cx| {
5783 workspace.open_paths(
5784 vec![PathBuf::from("/root/two")],
5785 OpenOptions {
5786 visible: Some(OpenVisible::OnlyDirectories),
5787 ..Default::default()
5788 },
5789 None,
5790 window,
5791 cx,
5792 )
5793 })
5794 .unwrap()
5795 .await;
5796 assert_eq!(items.len(), 1, "Were opening another worktree directory");
5797 assert!(
5798 items[0].is_none(),
5799 "Directory should be opened successfully"
5800 );
5801
5802 workspace
5803 .update(cx, |workspace, window, cx| {
5804 ProjectSearchView::deploy_search(
5805 workspace,
5806 &workspace::DeploySearch::default(),
5807 window,
5808 cx,
5809 )
5810 })
5811 .unwrap();
5812 let search_view = workspace
5813 .update(cx, |workspace, _, cx| {
5814 workspace
5815 .active_pane()
5816 .read(cx)
5817 .items()
5818 .find_map(|item| item.downcast::<ProjectSearchView>())
5819 .expect("Project search view expected to appear after new search event trigger")
5820 })
5821 .unwrap();
5822
5823 let query = "aaa";
5824 perform_project_search(&search_view, query, cx);
5825 search_view.update(cx, |search_view, cx| {
5826 search_view
5827 .results_editor()
5828 .update(cx, |results_editor, cx| {
5829 assert_eq!(
5830 results_editor.display_text(cx).match_indices(query).count(),
5831 3
5832 );
5833 });
5834 });
5835
5836 cx.executor()
5837 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5838 cx.run_until_parked();
5839 outline_panel.update(cx, |outline_panel, cx| {
5840 assert_eq!(
5841 display_entries(
5842 &project,
5843 &snapshot(&outline_panel, cx),
5844 &outline_panel.cached_entries,
5845 outline_panel.selected_entry(),
5846 cx,
5847 ),
5848 r#"/root/one/
5849 a.txt
5850 search: aaa aaa <==== selected
5851 search: aaa aaa
5852/root/two/
5853 b.txt
5854 search: a aaa"#
5855 );
5856 });
5857
5858 outline_panel.update_in(cx, |outline_panel, window, cx| {
5859 outline_panel.select_previous(&SelectPrevious, window, cx);
5860 outline_panel.open(&Open, window, cx);
5861 });
5862 cx.executor()
5863 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5864 cx.run_until_parked();
5865 outline_panel.update(cx, |outline_panel, cx| {
5866 assert_eq!(
5867 display_entries(
5868 &project,
5869 &snapshot(&outline_panel, cx),
5870 &outline_panel.cached_entries,
5871 outline_panel.selected_entry(),
5872 cx,
5873 ),
5874 r#"/root/one/
5875 a.txt <==== selected
5876/root/two/
5877 b.txt
5878 search: a aaa"#
5879 );
5880 });
5881
5882 outline_panel.update_in(cx, |outline_panel, window, cx| {
5883 outline_panel.select_next(&SelectNext, window, cx);
5884 outline_panel.open(&Open, window, cx);
5885 });
5886 cx.executor()
5887 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5888 cx.run_until_parked();
5889 outline_panel.update(cx, |outline_panel, cx| {
5890 assert_eq!(
5891 display_entries(
5892 &project,
5893 &snapshot(&outline_panel, cx),
5894 &outline_panel.cached_entries,
5895 outline_panel.selected_entry(),
5896 cx,
5897 ),
5898 r#"/root/one/
5899 a.txt
5900/root/two/ <==== selected"#
5901 );
5902 });
5903
5904 outline_panel.update_in(cx, |outline_panel, window, cx| {
5905 outline_panel.open(&Open, window, cx);
5906 });
5907 cx.executor()
5908 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5909 cx.run_until_parked();
5910 outline_panel.update(cx, |outline_panel, cx| {
5911 assert_eq!(
5912 display_entries(
5913 &project,
5914 &snapshot(&outline_panel, cx),
5915 &outline_panel.cached_entries,
5916 outline_panel.selected_entry(),
5917 cx,
5918 ),
5919 r#"/root/one/
5920 a.txt
5921/root/two/ <==== selected
5922 b.txt
5923 search: a aaa"#
5924 );
5925 });
5926 }
5927
5928 #[gpui::test]
5929 async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
5930 init_test(cx);
5931
5932 let root = path!("/root");
5933 let fs = FakeFs::new(cx.background_executor.clone());
5934 fs.insert_tree(
5935 root,
5936 json!({
5937 "src": {
5938 "lib.rs": indoc!("
5939#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5940struct OutlineEntryExcerpt {
5941 id: ExcerptId,
5942 buffer_id: BufferId,
5943 range: ExcerptRange<language::Anchor>,
5944}"),
5945 }
5946 }),
5947 )
5948 .await;
5949 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
5950 project.read_with(cx, |project, _| {
5951 project.languages().add(Arc::new(
5952 rust_lang()
5953 .with_outline_query(
5954 r#"
5955 (struct_item
5956 (visibility_modifier)? @context
5957 "struct" @context
5958 name: (_) @name) @item
5959
5960 (field_declaration
5961 (visibility_modifier)? @context
5962 name: (_) @name) @item
5963"#,
5964 )
5965 .unwrap(),
5966 ))
5967 });
5968 let workspace = add_outline_panel(&project, cx).await;
5969 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5970 let outline_panel = outline_panel(&workspace, cx);
5971 cx.update(|window, cx| {
5972 outline_panel.update(cx, |outline_panel, cx| {
5973 outline_panel.set_active(true, window, cx)
5974 });
5975 });
5976
5977 let _editor = workspace
5978 .update(cx, |workspace, window, cx| {
5979 workspace.open_abs_path(
5980 PathBuf::from(path!("/root/src/lib.rs")),
5981 OpenOptions {
5982 visible: Some(OpenVisible::All),
5983 ..Default::default()
5984 },
5985 window,
5986 cx,
5987 )
5988 })
5989 .unwrap()
5990 .await
5991 .expect("Failed to open Rust source file")
5992 .downcast::<Editor>()
5993 .expect("Should open an editor for Rust source file");
5994
5995 cx.executor()
5996 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5997 cx.run_until_parked();
5998 outline_panel.update(cx, |outline_panel, cx| {
5999 assert_eq!(
6000 display_entries(
6001 &project,
6002 &snapshot(&outline_panel, cx),
6003 &outline_panel.cached_entries,
6004 outline_panel.selected_entry(),
6005 cx,
6006 ),
6007 indoc!(
6008 "
6009outline: struct OutlineEntryExcerpt
6010 outline: id
6011 outline: buffer_id
6012 outline: range"
6013 )
6014 );
6015 });
6016
6017 cx.update(|window, cx| {
6018 outline_panel.update(cx, |outline_panel, cx| {
6019 outline_panel.select_next(&SelectNext, window, cx);
6020 });
6021 });
6022 cx.executor()
6023 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6024 cx.run_until_parked();
6025 outline_panel.update(cx, |outline_panel, cx| {
6026 assert_eq!(
6027 display_entries(
6028 &project,
6029 &snapshot(&outline_panel, cx),
6030 &outline_panel.cached_entries,
6031 outline_panel.selected_entry(),
6032 cx,
6033 ),
6034 indoc!(
6035 "
6036outline: struct OutlineEntryExcerpt <==== selected
6037 outline: id
6038 outline: buffer_id
6039 outline: range"
6040 )
6041 );
6042 });
6043
6044 cx.update(|window, cx| {
6045 outline_panel.update(cx, |outline_panel, cx| {
6046 outline_panel.select_next(&SelectNext, window, cx);
6047 });
6048 });
6049 cx.executor()
6050 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6051 cx.run_until_parked();
6052 outline_panel.update(cx, |outline_panel, cx| {
6053 assert_eq!(
6054 display_entries(
6055 &project,
6056 &snapshot(&outline_panel, cx),
6057 &outline_panel.cached_entries,
6058 outline_panel.selected_entry(),
6059 cx,
6060 ),
6061 indoc!(
6062 "
6063outline: struct OutlineEntryExcerpt
6064 outline: id <==== selected
6065 outline: buffer_id
6066 outline: range"
6067 )
6068 );
6069 });
6070
6071 cx.update(|window, cx| {
6072 outline_panel.update(cx, |outline_panel, cx| {
6073 outline_panel.select_next(&SelectNext, window, cx);
6074 });
6075 });
6076 cx.executor()
6077 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6078 cx.run_until_parked();
6079 outline_panel.update(cx, |outline_panel, cx| {
6080 assert_eq!(
6081 display_entries(
6082 &project,
6083 &snapshot(&outline_panel, cx),
6084 &outline_panel.cached_entries,
6085 outline_panel.selected_entry(),
6086 cx,
6087 ),
6088 indoc!(
6089 "
6090outline: struct OutlineEntryExcerpt
6091 outline: id
6092 outline: buffer_id <==== selected
6093 outline: range"
6094 )
6095 );
6096 });
6097
6098 cx.update(|window, cx| {
6099 outline_panel.update(cx, |outline_panel, cx| {
6100 outline_panel.select_next(&SelectNext, window, cx);
6101 });
6102 });
6103 cx.executor()
6104 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6105 cx.run_until_parked();
6106 outline_panel.update(cx, |outline_panel, cx| {
6107 assert_eq!(
6108 display_entries(
6109 &project,
6110 &snapshot(&outline_panel, cx),
6111 &outline_panel.cached_entries,
6112 outline_panel.selected_entry(),
6113 cx,
6114 ),
6115 indoc!(
6116 "
6117outline: struct OutlineEntryExcerpt
6118 outline: id
6119 outline: buffer_id
6120 outline: range <==== selected"
6121 )
6122 );
6123 });
6124
6125 cx.update(|window, cx| {
6126 outline_panel.update(cx, |outline_panel, cx| {
6127 outline_panel.select_next(&SelectNext, window, cx);
6128 });
6129 });
6130 cx.executor()
6131 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6132 cx.run_until_parked();
6133 outline_panel.update(cx, |outline_panel, cx| {
6134 assert_eq!(
6135 display_entries(
6136 &project,
6137 &snapshot(&outline_panel, cx),
6138 &outline_panel.cached_entries,
6139 outline_panel.selected_entry(),
6140 cx,
6141 ),
6142 indoc!(
6143 "
6144outline: struct OutlineEntryExcerpt <==== selected
6145 outline: id
6146 outline: buffer_id
6147 outline: range"
6148 )
6149 );
6150 });
6151
6152 cx.update(|window, cx| {
6153 outline_panel.update(cx, |outline_panel, cx| {
6154 outline_panel.select_previous(&SelectPrevious, window, cx);
6155 });
6156 });
6157 cx.executor()
6158 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6159 cx.run_until_parked();
6160 outline_panel.update(cx, |outline_panel, cx| {
6161 assert_eq!(
6162 display_entries(
6163 &project,
6164 &snapshot(&outline_panel, cx),
6165 &outline_panel.cached_entries,
6166 outline_panel.selected_entry(),
6167 cx,
6168 ),
6169 indoc!(
6170 "
6171outline: struct OutlineEntryExcerpt
6172 outline: id
6173 outline: buffer_id
6174 outline: range <==== selected"
6175 )
6176 );
6177 });
6178
6179 cx.update(|window, cx| {
6180 outline_panel.update(cx, |outline_panel, cx| {
6181 outline_panel.select_previous(&SelectPrevious, window, cx);
6182 });
6183 });
6184 cx.executor()
6185 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6186 cx.run_until_parked();
6187 outline_panel.update(cx, |outline_panel, cx| {
6188 assert_eq!(
6189 display_entries(
6190 &project,
6191 &snapshot(&outline_panel, cx),
6192 &outline_panel.cached_entries,
6193 outline_panel.selected_entry(),
6194 cx,
6195 ),
6196 indoc!(
6197 "
6198outline: struct OutlineEntryExcerpt
6199 outline: id
6200 outline: buffer_id <==== selected
6201 outline: range"
6202 )
6203 );
6204 });
6205
6206 cx.update(|window, cx| {
6207 outline_panel.update(cx, |outline_panel, cx| {
6208 outline_panel.select_previous(&SelectPrevious, window, cx);
6209 });
6210 });
6211 cx.executor()
6212 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6213 cx.run_until_parked();
6214 outline_panel.update(cx, |outline_panel, cx| {
6215 assert_eq!(
6216 display_entries(
6217 &project,
6218 &snapshot(&outline_panel, cx),
6219 &outline_panel.cached_entries,
6220 outline_panel.selected_entry(),
6221 cx,
6222 ),
6223 indoc!(
6224 "
6225outline: struct OutlineEntryExcerpt
6226 outline: id <==== selected
6227 outline: buffer_id
6228 outline: range"
6229 )
6230 );
6231 });
6232
6233 cx.update(|window, cx| {
6234 outline_panel.update(cx, |outline_panel, cx| {
6235 outline_panel.select_previous(&SelectPrevious, window, cx);
6236 });
6237 });
6238 cx.executor()
6239 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6240 cx.run_until_parked();
6241 outline_panel.update(cx, |outline_panel, cx| {
6242 assert_eq!(
6243 display_entries(
6244 &project,
6245 &snapshot(&outline_panel, cx),
6246 &outline_panel.cached_entries,
6247 outline_panel.selected_entry(),
6248 cx,
6249 ),
6250 indoc!(
6251 "
6252outline: struct OutlineEntryExcerpt <==== selected
6253 outline: id
6254 outline: buffer_id
6255 outline: range"
6256 )
6257 );
6258 });
6259
6260 cx.update(|window, cx| {
6261 outline_panel.update(cx, |outline_panel, cx| {
6262 outline_panel.select_previous(&SelectPrevious, window, cx);
6263 });
6264 });
6265 cx.executor()
6266 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6267 cx.run_until_parked();
6268 outline_panel.update(cx, |outline_panel, cx| {
6269 assert_eq!(
6270 display_entries(
6271 &project,
6272 &snapshot(&outline_panel, cx),
6273 &outline_panel.cached_entries,
6274 outline_panel.selected_entry(),
6275 cx,
6276 ),
6277 indoc!(
6278 "
6279outline: struct OutlineEntryExcerpt
6280 outline: id
6281 outline: buffer_id
6282 outline: range <==== selected"
6283 )
6284 );
6285 });
6286 }
6287
6288 #[gpui::test(iterations = 10)]
6289 async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6290 init_test(cx);
6291
6292 let root = "/frontend-project";
6293 let fs = FakeFs::new(cx.background_executor.clone());
6294 fs.insert_tree(
6295 root,
6296 json!({
6297 "public": {
6298 "lottie": {
6299 "syntax-tree.json": r#"{ "something": "static" }"#
6300 }
6301 },
6302 "src": {
6303 "app": {
6304 "(site)": {
6305 "(about)": {
6306 "jobs": {
6307 "[slug]": {
6308 "page.tsx": r#"static"#
6309 }
6310 }
6311 },
6312 "(blog)": {
6313 "post": {
6314 "[slug]": {
6315 "page.tsx": r#"static"#
6316 }
6317 }
6318 },
6319 }
6320 },
6321 "components": {
6322 "ErrorBoundary.tsx": r#"static"#,
6323 }
6324 }
6325
6326 }),
6327 )
6328 .await;
6329 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
6330 let workspace = add_outline_panel(&project, cx).await;
6331 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6332 let outline_panel = outline_panel(&workspace, cx);
6333 outline_panel.update_in(cx, |outline_panel, window, cx| {
6334 outline_panel.set_active(true, window, cx)
6335 });
6336
6337 workspace
6338 .update(cx, |workspace, window, cx| {
6339 ProjectSearchView::deploy_search(
6340 workspace,
6341 &workspace::DeploySearch::default(),
6342 window,
6343 cx,
6344 )
6345 })
6346 .unwrap();
6347 let search_view = workspace
6348 .update(cx, |workspace, _, cx| {
6349 workspace
6350 .active_pane()
6351 .read(cx)
6352 .items()
6353 .find_map(|item| item.downcast::<ProjectSearchView>())
6354 .expect("Project search view expected to appear after new search event trigger")
6355 })
6356 .unwrap();
6357
6358 let query = "static";
6359 perform_project_search(&search_view, query, cx);
6360 search_view.update(cx, |search_view, cx| {
6361 search_view
6362 .results_editor()
6363 .update(cx, |results_editor, cx| {
6364 assert_eq!(
6365 results_editor.display_text(cx).match_indices(query).count(),
6366 4
6367 );
6368 });
6369 });
6370
6371 cx.executor()
6372 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6373 cx.run_until_parked();
6374 outline_panel.update(cx, |outline_panel, cx| {
6375 assert_eq!(
6376 display_entries(
6377 &project,
6378 &snapshot(&outline_panel, cx),
6379 &outline_panel.cached_entries,
6380 outline_panel.selected_entry(),
6381 cx,
6382 ),
6383 r#"/frontend-project/
6384 public/lottie/
6385 syntax-tree.json
6386 search: { "something": "static" } <==== selected
6387 src/
6388 app/(site)/
6389 (about)/jobs/[slug]/
6390 page.tsx
6391 search: static
6392 (blog)/post/[slug]/
6393 page.tsx
6394 search: static
6395 components/
6396 ErrorBoundary.tsx
6397 search: static"#
6398 );
6399 });
6400
6401 outline_panel.update_in(cx, |outline_panel, window, cx| {
6402 // Move to 5th element in the list, 3 items down.
6403 for _ in 0..2 {
6404 outline_panel.select_next(&SelectNext, window, cx);
6405 }
6406 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6407 });
6408 cx.executor()
6409 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6410 cx.run_until_parked();
6411 outline_panel.update(cx, |outline_panel, cx| {
6412 assert_eq!(
6413 display_entries(
6414 &project,
6415 &snapshot(&outline_panel, cx),
6416 &outline_panel.cached_entries,
6417 outline_panel.selected_entry(),
6418 cx,
6419 ),
6420 r#"/frontend-project/
6421 public/lottie/
6422 syntax-tree.json
6423 search: { "something": "static" }
6424 src/
6425 app/(site)/ <==== selected
6426 components/
6427 ErrorBoundary.tsx
6428 search: static"#
6429 );
6430 });
6431
6432 outline_panel.update_in(cx, |outline_panel, window, cx| {
6433 // Move to the next visible non-FS entry
6434 for _ in 0..3 {
6435 outline_panel.select_next(&SelectNext, window, cx);
6436 }
6437 });
6438 cx.run_until_parked();
6439 outline_panel.update(cx, |outline_panel, cx| {
6440 assert_eq!(
6441 display_entries(
6442 &project,
6443 &snapshot(&outline_panel, cx),
6444 &outline_panel.cached_entries,
6445 outline_panel.selected_entry(),
6446 cx,
6447 ),
6448 r#"/frontend-project/
6449 public/lottie/
6450 syntax-tree.json
6451 search: { "something": "static" }
6452 src/
6453 app/(site)/
6454 components/
6455 ErrorBoundary.tsx
6456 search: static <==== selected"#
6457 );
6458 });
6459
6460 outline_panel.update_in(cx, |outline_panel, window, cx| {
6461 outline_panel
6462 .active_editor()
6463 .expect("Should have an active editor")
6464 .update(cx, |editor, cx| {
6465 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6466 });
6467 });
6468 cx.executor()
6469 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6470 cx.run_until_parked();
6471 outline_panel.update(cx, |outline_panel, cx| {
6472 assert_eq!(
6473 display_entries(
6474 &project,
6475 &snapshot(&outline_panel, cx),
6476 &outline_panel.cached_entries,
6477 outline_panel.selected_entry(),
6478 cx,
6479 ),
6480 r#"/frontend-project/
6481 public/lottie/
6482 syntax-tree.json
6483 search: { "something": "static" }
6484 src/
6485 app/(site)/
6486 components/
6487 ErrorBoundary.tsx <==== selected"#
6488 );
6489 });
6490
6491 outline_panel.update_in(cx, |outline_panel, window, cx| {
6492 outline_panel
6493 .active_editor()
6494 .expect("Should have an active editor")
6495 .update(cx, |editor, cx| {
6496 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6497 });
6498 });
6499 cx.executor()
6500 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6501 cx.run_until_parked();
6502 outline_panel.update(cx, |outline_panel, cx| {
6503 assert_eq!(
6504 display_entries(
6505 &project,
6506 &snapshot(&outline_panel, cx),
6507 &outline_panel.cached_entries,
6508 outline_panel.selected_entry(),
6509 cx,
6510 ),
6511 r#"/frontend-project/
6512 public/lottie/
6513 syntax-tree.json
6514 search: { "something": "static" }
6515 src/
6516 app/(site)/
6517 components/
6518 ErrorBoundary.tsx <==== selected
6519 search: static"#
6520 );
6521 });
6522 }
6523
6524 async fn add_outline_panel(
6525 project: &Entity<Project>,
6526 cx: &mut TestAppContext,
6527 ) -> WindowHandle<Workspace> {
6528 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6529
6530 let outline_panel = window
6531 .update(cx, |_, window, cx| {
6532 cx.spawn_in(window, async |this, cx| {
6533 OutlinePanel::load(this, cx.clone()).await
6534 })
6535 })
6536 .unwrap()
6537 .await
6538 .expect("Failed to load outline panel");
6539
6540 window
6541 .update(cx, |workspace, window, cx| {
6542 workspace.add_panel(outline_panel, window, cx);
6543 })
6544 .unwrap();
6545 window
6546 }
6547
6548 fn outline_panel(
6549 workspace: &WindowHandle<Workspace>,
6550 cx: &mut TestAppContext,
6551 ) -> Entity<OutlinePanel> {
6552 workspace
6553 .update(cx, |workspace, _, cx| {
6554 workspace
6555 .panel::<OutlinePanel>(cx)
6556 .expect("no outline panel")
6557 })
6558 .unwrap()
6559 }
6560
6561 fn display_entries(
6562 project: &Entity<Project>,
6563 multi_buffer_snapshot: &MultiBufferSnapshot,
6564 cached_entries: &[CachedEntry],
6565 selected_entry: Option<&PanelEntry>,
6566 cx: &mut App,
6567 ) -> String {
6568 let mut display_string = String::new();
6569 for entry in cached_entries {
6570 if !display_string.is_empty() {
6571 display_string += "\n";
6572 }
6573 for _ in 0..entry.depth {
6574 display_string += " ";
6575 }
6576 display_string += &match &entry.entry {
6577 PanelEntry::Fs(entry) => match entry {
6578 FsEntry::ExternalFile(_) => {
6579 panic!("Did not cover external files with tests")
6580 }
6581 FsEntry::Directory(directory) => {
6582 match project
6583 .read(cx)
6584 .worktree_for_id(directory.worktree_id, cx)
6585 .and_then(|worktree| {
6586 if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
6587 Some(worktree.read(cx).abs_path())
6588 } else {
6589 None
6590 }
6591 }) {
6592 Some(root_path) => format!(
6593 "{}/{}",
6594 root_path.display(),
6595 directory.entry.path.display(),
6596 ),
6597 None => format!(
6598 "{}/",
6599 directory
6600 .entry
6601 .path
6602 .file_name()
6603 .unwrap_or_default()
6604 .to_string_lossy()
6605 ),
6606 }
6607 }
6608 FsEntry::File(file) => file
6609 .entry
6610 .path
6611 .file_name()
6612 .map(|name| name.to_string_lossy().to_string())
6613 .unwrap_or_default(),
6614 },
6615 PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6616 .entries
6617 .iter()
6618 .filter_map(|dir| dir.path.file_name())
6619 .map(|name| name.to_string_lossy().to_string() + "/")
6620 .collect(),
6621 PanelEntry::Outline(outline_entry) => match outline_entry {
6622 OutlineEntry::Excerpt(_) => continue,
6623 OutlineEntry::Outline(outline_entry) => {
6624 format!("outline: {}", outline_entry.outline.text)
6625 }
6626 },
6627 PanelEntry::Search(search_entry) => {
6628 format!(
6629 "search: {}",
6630 search_entry
6631 .render_data
6632 .get_or_init(|| SearchData::new(
6633 &search_entry.match_range,
6634 &multi_buffer_snapshot
6635 ))
6636 .context_text
6637 )
6638 }
6639 };
6640
6641 if Some(&entry.entry) == selected_entry {
6642 display_string += SELECTED_MARKER;
6643 }
6644 }
6645 display_string
6646 }
6647
6648 fn init_test(cx: &mut TestAppContext) {
6649 cx.update(|cx| {
6650 let settings = SettingsStore::test(cx);
6651 cx.set_global(settings);
6652
6653 theme::init(theme::LoadThemes::JustBase, cx);
6654
6655 language::init(cx);
6656 editor::init(cx);
6657 workspace::init_settings(cx);
6658 Project::init_settings(cx);
6659 project_search::init(cx);
6660 super::init(cx);
6661 });
6662 }
6663
6664 // Based on https://github.com/rust-lang/rust-analyzer/
6665 async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6666 fs.insert_tree(
6667 root,
6668 json!({
6669 "crates": {
6670 "ide": {
6671 "src": {
6672 "inlay_hints": {
6673 "fn_lifetime_fn.rs": r##"
6674 pub(super) fn hints(
6675 acc: &mut Vec<InlayHint>,
6676 config: &InlayHintsConfig,
6677 func: ast::Fn,
6678 ) -> Option<()> {
6679 // ... snip
6680
6681 let mut used_names: FxHashMap<SmolStr, usize> =
6682 match config.param_names_for_lifetime_elision_hints {
6683 true => generic_param_list
6684 .iter()
6685 .flat_map(|gpl| gpl.lifetime_params())
6686 .filter_map(|param| param.lifetime())
6687 .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6688 .collect(),
6689 false => Default::default(),
6690 };
6691 {
6692 let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6693 if self_param.is_some() && potential_lt_refs.next().is_some() {
6694 allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6695 // self can't be used as a lifetime, so no need to check for collisions
6696 "'self".into()
6697 } else {
6698 gen_idx_name()
6699 });
6700 }
6701 potential_lt_refs.for_each(|(name, ..)| {
6702 let name = match name {
6703 Some(it) if config.param_names_for_lifetime_elision_hints => {
6704 if let Some(c) = used_names.get_mut(it.text().as_str()) {
6705 *c += 1;
6706 SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6707 } else {
6708 used_names.insert(it.text().as_str().into(), 0);
6709 SmolStr::from_iter(["\'", it.text().as_str()])
6710 }
6711 }
6712 _ => gen_idx_name(),
6713 };
6714 allocated_lifetimes.push(name);
6715 });
6716 }
6717
6718 // ... snip
6719 }
6720
6721 // ... snip
6722
6723 #[test]
6724 fn hints_lifetimes_named() {
6725 check_with_config(
6726 InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6727 r#"
6728 fn nested_in<'named>(named: & &X< &()>) {}
6729 // ^'named1, 'named2, 'named3, $
6730 //^'named1 ^'named2 ^'named3
6731 "#,
6732 );
6733 }
6734
6735 // ... snip
6736 "##,
6737 },
6738 "inlay_hints.rs": r#"
6739 #[derive(Clone, Debug, PartialEq, Eq)]
6740 pub struct InlayHintsConfig {
6741 // ... snip
6742 pub param_names_for_lifetime_elision_hints: bool,
6743 pub max_length: Option<usize>,
6744 // ... snip
6745 }
6746
6747 impl Config {
6748 pub fn inlay_hints(&self) -> InlayHintsConfig {
6749 InlayHintsConfig {
6750 // ... snip
6751 param_names_for_lifetime_elision_hints: self
6752 .inlayHints_lifetimeElisionHints_useParameterNames()
6753 .to_owned(),
6754 max_length: self.inlayHints_maxLength().to_owned(),
6755 // ... snip
6756 }
6757 }
6758 }
6759 "#,
6760 "static_index.rs": r#"
6761// ... snip
6762 fn add_file(&mut self, file_id: FileId) {
6763 let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6764 let folds = self.analysis.folding_ranges(file_id).unwrap();
6765 let inlay_hints = self
6766 .analysis
6767 .inlay_hints(
6768 &InlayHintsConfig {
6769 // ... snip
6770 closure_style: hir::ClosureStyle::ImplFn,
6771 param_names_for_lifetime_elision_hints: false,
6772 binding_mode_hints: false,
6773 max_length: Some(25),
6774 closure_capture_hints: false,
6775 // ... snip
6776 },
6777 file_id,
6778 None,
6779 )
6780 .unwrap();
6781 // ... snip
6782 }
6783// ... snip
6784 "#
6785 }
6786 },
6787 "rust-analyzer": {
6788 "src": {
6789 "cli": {
6790 "analysis_stats.rs": r#"
6791 // ... snip
6792 for &file_id in &file_ids {
6793 _ = analysis.inlay_hints(
6794 &InlayHintsConfig {
6795 // ... snip
6796 implicit_drop_hints: true,
6797 lifetime_elision_hints: ide::LifetimeElisionHints::Always,
6798 param_names_for_lifetime_elision_hints: true,
6799 hide_named_constructor_hints: false,
6800 hide_closure_initialization_hints: false,
6801 closure_style: hir::ClosureStyle::ImplFn,
6802 max_length: Some(25),
6803 closing_brace_hints_min_lines: Some(20),
6804 fields_to_resolve: InlayFieldsToResolve::empty(),
6805 range_exclusive_hints: true,
6806 },
6807 file_id.into(),
6808 None,
6809 );
6810 }
6811 // ... snip
6812 "#,
6813 },
6814 "config.rs": r#"
6815 config_data! {
6816 /// Configs that only make sense when they are set by a client. As such they can only be defined
6817 /// by setting them using client's settings (e.g `settings.json` on VS Code).
6818 client: struct ClientDefaultConfigData <- ClientConfigInput -> {
6819 // ... snip
6820 /// Maximum length for inlay hints. Set to null to have an unlimited length.
6821 inlayHints_maxLength: Option<usize> = Some(25),
6822 // ... snip
6823 /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
6824 inlayHints_lifetimeElisionHints_useParameterNames: bool = false,
6825 // ... snip
6826 }
6827 }
6828
6829 impl Config {
6830 // ... snip
6831 pub fn inlay_hints(&self) -> InlayHintsConfig {
6832 InlayHintsConfig {
6833 // ... snip
6834 param_names_for_lifetime_elision_hints: self
6835 .inlayHints_lifetimeElisionHints_useParameterNames()
6836 .to_owned(),
6837 max_length: self.inlayHints_maxLength().to_owned(),
6838 // ... snip
6839 }
6840 }
6841 // ... snip
6842 }
6843 "#
6844 }
6845 }
6846 }
6847 }),
6848 )
6849 .await;
6850 }
6851
6852 fn rust_lang() -> Language {
6853 Language::new(
6854 LanguageConfig {
6855 name: "Rust".into(),
6856 matcher: LanguageMatcher {
6857 path_suffixes: vec!["rs".to_string()],
6858 ..Default::default()
6859 },
6860 ..Default::default()
6861 },
6862 Some(tree_sitter_rust::LANGUAGE.into()),
6863 )
6864 .with_highlights_query(
6865 r#"
6866 (field_identifier) @field
6867 (struct_expression) @struct
6868 "#,
6869 )
6870 .unwrap()
6871 .with_injection_query(
6872 r#"
6873 (macro_invocation
6874 (token_tree) @injection.content
6875 (#set! injection.language "rust"))
6876 "#,
6877 )
6878 .unwrap()
6879 }
6880
6881 fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
6882 outline_panel
6883 .active_editor()
6884 .unwrap()
6885 .read(cx)
6886 .buffer()
6887 .read(cx)
6888 .snapshot(cx)
6889 }
6890
6891 fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
6892 editor.update(cx, |editor, cx| {
6893 let selections = editor.selections.all::<language::Point>(cx);
6894 assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
6895 let selection = selections.first().unwrap();
6896 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
6897 let line_start = language::Point::new(selection.start.row, 0);
6898 let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
6899 multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
6900 })
6901 }
6902}