1mod outline_panel_settings;
2
3use std::{
4 cmp,
5 collections::BTreeMap,
6 hash::Hash,
7 ops::Range,
8 path::{MAIN_SEPARATOR_STR, Path, PathBuf},
9 sync::{
10 Arc, OnceLock,
11 atomic::{self, AtomicBool},
12 },
13 time::Duration,
14 u32,
15};
16
17use anyhow::Context as _;
18use collections::{BTreeSet, HashMap, HashSet, hash_map};
19use db::kvp::KEY_VALUE_STORE;
20use editor::{
21 AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorSettings, ExcerptId,
22 ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, ShowScrollbar,
23 display_map::ToDisplayPoint,
24 items::{entry_git_aware_label_color, entry_label_color},
25 scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor, ScrollbarAutoHide},
26};
27use file_icons::FileIcons;
28use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
29use gpui::{
30 Action, AnyElement, App, AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context,
31 DismissEvent, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
32 InteractiveElement, IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
33 MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy,
34 SharedString, Stateful, StatefulInteractiveElement as _, Styled, Subscription, Task,
35 UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, point, px, size,
36 uniform_list,
37};
38use itertools::Itertools;
39use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
40use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};
41
42use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings, ShowIndentGuides};
43use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem};
44use search::{BufferSearchBar, ProjectSearchView};
45use serde::{Deserialize, Serialize};
46use settings::{Settings, SettingsStore};
47use smol::channel;
48use theme::{SyntaxTheme, ThemeSettings};
49use ui::{DynamicSpacing, IndentGuideColors, IndentGuideLayout};
50use util::{RangeExt, ResultExt, TryFutureExt, debug_panic};
51use workspace::{
52 OpenInTerminal, WeakItemHandle, Workspace,
53 dock::{DockPosition, Panel, PanelEvent},
54 item::ItemHandle,
55 searchable::{SearchEvent, SearchableItem},
56 ui::{
57 ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder, HighlightedLabel,
58 Icon, IconButton, IconButtonShape, IconName, IconSize, Label, LabelCommon, ListItem,
59 Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable, Tooltip, h_flex,
60 v_flex,
61 },
62};
63use worktree::{Entry, ProjectEntryId, WorktreeId};
64
65actions!(
66 outline_panel,
67 [
68 CollapseAllEntries,
69 CollapseSelectedEntry,
70 ExpandAllEntries,
71 ExpandSelectedEntry,
72 FoldDirectory,
73 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 { !folded } else { folded }
1771 });
1772 });
1773
1774 self.select_entry(entry.clone(), true, window, cx);
1775 if buffers_to_toggle.is_empty() {
1776 self.update_cached_entries(None, window, cx);
1777 } else {
1778 self.toggle_buffers_fold(buffers_to_toggle, fold, window, cx)
1779 .detach();
1780 }
1781 }
1782
1783 fn toggle_buffers_fold(
1784 &self,
1785 buffers: HashSet<BufferId>,
1786 fold: bool,
1787 window: &mut Window,
1788 cx: &mut Context<Self>,
1789 ) -> Task<()> {
1790 let Some(active_editor) = self.active_editor() else {
1791 return Task::ready(());
1792 };
1793 cx.spawn_in(window, async move |outline_panel, cx| {
1794 outline_panel
1795 .update_in(cx, |outline_panel, window, cx| {
1796 active_editor.update(cx, |editor, cx| {
1797 for buffer_id in buffers {
1798 outline_panel
1799 .preserve_selection_on_buffer_fold_toggles
1800 .insert(buffer_id);
1801 if fold {
1802 editor.fold_buffer(buffer_id, cx);
1803 } else {
1804 editor.unfold_buffer(buffer_id, cx);
1805 }
1806 }
1807 });
1808 if let Some(selection) = outline_panel.selected_entry().cloned() {
1809 outline_panel.scroll_editor_to_entry(&selection, false, false, window, cx);
1810 }
1811 })
1812 .ok();
1813 })
1814 }
1815
1816 fn copy_path(
1817 &mut self,
1818 _: &zed_actions::workspace::CopyPath,
1819 _: &mut Window,
1820 cx: &mut Context<Self>,
1821 ) {
1822 if let Some(clipboard_text) = self
1823 .selected_entry()
1824 .and_then(|entry| self.abs_path(entry, cx))
1825 .map(|p| p.to_string_lossy().to_string())
1826 {
1827 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1828 }
1829 }
1830
1831 fn copy_relative_path(
1832 &mut self,
1833 _: &zed_actions::workspace::CopyRelativePath,
1834 _: &mut Window,
1835 cx: &mut Context<Self>,
1836 ) {
1837 if let Some(clipboard_text) = self
1838 .selected_entry()
1839 .and_then(|entry| match entry {
1840 PanelEntry::Fs(entry) => self.relative_path(entry, cx),
1841 PanelEntry::FoldedDirs(folded_dirs) => {
1842 folded_dirs.entries.last().map(|entry| entry.path.clone())
1843 }
1844 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1845 })
1846 .map(|p| p.to_string_lossy().to_string())
1847 {
1848 cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1849 }
1850 }
1851
1852 fn reveal_in_finder(
1853 &mut self,
1854 _: &RevealInFileManager,
1855 _: &mut Window,
1856 cx: &mut Context<Self>,
1857 ) {
1858 if let Some(abs_path) = self
1859 .selected_entry()
1860 .and_then(|entry| self.abs_path(entry, cx))
1861 {
1862 cx.reveal_path(&abs_path);
1863 }
1864 }
1865
1866 fn open_in_terminal(
1867 &mut self,
1868 _: &OpenInTerminal,
1869 window: &mut Window,
1870 cx: &mut Context<Self>,
1871 ) {
1872 let selected_entry = self.selected_entry();
1873 let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx));
1874 let working_directory = if let (
1875 Some(abs_path),
1876 Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1877 ) = (&abs_path, selected_entry)
1878 {
1879 abs_path.parent().map(|p| p.to_owned())
1880 } else {
1881 abs_path
1882 };
1883
1884 if let Some(working_directory) = working_directory {
1885 window.dispatch_action(
1886 workspace::OpenTerminal { working_directory }.boxed_clone(),
1887 cx,
1888 )
1889 }
1890 }
1891
1892 fn reveal_entry_for_selection(
1893 &mut self,
1894 editor: Entity<Editor>,
1895 window: &mut Window,
1896 cx: &mut Context<Self>,
1897 ) {
1898 if !self.active
1899 || !OutlinePanelSettings::get_global(cx).auto_reveal_entries
1900 || self.focus_handle.contains_focused(window, cx)
1901 {
1902 return;
1903 }
1904 let project = self.project.clone();
1905 self.reveal_selection_task = cx.spawn_in(window, async move |outline_panel, cx| {
1906 cx.background_executor().timer(UPDATE_DEBOUNCE).await;
1907 let entry_with_selection =
1908 outline_panel.update_in(cx, |outline_panel, window, cx| {
1909 outline_panel.location_for_editor_selection(&editor, window, cx)
1910 })?;
1911 let Some(entry_with_selection) = entry_with_selection else {
1912 outline_panel.update(cx, |outline_panel, cx| {
1913 outline_panel.selected_entry = SelectedEntry::None;
1914 cx.notify();
1915 })?;
1916 return Ok(());
1917 };
1918 let related_buffer_entry = match &entry_with_selection {
1919 PanelEntry::Fs(FsEntry::File(FsEntryFile {
1920 worktree_id,
1921 buffer_id,
1922 ..
1923 })) => project.update(cx, |project, cx| {
1924 let entry_id = project
1925 .buffer_for_id(*buffer_id, cx)
1926 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1927 project
1928 .worktree_for_id(*worktree_id, cx)
1929 .zip(entry_id)
1930 .and_then(|(worktree, entry_id)| {
1931 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1932 Some((worktree, entry))
1933 })
1934 })?,
1935 PanelEntry::Outline(outline_entry) => {
1936 let (buffer_id, excerpt_id) = outline_entry.ids();
1937 outline_panel.update(cx, |outline_panel, cx| {
1938 outline_panel
1939 .collapsed_entries
1940 .remove(&CollapsedEntry::ExternalFile(buffer_id));
1941 outline_panel
1942 .collapsed_entries
1943 .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1944 let project = outline_panel.project.read(cx);
1945 let entry_id = project
1946 .buffer_for_id(buffer_id, cx)
1947 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1948
1949 entry_id.and_then(|entry_id| {
1950 project
1951 .worktree_for_entry(entry_id, cx)
1952 .and_then(|worktree| {
1953 let worktree_id = worktree.read(cx).id();
1954 outline_panel
1955 .collapsed_entries
1956 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1957 let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1958 Some((worktree, entry))
1959 })
1960 })
1961 })?
1962 }
1963 PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
1964 PanelEntry::Search(SearchEntry { match_range, .. }) => match_range
1965 .start
1966 .buffer_id
1967 .or(match_range.end.buffer_id)
1968 .map(|buffer_id| {
1969 outline_panel.update(cx, |outline_panel, cx| {
1970 outline_panel
1971 .collapsed_entries
1972 .remove(&CollapsedEntry::ExternalFile(buffer_id));
1973 let project = project.read(cx);
1974 let entry_id = project
1975 .buffer_for_id(buffer_id, cx)
1976 .and_then(|buffer| buffer.read(cx).entry_id(cx));
1977
1978 entry_id.and_then(|entry_id| {
1979 project
1980 .worktree_for_entry(entry_id, cx)
1981 .and_then(|worktree| {
1982 let worktree_id = worktree.read(cx).id();
1983 outline_panel
1984 .collapsed_entries
1985 .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1986 let entry =
1987 worktree.read(cx).entry_for_id(entry_id)?.clone();
1988 Some((worktree, entry))
1989 })
1990 })
1991 })
1992 })
1993 .transpose()?
1994 .flatten(),
1995 _ => return anyhow::Ok(()),
1996 };
1997 if let Some((worktree, buffer_entry)) = related_buffer_entry {
1998 outline_panel.update(cx, |outline_panel, cx| {
1999 let worktree_id = worktree.read(cx).id();
2000 let mut dirs_to_expand = Vec::new();
2001 {
2002 let mut traversal = worktree.read(cx).traverse_from_path(
2003 true,
2004 true,
2005 true,
2006 buffer_entry.path.as_ref(),
2007 );
2008 let mut current_entry = buffer_entry;
2009 loop {
2010 if current_entry.is_dir()
2011 && outline_panel
2012 .collapsed_entries
2013 .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
2014 {
2015 dirs_to_expand.push(current_entry.id);
2016 }
2017
2018 if traversal.back_to_parent() {
2019 if let Some(parent_entry) = traversal.entry() {
2020 current_entry = parent_entry.clone();
2021 continue;
2022 }
2023 }
2024 break;
2025 }
2026 }
2027 for dir_to_expand in dirs_to_expand {
2028 project
2029 .update(cx, |project, cx| {
2030 project.expand_entry(worktree_id, dir_to_expand, cx)
2031 })
2032 .unwrap_or_else(|| Task::ready(Ok(())))
2033 .detach_and_log_err(cx)
2034 }
2035 })?
2036 }
2037
2038 outline_panel.update_in(cx, |outline_panel, window, cx| {
2039 outline_panel.select_entry(entry_with_selection, false, window, cx);
2040 outline_panel.update_cached_entries(None, window, cx);
2041 })?;
2042
2043 anyhow::Ok(())
2044 });
2045 }
2046
2047 fn render_excerpt(
2048 &self,
2049 excerpt: &OutlineEntryExcerpt,
2050 depth: usize,
2051 window: &mut Window,
2052 cx: &mut Context<OutlinePanel>,
2053 ) -> Option<Stateful<Div>> {
2054 let item_id = ElementId::from(excerpt.id.to_proto() as usize);
2055 let is_active = match self.selected_entry() {
2056 Some(PanelEntry::Outline(OutlineEntry::Excerpt(selected_excerpt))) => {
2057 selected_excerpt.buffer_id == excerpt.buffer_id && selected_excerpt.id == excerpt.id
2058 }
2059 _ => false,
2060 };
2061 let has_outlines = self
2062 .excerpts
2063 .get(&excerpt.buffer_id)
2064 .and_then(|excerpts| match &excerpts.get(&excerpt.id)?.outlines {
2065 ExcerptOutlines::Outlines(outlines) => Some(outlines),
2066 ExcerptOutlines::Invalidated(outlines) => Some(outlines),
2067 ExcerptOutlines::NotFetched => None,
2068 })
2069 .map_or(false, |outlines| !outlines.is_empty());
2070 let is_expanded = !self
2071 .collapsed_entries
2072 .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id));
2073 let color = entry_label_color(is_active);
2074 let icon = if has_outlines {
2075 FileIcons::get_chevron_icon(is_expanded, cx)
2076 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2077 } else {
2078 None
2079 }
2080 .unwrap_or_else(empty_icon);
2081
2082 let label = self.excerpt_label(excerpt.buffer_id, &excerpt.range, cx)?;
2083 let label_element = Label::new(label)
2084 .single_line()
2085 .color(color)
2086 .into_any_element();
2087
2088 Some(self.entry_element(
2089 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())),
2090 item_id,
2091 depth,
2092 Some(icon),
2093 is_active,
2094 label_element,
2095 window,
2096 cx,
2097 ))
2098 }
2099
2100 fn excerpt_label(
2101 &self,
2102 buffer_id: BufferId,
2103 range: &ExcerptRange<language::Anchor>,
2104 cx: &App,
2105 ) -> Option<String> {
2106 let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
2107 let excerpt_range = range.context.to_point(&buffer_snapshot);
2108 Some(format!(
2109 "Lines {}- {}",
2110 excerpt_range.start.row + 1,
2111 excerpt_range.end.row + 1,
2112 ))
2113 }
2114
2115 fn render_outline(
2116 &self,
2117 outline: &OutlineEntryOutline,
2118 depth: usize,
2119 string_match: Option<&StringMatch>,
2120 window: &mut Window,
2121 cx: &mut Context<Self>,
2122 ) -> Stateful<Div> {
2123 let item_id = ElementId::from(SharedString::from(format!(
2124 "{:?}|{:?}{:?}|{:?}",
2125 outline.buffer_id, outline.excerpt_id, outline.outline.range, &outline.outline.text,
2126 )));
2127
2128 let label_element = outline::render_item(
2129 &outline.outline,
2130 string_match
2131 .map(|string_match| string_match.ranges().collect::<Vec<_>>())
2132 .unwrap_or_default(),
2133 cx,
2134 )
2135 .into_any_element();
2136
2137 let is_active = match self.selected_entry() {
2138 Some(PanelEntry::Outline(OutlineEntry::Outline(selected))) => {
2139 outline == selected && outline.outline == selected.outline
2140 }
2141 _ => false,
2142 };
2143
2144 let icon = if self.is_singleton_active(cx) {
2145 None
2146 } else {
2147 Some(empty_icon())
2148 };
2149
2150 self.entry_element(
2151 PanelEntry::Outline(OutlineEntry::Outline(outline.clone())),
2152 item_id,
2153 depth,
2154 icon,
2155 is_active,
2156 label_element,
2157 window,
2158 cx,
2159 )
2160 }
2161
2162 fn render_entry(
2163 &self,
2164 rendered_entry: &FsEntry,
2165 depth: usize,
2166 string_match: Option<&StringMatch>,
2167 window: &mut Window,
2168 cx: &mut Context<Self>,
2169 ) -> Stateful<Div> {
2170 let settings = OutlinePanelSettings::get_global(cx);
2171 let is_active = match self.selected_entry() {
2172 Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry,
2173 _ => false,
2174 };
2175 let (item_id, label_element, icon) = match rendered_entry {
2176 FsEntry::File(FsEntryFile {
2177 worktree_id, entry, ..
2178 }) => {
2179 let name = self.entry_name(worktree_id, entry, cx);
2180 let color =
2181 entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
2182 let icon = if settings.file_icons {
2183 FileIcons::get_icon(&entry.path, cx)
2184 .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2185 } else {
2186 None
2187 };
2188 (
2189 ElementId::from(entry.id.to_proto() as usize),
2190 HighlightedLabel::new(
2191 name,
2192 string_match
2193 .map(|string_match| string_match.positions.clone())
2194 .unwrap_or_default(),
2195 )
2196 .color(color)
2197 .into_any_element(),
2198 icon.unwrap_or_else(empty_icon),
2199 )
2200 }
2201 FsEntry::Directory(directory) => {
2202 let name = self.entry_name(&directory.worktree_id, &directory.entry, cx);
2203
2204 let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Dir(
2205 directory.worktree_id,
2206 directory.entry.id,
2207 ));
2208 let color = entry_git_aware_label_color(
2209 directory.entry.git_summary,
2210 directory.entry.is_ignored,
2211 is_active,
2212 );
2213 let icon = if settings.folder_icons {
2214 FileIcons::get_folder_icon(is_expanded, cx)
2215 } else {
2216 FileIcons::get_chevron_icon(is_expanded, cx)
2217 }
2218 .map(Icon::from_path)
2219 .map(|icon| icon.color(color).into_any_element());
2220 (
2221 ElementId::from(directory.entry.id.to_proto() as usize),
2222 HighlightedLabel::new(
2223 name,
2224 string_match
2225 .map(|string_match| string_match.positions.clone())
2226 .unwrap_or_default(),
2227 )
2228 .color(color)
2229 .into_any_element(),
2230 icon.unwrap_or_else(empty_icon),
2231 )
2232 }
2233 FsEntry::ExternalFile(external_file) => {
2234 let color = entry_label_color(is_active);
2235 let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) {
2236 Some(buffer_snapshot) => match buffer_snapshot.file() {
2237 Some(file) => {
2238 let path = file.path();
2239 let icon = if settings.file_icons {
2240 FileIcons::get_icon(path.as_ref(), cx)
2241 } else {
2242 None
2243 }
2244 .map(Icon::from_path)
2245 .map(|icon| icon.color(color).into_any_element());
2246 (icon, file_name(path.as_ref()))
2247 }
2248 None => (None, "Untitled".to_string()),
2249 },
2250 None => (None, "Unknown buffer".to_string()),
2251 };
2252 (
2253 ElementId::from(external_file.buffer_id.to_proto() as usize),
2254 HighlightedLabel::new(
2255 name,
2256 string_match
2257 .map(|string_match| string_match.positions.clone())
2258 .unwrap_or_default(),
2259 )
2260 .color(color)
2261 .into_any_element(),
2262 icon.unwrap_or_else(empty_icon),
2263 )
2264 }
2265 };
2266
2267 self.entry_element(
2268 PanelEntry::Fs(rendered_entry.clone()),
2269 item_id,
2270 depth,
2271 Some(icon),
2272 is_active,
2273 label_element,
2274 window,
2275 cx,
2276 )
2277 }
2278
2279 fn render_folded_dirs(
2280 &self,
2281 folded_dir: &FoldedDirsEntry,
2282 depth: usize,
2283 string_match: Option<&StringMatch>,
2284 window: &mut Window,
2285 cx: &mut Context<OutlinePanel>,
2286 ) -> Stateful<Div> {
2287 let settings = OutlinePanelSettings::get_global(cx);
2288 let is_active = match self.selected_entry() {
2289 Some(PanelEntry::FoldedDirs(selected_dirs)) => {
2290 selected_dirs.worktree_id == folded_dir.worktree_id
2291 && selected_dirs.entries == folded_dir.entries
2292 }
2293 _ => false,
2294 };
2295 let (item_id, label_element, icon) = {
2296 let name = self.dir_names_string(&folded_dir.entries, folded_dir.worktree_id, cx);
2297
2298 let is_expanded = folded_dir.entries.iter().all(|dir| {
2299 !self
2300 .collapsed_entries
2301 .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id))
2302 });
2303 let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored);
2304 let git_status = folded_dir
2305 .entries
2306 .first()
2307 .map(|entry| entry.git_summary)
2308 .unwrap_or_default();
2309 let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
2310 let icon = if settings.folder_icons {
2311 FileIcons::get_folder_icon(is_expanded, cx)
2312 } else {
2313 FileIcons::get_chevron_icon(is_expanded, cx)
2314 }
2315 .map(Icon::from_path)
2316 .map(|icon| icon.color(color).into_any_element());
2317 (
2318 ElementId::from(
2319 folded_dir
2320 .entries
2321 .last()
2322 .map(|entry| entry.id.to_proto())
2323 .unwrap_or_else(|| folded_dir.worktree_id.to_proto())
2324 as usize,
2325 ),
2326 HighlightedLabel::new(
2327 name,
2328 string_match
2329 .map(|string_match| string_match.positions.clone())
2330 .unwrap_or_default(),
2331 )
2332 .color(color)
2333 .into_any_element(),
2334 icon.unwrap_or_else(empty_icon),
2335 )
2336 };
2337
2338 self.entry_element(
2339 PanelEntry::FoldedDirs(folded_dir.clone()),
2340 item_id,
2341 depth,
2342 Some(icon),
2343 is_active,
2344 label_element,
2345 window,
2346 cx,
2347 )
2348 }
2349
2350 fn render_search_match(
2351 &mut self,
2352 multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
2353 match_range: &Range<editor::Anchor>,
2354 render_data: &Arc<OnceLock<SearchData>>,
2355 kind: SearchKind,
2356 depth: usize,
2357 string_match: Option<&StringMatch>,
2358 window: &mut Window,
2359 cx: &mut Context<Self>,
2360 ) -> Option<Stateful<Div>> {
2361 let search_data = match render_data.get() {
2362 Some(search_data) => search_data,
2363 None => {
2364 if let ItemsDisplayMode::Search(search_state) = &mut self.mode {
2365 if let Some(multi_buffer_snapshot) = multi_buffer_snapshot {
2366 search_state
2367 .highlight_search_match_tx
2368 .try_send(HighlightArguments {
2369 multi_buffer_snapshot: multi_buffer_snapshot.clone(),
2370 match_range: match_range.clone(),
2371 search_data: Arc::clone(render_data),
2372 })
2373 .ok();
2374 }
2375 }
2376 return None;
2377 }
2378 };
2379 let search_matches = string_match
2380 .iter()
2381 .flat_map(|string_match| string_match.ranges())
2382 .collect::<Vec<_>>();
2383 let match_ranges = if search_matches.is_empty() {
2384 &search_data.search_match_indices
2385 } else {
2386 &search_matches
2387 };
2388 let label_element = outline::render_item(
2389 &OutlineItem {
2390 depth,
2391 annotation_range: None,
2392 range: search_data.context_range.clone(),
2393 text: search_data.context_text.clone(),
2394 highlight_ranges: search_data
2395 .highlights_data
2396 .get()
2397 .cloned()
2398 .unwrap_or_default(),
2399 name_ranges: search_data.search_match_indices.clone(),
2400 body_range: Some(search_data.context_range.clone()),
2401 },
2402 match_ranges.iter().cloned(),
2403 cx,
2404 );
2405 let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
2406 let entire_label = h_flex()
2407 .justify_center()
2408 .p_0()
2409 .when(search_data.truncated_left, |parent| {
2410 parent.child(truncated_contents_label())
2411 })
2412 .child(label_element)
2413 .when(search_data.truncated_right, |parent| {
2414 parent.child(truncated_contents_label())
2415 })
2416 .into_any_element();
2417
2418 let is_active = match self.selected_entry() {
2419 Some(PanelEntry::Search(SearchEntry {
2420 match_range: selected_match_range,
2421 ..
2422 })) => match_range == selected_match_range,
2423 _ => false,
2424 };
2425 Some(self.entry_element(
2426 PanelEntry::Search(SearchEntry {
2427 kind,
2428 match_range: match_range.clone(),
2429 render_data: render_data.clone(),
2430 }),
2431 ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
2432 depth,
2433 None,
2434 is_active,
2435 entire_label,
2436 window,
2437 cx,
2438 ))
2439 }
2440
2441 fn entry_element(
2442 &self,
2443 rendered_entry: PanelEntry,
2444 item_id: ElementId,
2445 depth: usize,
2446 icon_element: Option<AnyElement>,
2447 is_active: bool,
2448 label_element: gpui::AnyElement,
2449 window: &mut Window,
2450 cx: &mut Context<OutlinePanel>,
2451 ) -> Stateful<Div> {
2452 let settings = OutlinePanelSettings::get_global(cx);
2453 div()
2454 .text_ui(cx)
2455 .id(item_id.clone())
2456 .on_click({
2457 let clicked_entry = rendered_entry.clone();
2458 cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| {
2459 if event.down.button == MouseButton::Right || event.down.first_mouse {
2460 return;
2461 }
2462 let change_focus = event.down.click_count > 1;
2463 outline_panel.toggle_expanded(&clicked_entry, window, cx);
2464 outline_panel.scroll_editor_to_entry(
2465 &clicked_entry,
2466 true,
2467 change_focus,
2468 window,
2469 cx,
2470 );
2471 })
2472 })
2473 .cursor_pointer()
2474 .child(
2475 ListItem::new(item_id)
2476 .indent_level(depth)
2477 .indent_step_size(px(settings.indent_size))
2478 .toggle_state(is_active)
2479 .when_some(icon_element, |list_item, icon_element| {
2480 list_item.child(h_flex().child(icon_element))
2481 })
2482 .child(h_flex().h_6().child(label_element).ml_1())
2483 .on_secondary_mouse_down(cx.listener(
2484 move |outline_panel, event: &MouseDownEvent, window, cx| {
2485 // Stop propagation to prevent the catch-all context menu for the project
2486 // panel from being deployed.
2487 cx.stop_propagation();
2488 outline_panel.deploy_context_menu(
2489 event.position,
2490 rendered_entry.clone(),
2491 window,
2492 cx,
2493 )
2494 },
2495 )),
2496 )
2497 .border_1()
2498 .border_r_2()
2499 .rounded_none()
2500 .hover(|style| {
2501 if is_active {
2502 style
2503 } else {
2504 let hover_color = cx.theme().colors().ghost_element_hover;
2505 style.bg(hover_color).border_color(hover_color)
2506 }
2507 })
2508 .when(
2509 is_active && self.focus_handle.contains_focused(window, cx),
2510 |div| div.border_color(Color::Selected.color(cx)),
2511 )
2512 }
2513
2514 fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String {
2515 let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2516 Some(worktree) => {
2517 let worktree = worktree.read(cx);
2518 match worktree.snapshot().root_entry() {
2519 Some(root_entry) => {
2520 if root_entry.id == entry.id {
2521 file_name(worktree.abs_path().as_ref())
2522 } else {
2523 let path = worktree.absolutize(entry.path.as_ref()).ok();
2524 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2525 file_name(path)
2526 }
2527 }
2528 None => {
2529 let path = worktree.absolutize(entry.path.as_ref()).ok();
2530 let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2531 file_name(path)
2532 }
2533 }
2534 }
2535 None => file_name(entry.path.as_ref()),
2536 };
2537 name
2538 }
2539
2540 fn update_fs_entries(
2541 &mut self,
2542 active_editor: Entity<Editor>,
2543 debounce: Option<Duration>,
2544 window: &mut Window,
2545 cx: &mut Context<Self>,
2546 ) {
2547 if !self.active {
2548 return;
2549 }
2550
2551 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2552 let active_multi_buffer = active_editor.read(cx).buffer().clone();
2553 let new_entries = self.new_entries_for_fs_update.clone();
2554 let repo_snapshots = self.project.update(cx, |project, cx| {
2555 project.git_store().read(cx).repo_snapshots(cx)
2556 });
2557 self.updating_fs_entries = true;
2558 self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
2559 if let Some(debounce) = debounce {
2560 cx.background_executor().timer(debounce).await;
2561 }
2562
2563 let mut new_collapsed_entries = HashSet::default();
2564 let mut new_unfolded_dirs = HashMap::default();
2565 let mut root_entries = HashSet::default();
2566 let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
2567 let Ok(buffer_excerpts) = outline_panel.update(cx, |outline_panel, cx| {
2568 let git_store = outline_panel.project.read(cx).git_store().clone();
2569 new_collapsed_entries = outline_panel.collapsed_entries.clone();
2570 new_unfolded_dirs = outline_panel.unfolded_dirs.clone();
2571 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
2572 let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
2573 HashMap::default(),
2574 |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
2575 let buffer_id = buffer_snapshot.remote_id();
2576 let file = File::from_dyn(buffer_snapshot.file());
2577 let entry_id = file.and_then(|file| file.project_entry_id(cx));
2578 let worktree = file.map(|file| file.worktree.read(cx).snapshot());
2579 let is_new = new_entries.contains(&excerpt_id)
2580 || !outline_panel.excerpts.contains_key(&buffer_id);
2581 let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
2582 let status = git_store
2583 .read(cx)
2584 .repository_and_path_for_buffer_id(buffer_id, cx)
2585 .and_then(|(repo, path)| {
2586 Some(repo.read(cx).status_for_path(&path)?.status)
2587 });
2588 buffer_excerpts
2589 .entry(buffer_id)
2590 .or_insert_with(|| {
2591 (is_new, is_folded, Vec::new(), entry_id, worktree, status)
2592 })
2593 .2
2594 .push(excerpt_id);
2595
2596 let outlines = match outline_panel
2597 .excerpts
2598 .get(&buffer_id)
2599 .and_then(|excerpts| excerpts.get(&excerpt_id))
2600 {
2601 Some(old_excerpt) => match &old_excerpt.outlines {
2602 ExcerptOutlines::Outlines(outlines) => {
2603 ExcerptOutlines::Outlines(outlines.clone())
2604 }
2605 ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
2606 ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
2607 },
2608 None => ExcerptOutlines::NotFetched,
2609 };
2610 new_excerpts.entry(buffer_id).or_default().insert(
2611 excerpt_id,
2612 Excerpt {
2613 range: excerpt_range,
2614 outlines,
2615 },
2616 );
2617 buffer_excerpts
2618 },
2619 );
2620 buffer_excerpts
2621 }) else {
2622 return;
2623 };
2624
2625 let Some((
2626 new_collapsed_entries,
2627 new_unfolded_dirs,
2628 new_fs_entries,
2629 new_depth_map,
2630 new_children_count,
2631 )) = cx
2632 .background_spawn(async move {
2633 let mut processed_external_buffers = HashSet::default();
2634 let mut new_worktree_entries =
2635 BTreeMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
2636 let mut worktree_excerpts = HashMap::<
2637 WorktreeId,
2638 HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
2639 >::default();
2640 let mut external_excerpts = HashMap::default();
2641
2642 for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree, status)) in
2643 buffer_excerpts
2644 {
2645 if is_folded {
2646 match &worktree {
2647 Some(worktree) => {
2648 new_collapsed_entries
2649 .insert(CollapsedEntry::File(worktree.id(), buffer_id));
2650 }
2651 None => {
2652 new_collapsed_entries
2653 .insert(CollapsedEntry::ExternalFile(buffer_id));
2654 }
2655 }
2656 } else if is_new {
2657 match &worktree {
2658 Some(worktree) => {
2659 new_collapsed_entries
2660 .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
2661 }
2662 None => {
2663 new_collapsed_entries
2664 .remove(&CollapsedEntry::ExternalFile(buffer_id));
2665 }
2666 }
2667 }
2668
2669 if let Some(worktree) = worktree {
2670 let worktree_id = worktree.id();
2671 let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
2672
2673 match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
2674 Some(entry) => {
2675 let entry = GitEntry {
2676 git_summary: status
2677 .map(|status| status.summary())
2678 .unwrap_or_default(),
2679 entry,
2680 };
2681 let mut traversal = GitTraversal::new(
2682 &repo_snapshots,
2683 worktree.traverse_from_path(
2684 true,
2685 true,
2686 true,
2687 entry.path.as_ref(),
2688 ),
2689 );
2690
2691 let mut entries_to_add = HashMap::default();
2692 worktree_excerpts
2693 .entry(worktree_id)
2694 .or_default()
2695 .insert(entry.id, (buffer_id, excerpts));
2696 let mut current_entry = entry;
2697 loop {
2698 if current_entry.is_dir() {
2699 let is_root =
2700 worktree.root_entry().map(|entry| entry.id)
2701 == Some(current_entry.id);
2702 if is_root {
2703 root_entries.insert(current_entry.id);
2704 if auto_fold_dirs {
2705 unfolded_dirs.insert(current_entry.id);
2706 }
2707 }
2708 if is_new {
2709 new_collapsed_entries.remove(&CollapsedEntry::Dir(
2710 worktree_id,
2711 current_entry.id,
2712 ));
2713 }
2714 }
2715
2716 let new_entry_added = entries_to_add
2717 .insert(current_entry.id, current_entry)
2718 .is_none();
2719 if new_entry_added && traversal.back_to_parent() {
2720 if let Some(parent_entry) = traversal.entry() {
2721 current_entry = parent_entry.to_owned();
2722 continue;
2723 }
2724 }
2725 break;
2726 }
2727 new_worktree_entries
2728 .entry(worktree_id)
2729 .or_insert_with(HashMap::default)
2730 .extend(entries_to_add);
2731 }
2732 None => {
2733 if processed_external_buffers.insert(buffer_id) {
2734 external_excerpts
2735 .entry(buffer_id)
2736 .or_insert_with(Vec::new)
2737 .extend(excerpts);
2738 }
2739 }
2740 }
2741 } else if processed_external_buffers.insert(buffer_id) {
2742 external_excerpts
2743 .entry(buffer_id)
2744 .or_insert_with(Vec::new)
2745 .extend(excerpts);
2746 }
2747 }
2748
2749 let mut new_children_count =
2750 HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
2751
2752 let worktree_entries = new_worktree_entries
2753 .into_iter()
2754 .map(|(worktree_id, entries)| {
2755 let mut entries = entries.into_values().collect::<Vec<_>>();
2756 entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
2757 (worktree_id, entries)
2758 })
2759 .flat_map(|(worktree_id, entries)| {
2760 {
2761 entries
2762 .into_iter()
2763 .filter_map(|entry| {
2764 if auto_fold_dirs {
2765 if let Some(parent) = entry.path.parent() {
2766 let children = new_children_count
2767 .entry(worktree_id)
2768 .or_default()
2769 .entry(Arc::from(parent))
2770 .or_default();
2771 if entry.is_dir() {
2772 children.dirs += 1;
2773 } else {
2774 children.files += 1;
2775 }
2776 }
2777 }
2778
2779 if entry.is_dir() {
2780 Some(FsEntry::Directory(FsEntryDirectory {
2781 worktree_id,
2782 entry,
2783 }))
2784 } else {
2785 let (buffer_id, excerpts) = worktree_excerpts
2786 .get_mut(&worktree_id)
2787 .and_then(|worktree_excerpts| {
2788 worktree_excerpts.remove(&entry.id)
2789 })?;
2790 Some(FsEntry::File(FsEntryFile {
2791 worktree_id,
2792 buffer_id,
2793 entry,
2794 excerpts,
2795 }))
2796 }
2797 })
2798 .collect::<Vec<_>>()
2799 }
2800 })
2801 .collect::<Vec<_>>();
2802
2803 let mut visited_dirs = Vec::new();
2804 let mut new_depth_map = HashMap::default();
2805 let new_visible_entries = external_excerpts
2806 .into_iter()
2807 .sorted_by_key(|(id, _)| *id)
2808 .map(|(buffer_id, excerpts)| {
2809 FsEntry::ExternalFile(FsEntryExternalFile {
2810 buffer_id,
2811 excerpts,
2812 })
2813 })
2814 .chain(worktree_entries)
2815 .filter(|visible_item| {
2816 match visible_item {
2817 FsEntry::Directory(directory) => {
2818 let parent_id = back_to_common_visited_parent(
2819 &mut visited_dirs,
2820 &directory.worktree_id,
2821 &directory.entry,
2822 );
2823
2824 let mut depth = 0;
2825 if !root_entries.contains(&directory.entry.id) {
2826 if auto_fold_dirs {
2827 let children = new_children_count
2828 .get(&directory.worktree_id)
2829 .and_then(|children_count| {
2830 children_count.get(&directory.entry.path)
2831 })
2832 .copied()
2833 .unwrap_or_default();
2834
2835 if !children.may_be_fold_part()
2836 || (children.dirs == 0
2837 && visited_dirs
2838 .last()
2839 .map(|(parent_dir_id, _)| {
2840 new_unfolded_dirs
2841 .get(&directory.worktree_id)
2842 .map_or(true, |unfolded_dirs| {
2843 unfolded_dirs
2844 .contains(parent_dir_id)
2845 })
2846 })
2847 .unwrap_or(true))
2848 {
2849 new_unfolded_dirs
2850 .entry(directory.worktree_id)
2851 .or_default()
2852 .insert(directory.entry.id);
2853 }
2854 }
2855
2856 depth = parent_id
2857 .and_then(|(worktree_id, id)| {
2858 new_depth_map.get(&(worktree_id, id)).copied()
2859 })
2860 .unwrap_or(0)
2861 + 1;
2862 };
2863 visited_dirs
2864 .push((directory.entry.id, directory.entry.path.clone()));
2865 new_depth_map
2866 .insert((directory.worktree_id, directory.entry.id), depth);
2867 }
2868 FsEntry::File(FsEntryFile {
2869 worktree_id,
2870 entry: file_entry,
2871 ..
2872 }) => {
2873 let parent_id = back_to_common_visited_parent(
2874 &mut visited_dirs,
2875 worktree_id,
2876 file_entry,
2877 );
2878 let depth = if root_entries.contains(&file_entry.id) {
2879 0
2880 } else {
2881 parent_id
2882 .and_then(|(worktree_id, id)| {
2883 new_depth_map.get(&(worktree_id, id)).copied()
2884 })
2885 .unwrap_or(0)
2886 + 1
2887 };
2888 new_depth_map.insert((*worktree_id, file_entry.id), depth);
2889 }
2890 FsEntry::ExternalFile(..) => {
2891 visited_dirs.clear();
2892 }
2893 }
2894
2895 true
2896 })
2897 .collect::<Vec<_>>();
2898
2899 anyhow::Ok((
2900 new_collapsed_entries,
2901 new_unfolded_dirs,
2902 new_visible_entries,
2903 new_depth_map,
2904 new_children_count,
2905 ))
2906 })
2907 .await
2908 .log_err()
2909 else {
2910 return;
2911 };
2912
2913 outline_panel
2914 .update_in(cx, |outline_panel, window, cx| {
2915 outline_panel.updating_fs_entries = false;
2916 outline_panel.new_entries_for_fs_update.clear();
2917 outline_panel.excerpts = new_excerpts;
2918 outline_panel.collapsed_entries = new_collapsed_entries;
2919 outline_panel.unfolded_dirs = new_unfolded_dirs;
2920 outline_panel.fs_entries = new_fs_entries;
2921 outline_panel.fs_entries_depth = new_depth_map;
2922 outline_panel.fs_children_count = new_children_count;
2923 outline_panel.update_non_fs_items(window, cx);
2924 outline_panel.update_cached_entries(debounce, window, cx);
2925
2926 cx.notify();
2927 })
2928 .ok();
2929 });
2930 }
2931
2932 fn replace_active_editor(
2933 &mut self,
2934 new_active_item: Box<dyn ItemHandle>,
2935 new_active_editor: Entity<Editor>,
2936 window: &mut Window,
2937 cx: &mut Context<Self>,
2938 ) {
2939 self.clear_previous(window, cx);
2940 let buffer_search_subscription = cx.subscribe_in(
2941 &new_active_editor,
2942 window,
2943 |outline_panel: &mut Self,
2944 _,
2945 e: &SearchEvent,
2946 window: &mut Window,
2947 cx: &mut Context<Self>| {
2948 if matches!(e, SearchEvent::MatchesInvalidated) {
2949 let update_cached_items = outline_panel.update_search_matches(window, cx);
2950 if update_cached_items {
2951 outline_panel.selected_entry.invalidate();
2952 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
2953 }
2954 };
2955 outline_panel.autoscroll(cx);
2956 },
2957 );
2958 self.active_item = Some(ActiveItem {
2959 _buffer_search_subscription: buffer_search_subscription,
2960 _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, window, cx),
2961 item_handle: new_active_item.downgrade_item(),
2962 active_editor: new_active_editor.downgrade(),
2963 });
2964 self.new_entries_for_fs_update
2965 .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
2966 self.selected_entry.invalidate();
2967 self.update_fs_entries(new_active_editor, None, window, cx);
2968 }
2969
2970 fn clear_previous(&mut self, window: &mut Window, cx: &mut App) {
2971 self.fs_entries_update_task = Task::ready(());
2972 self.outline_fetch_tasks.clear();
2973 self.cached_entries_update_task = Task::ready(());
2974 self.reveal_selection_task = Task::ready(Ok(()));
2975 self.filter_editor
2976 .update(cx, |editor, cx| editor.clear(window, cx));
2977 self.collapsed_entries.clear();
2978 self.unfolded_dirs.clear();
2979 self.active_item = None;
2980 self.fs_entries.clear();
2981 self.fs_entries_depth.clear();
2982 self.fs_children_count.clear();
2983 self.excerpts.clear();
2984 self.cached_entries = Vec::new();
2985 self.selected_entry = SelectedEntry::None;
2986 self.pinned = false;
2987 self.mode = ItemsDisplayMode::Outline;
2988 }
2989
2990 fn location_for_editor_selection(
2991 &self,
2992 editor: &Entity<Editor>,
2993 window: &mut Window,
2994 cx: &mut Context<Self>,
2995 ) -> Option<PanelEntry> {
2996 let selection = editor.update(cx, |editor, cx| {
2997 editor.selections.newest::<language::Point>(cx).head()
2998 });
2999 let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
3000 let multi_buffer = editor.read(cx).buffer();
3001 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
3002 let (excerpt_id, buffer, _) = editor
3003 .read(cx)
3004 .buffer()
3005 .read(cx)
3006 .excerpt_containing(selection, cx)?;
3007 let buffer_id = buffer.read(cx).remote_id();
3008
3009 if editor.read(cx).is_buffer_folded(buffer_id, cx) {
3010 return self
3011 .fs_entries
3012 .iter()
3013 .find(|fs_entry| match fs_entry {
3014 FsEntry::Directory(..) => false,
3015 FsEntry::File(FsEntryFile {
3016 buffer_id: other_buffer_id,
3017 ..
3018 })
3019 | FsEntry::ExternalFile(FsEntryExternalFile {
3020 buffer_id: other_buffer_id,
3021 ..
3022 }) => buffer_id == *other_buffer_id,
3023 })
3024 .cloned()
3025 .map(PanelEntry::Fs);
3026 }
3027
3028 let selection_display_point = selection.to_display_point(&editor_snapshot);
3029
3030 match &self.mode {
3031 ItemsDisplayMode::Search(search_state) => search_state
3032 .matches
3033 .iter()
3034 .rev()
3035 .min_by_key(|&(match_range, _)| {
3036 let match_display_range =
3037 match_range.clone().to_display_points(&editor_snapshot);
3038 let start_distance = if selection_display_point < match_display_range.start {
3039 match_display_range.start - selection_display_point
3040 } else {
3041 selection_display_point - match_display_range.start
3042 };
3043 let end_distance = if selection_display_point < match_display_range.end {
3044 match_display_range.end - selection_display_point
3045 } else {
3046 selection_display_point - match_display_range.end
3047 };
3048 start_distance + end_distance
3049 })
3050 .and_then(|(closest_range, _)| {
3051 self.cached_entries.iter().find_map(|cached_entry| {
3052 if let PanelEntry::Search(SearchEntry { match_range, .. }) =
3053 &cached_entry.entry
3054 {
3055 if match_range == closest_range {
3056 Some(cached_entry.entry.clone())
3057 } else {
3058 None
3059 }
3060 } else {
3061 None
3062 }
3063 })
3064 }),
3065 ItemsDisplayMode::Outline => self.outline_location(
3066 buffer_id,
3067 excerpt_id,
3068 multi_buffer_snapshot,
3069 editor_snapshot,
3070 selection_display_point,
3071 ),
3072 }
3073 }
3074
3075 fn outline_location(
3076 &self,
3077 buffer_id: BufferId,
3078 excerpt_id: ExcerptId,
3079 multi_buffer_snapshot: editor::MultiBufferSnapshot,
3080 editor_snapshot: editor::EditorSnapshot,
3081 selection_display_point: DisplayPoint,
3082 ) -> Option<PanelEntry> {
3083 let excerpt_outlines = self
3084 .excerpts
3085 .get(&buffer_id)
3086 .and_then(|excerpts| excerpts.get(&excerpt_id))
3087 .into_iter()
3088 .flat_map(|excerpt| excerpt.iter_outlines())
3089 .flat_map(|outline| {
3090 let start = multi_buffer_snapshot
3091 .anchor_in_excerpt(excerpt_id, outline.range.start)?
3092 .to_display_point(&editor_snapshot);
3093 let end = multi_buffer_snapshot
3094 .anchor_in_excerpt(excerpt_id, outline.range.end)?
3095 .to_display_point(&editor_snapshot);
3096 Some((start..end, outline))
3097 })
3098 .collect::<Vec<_>>();
3099
3100 let mut matching_outline_indices = Vec::new();
3101 let mut children = HashMap::default();
3102 let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
3103
3104 for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
3105 if outline_range
3106 .to_inclusive()
3107 .contains(&selection_display_point)
3108 {
3109 matching_outline_indices.push(i);
3110 } else if (outline_range.start.row()..outline_range.end.row())
3111 .to_inclusive()
3112 .contains(&selection_display_point.row())
3113 {
3114 matching_outline_indices.push(i);
3115 }
3116
3117 while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
3118 if parent_outline.depth >= outline.depth
3119 || !parent_range.contains(&outline_range.start)
3120 {
3121 parents_stack.pop();
3122 } else {
3123 break;
3124 }
3125 }
3126 if let Some((_, _, parent_index)) = parents_stack.last_mut() {
3127 children
3128 .entry(*parent_index)
3129 .or_insert_with(Vec::new)
3130 .push(i);
3131 }
3132 parents_stack.push((outline_range, outline, i));
3133 }
3134
3135 let outline_item = matching_outline_indices
3136 .into_iter()
3137 .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
3138 .filter(|(i, _)| {
3139 children
3140 .get(i)
3141 .map(|children| {
3142 children.iter().all(|child_index| {
3143 excerpt_outlines
3144 .get(*child_index)
3145 .map(|(child_range, _)| child_range.start > selection_display_point)
3146 .unwrap_or(false)
3147 })
3148 })
3149 .unwrap_or(true)
3150 })
3151 .min_by_key(|(_, (outline_range, outline))| {
3152 let distance_from_start = if outline_range.start > selection_display_point {
3153 outline_range.start - selection_display_point
3154 } else {
3155 selection_display_point - outline_range.start
3156 };
3157 let distance_from_end = if outline_range.end > selection_display_point {
3158 outline_range.end - selection_display_point
3159 } else {
3160 selection_display_point - outline_range.end
3161 };
3162
3163 (
3164 cmp::Reverse(outline.depth),
3165 distance_from_start + distance_from_end,
3166 )
3167 })
3168 .map(|(_, (_, outline))| *outline)
3169 .cloned();
3170
3171 let closest_container = match outline_item {
3172 Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
3173 buffer_id,
3174 excerpt_id,
3175 outline,
3176 })),
3177 None => {
3178 self.cached_entries.iter().rev().find_map(|cached_entry| {
3179 match &cached_entry.entry {
3180 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
3181 if excerpt.buffer_id == buffer_id && excerpt.id == excerpt_id {
3182 Some(cached_entry.entry.clone())
3183 } else {
3184 None
3185 }
3186 }
3187 PanelEntry::Fs(
3188 FsEntry::ExternalFile(FsEntryExternalFile {
3189 buffer_id: file_buffer_id,
3190 excerpts: file_excerpts,
3191 })
3192 | FsEntry::File(FsEntryFile {
3193 buffer_id: file_buffer_id,
3194 excerpts: file_excerpts,
3195 ..
3196 }),
3197 ) => {
3198 if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
3199 Some(cached_entry.entry.clone())
3200 } else {
3201 None
3202 }
3203 }
3204 _ => None,
3205 }
3206 })?
3207 }
3208 };
3209 Some(closest_container)
3210 }
3211
3212 fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3213 let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
3214 if excerpt_fetch_ranges.is_empty() {
3215 return;
3216 }
3217
3218 let syntax_theme = cx.theme().syntax().clone();
3219 let first_update = Arc::new(AtomicBool::new(true));
3220 for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
3221 for (excerpt_id, excerpt_range) in excerpt_ranges {
3222 let syntax_theme = syntax_theme.clone();
3223 let buffer_snapshot = buffer_snapshot.clone();
3224 let first_update = first_update.clone();
3225 self.outline_fetch_tasks.insert(
3226 (buffer_id, excerpt_id),
3227 cx.spawn_in(window, async move |outline_panel, cx| {
3228 let fetched_outlines = cx
3229 .background_spawn(async move {
3230 buffer_snapshot
3231 .outline_items_containing(
3232 excerpt_range.context,
3233 false,
3234 Some(&syntax_theme),
3235 )
3236 .unwrap_or_default()
3237 })
3238 .await;
3239 outline_panel
3240 .update_in(cx, |outline_panel, window, cx| {
3241 if let Some(excerpt) = outline_panel
3242 .excerpts
3243 .entry(buffer_id)
3244 .or_default()
3245 .get_mut(&excerpt_id)
3246 {
3247 let debounce = if first_update
3248 .fetch_and(false, atomic::Ordering::AcqRel)
3249 {
3250 None
3251 } else {
3252 Some(UPDATE_DEBOUNCE)
3253 };
3254 excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
3255 outline_panel.update_cached_entries(debounce, window, cx);
3256 }
3257 })
3258 .ok();
3259 }),
3260 );
3261 }
3262 }
3263 }
3264
3265 fn is_singleton_active(&self, cx: &App) -> bool {
3266 self.active_editor().map_or(false, |active_editor| {
3267 active_editor.read(cx).buffer().read(cx).is_singleton()
3268 })
3269 }
3270
3271 fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
3272 self.outline_fetch_tasks.clear();
3273 let mut ids = ids.iter().collect::<HashSet<_>>();
3274 for excerpts in self.excerpts.values_mut() {
3275 ids.retain(|id| {
3276 if let Some(excerpt) = excerpts.get_mut(id) {
3277 excerpt.invalidate_outlines();
3278 false
3279 } else {
3280 true
3281 }
3282 });
3283 if ids.is_empty() {
3284 break;
3285 }
3286 }
3287 }
3288
3289 fn excerpt_fetch_ranges(
3290 &self,
3291 cx: &App,
3292 ) -> HashMap<
3293 BufferId,
3294 (
3295 BufferSnapshot,
3296 HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
3297 ),
3298 > {
3299 self.fs_entries
3300 .iter()
3301 .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
3302 match fs_entry {
3303 FsEntry::File(FsEntryFile {
3304 buffer_id,
3305 excerpts: file_excerpts,
3306 ..
3307 })
3308 | FsEntry::ExternalFile(FsEntryExternalFile {
3309 buffer_id,
3310 excerpts: file_excerpts,
3311 }) => {
3312 let excerpts = self.excerpts.get(buffer_id);
3313 for &file_excerpt in file_excerpts {
3314 if let Some(excerpt) = excerpts
3315 .and_then(|excerpts| excerpts.get(&file_excerpt))
3316 .filter(|excerpt| excerpt.should_fetch_outlines())
3317 {
3318 match excerpts_to_fetch.entry(*buffer_id) {
3319 hash_map::Entry::Occupied(mut o) => {
3320 o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
3321 }
3322 hash_map::Entry::Vacant(v) => {
3323 if let Some(buffer_snapshot) =
3324 self.buffer_snapshot_for_id(*buffer_id, cx)
3325 {
3326 v.insert((buffer_snapshot, HashMap::default()))
3327 .1
3328 .insert(file_excerpt, excerpt.range.clone());
3329 }
3330 }
3331 }
3332 }
3333 }
3334 }
3335 FsEntry::Directory(..) => {}
3336 }
3337 excerpts_to_fetch
3338 })
3339 }
3340
3341 fn buffer_snapshot_for_id(&self, buffer_id: BufferId, cx: &App) -> Option<BufferSnapshot> {
3342 let editor = self.active_editor()?;
3343 Some(
3344 editor
3345 .read(cx)
3346 .buffer()
3347 .read(cx)
3348 .buffer(buffer_id)?
3349 .read(cx)
3350 .snapshot(),
3351 )
3352 }
3353
3354 fn abs_path(&self, entry: &PanelEntry, cx: &App) -> Option<PathBuf> {
3355 match entry {
3356 PanelEntry::Fs(
3357 FsEntry::File(FsEntryFile { buffer_id, .. })
3358 | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }),
3359 ) => self
3360 .buffer_snapshot_for_id(*buffer_id, cx)
3361 .and_then(|buffer_snapshot| {
3362 let file = File::from_dyn(buffer_snapshot.file())?;
3363 file.worktree.read(cx).absolutize(&file.path).ok()
3364 }),
3365 PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3366 worktree_id, entry, ..
3367 })) => self
3368 .project
3369 .read(cx)
3370 .worktree_for_id(*worktree_id, cx)?
3371 .read(cx)
3372 .absolutize(&entry.path)
3373 .ok(),
3374 PanelEntry::FoldedDirs(FoldedDirsEntry {
3375 worktree_id,
3376 entries: dirs,
3377 ..
3378 }) => dirs.last().and_then(|entry| {
3379 self.project
3380 .read(cx)
3381 .worktree_for_id(*worktree_id, cx)
3382 .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
3383 }),
3384 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
3385 }
3386 }
3387
3388 fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<Path>> {
3389 match entry {
3390 FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3391 let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
3392 Some(buffer_snapshot.file()?.path().clone())
3393 }
3394 FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()),
3395 FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()),
3396 }
3397 }
3398
3399 fn update_cached_entries(
3400 &mut self,
3401 debounce: Option<Duration>,
3402 window: &mut Window,
3403 cx: &mut Context<OutlinePanel>,
3404 ) {
3405 if !self.active {
3406 return;
3407 }
3408
3409 let is_singleton = self.is_singleton_active(cx);
3410 let query = self.query(cx);
3411 self.updating_cached_entries = true;
3412 self.cached_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
3413 if let Some(debounce) = debounce {
3414 cx.background_executor().timer(debounce).await;
3415 }
3416 let Some(new_cached_entries) = outline_panel
3417 .update_in(cx, |outline_panel, window, cx| {
3418 outline_panel.generate_cached_entries(is_singleton, query, window, cx)
3419 })
3420 .ok()
3421 else {
3422 return;
3423 };
3424 let (new_cached_entries, max_width_item_index) = new_cached_entries.await;
3425 outline_panel
3426 .update_in(cx, |outline_panel, window, cx| {
3427 outline_panel.cached_entries = new_cached_entries;
3428 outline_panel.max_width_item_index = max_width_item_index;
3429 if outline_panel.selected_entry.is_invalidated()
3430 || matches!(outline_panel.selected_entry, SelectedEntry::None)
3431 {
3432 if let Some(new_selected_entry) =
3433 outline_panel.active_editor().and_then(|active_editor| {
3434 outline_panel.location_for_editor_selection(
3435 &active_editor,
3436 window,
3437 cx,
3438 )
3439 })
3440 {
3441 outline_panel.select_entry(new_selected_entry, false, window, cx);
3442 }
3443 }
3444
3445 outline_panel.autoscroll(cx);
3446 outline_panel.updating_cached_entries = false;
3447 cx.notify();
3448 })
3449 .ok();
3450 });
3451 }
3452
3453 fn generate_cached_entries(
3454 &self,
3455 is_singleton: bool,
3456 query: Option<String>,
3457 window: &mut Window,
3458 cx: &mut Context<Self>,
3459 ) -> Task<(Vec<CachedEntry>, Option<usize>)> {
3460 let project = self.project.clone();
3461 let Some(active_editor) = self.active_editor() else {
3462 return Task::ready((Vec::new(), None));
3463 };
3464 cx.spawn_in(window, async move |outline_panel, cx| {
3465 let mut generation_state = GenerationState::default();
3466
3467 let Ok(()) = outline_panel.update(cx, |outline_panel, cx| {
3468 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
3469 let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>;
3470 let track_matches = query.is_some();
3471
3472 #[derive(Debug)]
3473 struct ParentStats {
3474 path: Arc<Path>,
3475 folded: bool,
3476 expanded: bool,
3477 depth: usize,
3478 }
3479 let mut parent_dirs = Vec::<ParentStats>::new();
3480 for entry in outline_panel.fs_entries.clone() {
3481 let is_expanded = outline_panel.is_expanded(&entry);
3482 let (depth, should_add) = match &entry {
3483 FsEntry::Directory(directory_entry) => {
3484 let mut should_add = true;
3485 let is_root = project
3486 .read(cx)
3487 .worktree_for_id(directory_entry.worktree_id, cx)
3488 .map_or(false, |worktree| {
3489 worktree.read(cx).root_entry() == Some(&directory_entry.entry)
3490 });
3491 let folded = auto_fold_dirs
3492 && !is_root
3493 && outline_panel
3494 .unfolded_dirs
3495 .get(&directory_entry.worktree_id)
3496 .map_or(true, |unfolded_dirs| {
3497 !unfolded_dirs.contains(&directory_entry.entry.id)
3498 });
3499 let fs_depth = outline_panel
3500 .fs_entries_depth
3501 .get(&(directory_entry.worktree_id, directory_entry.entry.id))
3502 .copied()
3503 .unwrap_or(0);
3504 while let Some(parent) = parent_dirs.last() {
3505 if !is_root && directory_entry.entry.path.starts_with(&parent.path)
3506 {
3507 break;
3508 }
3509 parent_dirs.pop();
3510 }
3511 let auto_fold = match parent_dirs.last() {
3512 Some(parent) => {
3513 parent.folded
3514 && Some(parent.path.as_ref())
3515 == directory_entry.entry.path.parent()
3516 && outline_panel
3517 .fs_children_count
3518 .get(&directory_entry.worktree_id)
3519 .and_then(|entries| {
3520 entries.get(&directory_entry.entry.path)
3521 })
3522 .copied()
3523 .unwrap_or_default()
3524 .may_be_fold_part()
3525 }
3526 None => false,
3527 };
3528 let folded = folded || auto_fold;
3529 let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
3530 Some(parent) => {
3531 let parent_folded = parent.folded;
3532 let parent_expanded = parent.expanded;
3533 let new_depth = if parent_folded {
3534 parent.depth
3535 } else {
3536 parent.depth + 1
3537 };
3538 parent_dirs.push(ParentStats {
3539 path: directory_entry.entry.path.clone(),
3540 folded,
3541 expanded: parent_expanded && is_expanded,
3542 depth: new_depth,
3543 });
3544 (new_depth, parent_expanded, parent_folded)
3545 }
3546 None => {
3547 parent_dirs.push(ParentStats {
3548 path: directory_entry.entry.path.clone(),
3549 folded,
3550 expanded: is_expanded,
3551 depth: fs_depth,
3552 });
3553 (fs_depth, true, false)
3554 }
3555 };
3556
3557 if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take()
3558 {
3559 if folded
3560 && directory_entry.worktree_id == folded_dirs.worktree_id
3561 && directory_entry.entry.path.parent()
3562 == folded_dirs
3563 .entries
3564 .last()
3565 .map(|entry| entry.path.as_ref())
3566 {
3567 folded_dirs.entries.push(directory_entry.entry.clone());
3568 folded_dirs_entry = Some((folded_depth, folded_dirs))
3569 } else {
3570 if !is_singleton {
3571 let start_of_collapsed_dir_sequence = !parent_expanded
3572 && parent_dirs
3573 .iter()
3574 .rev()
3575 .nth(folded_dirs.entries.len() + 1)
3576 .map_or(true, |parent| parent.expanded);
3577 if start_of_collapsed_dir_sequence
3578 || parent_expanded
3579 || query.is_some()
3580 {
3581 if parent_folded {
3582 folded_dirs
3583 .entries
3584 .push(directory_entry.entry.clone());
3585 should_add = false;
3586 }
3587 let new_folded_dirs =
3588 PanelEntry::FoldedDirs(folded_dirs.clone());
3589 outline_panel.push_entry(
3590 &mut generation_state,
3591 track_matches,
3592 new_folded_dirs,
3593 folded_depth,
3594 cx,
3595 );
3596 }
3597 }
3598
3599 folded_dirs_entry = if parent_folded {
3600 None
3601 } else {
3602 Some((
3603 depth,
3604 FoldedDirsEntry {
3605 worktree_id: directory_entry.worktree_id,
3606 entries: vec![directory_entry.entry.clone()],
3607 },
3608 ))
3609 };
3610 }
3611 } else if folded {
3612 folded_dirs_entry = Some((
3613 depth,
3614 FoldedDirsEntry {
3615 worktree_id: directory_entry.worktree_id,
3616 entries: vec![directory_entry.entry.clone()],
3617 },
3618 ));
3619 }
3620
3621 let should_add =
3622 should_add && parent_expanded && folded_dirs_entry.is_none();
3623 (depth, should_add)
3624 }
3625 FsEntry::ExternalFile(..) => {
3626 if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() {
3627 let parent_expanded = parent_dirs
3628 .iter()
3629 .rev()
3630 .find(|parent| {
3631 folded_dir
3632 .entries
3633 .iter()
3634 .all(|entry| entry.path != parent.path)
3635 })
3636 .map_or(true, |parent| parent.expanded);
3637 if !is_singleton && (parent_expanded || query.is_some()) {
3638 outline_panel.push_entry(
3639 &mut generation_state,
3640 track_matches,
3641 PanelEntry::FoldedDirs(folded_dir),
3642 folded_depth,
3643 cx,
3644 );
3645 }
3646 }
3647 parent_dirs.clear();
3648 (0, true)
3649 }
3650 FsEntry::File(file) => {
3651 if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3652 let parent_expanded = parent_dirs
3653 .iter()
3654 .rev()
3655 .find(|parent| {
3656 folded_dirs
3657 .entries
3658 .iter()
3659 .all(|entry| entry.path != parent.path)
3660 })
3661 .map_or(true, |parent| parent.expanded);
3662 if !is_singleton && (parent_expanded || query.is_some()) {
3663 outline_panel.push_entry(
3664 &mut generation_state,
3665 track_matches,
3666 PanelEntry::FoldedDirs(folded_dirs),
3667 folded_depth,
3668 cx,
3669 );
3670 }
3671 }
3672
3673 let fs_depth = outline_panel
3674 .fs_entries_depth
3675 .get(&(file.worktree_id, file.entry.id))
3676 .copied()
3677 .unwrap_or(0);
3678 while let Some(parent) = parent_dirs.last() {
3679 if file.entry.path.starts_with(&parent.path) {
3680 break;
3681 }
3682 parent_dirs.pop();
3683 }
3684 match parent_dirs.last() {
3685 Some(parent) => {
3686 let new_depth = parent.depth + 1;
3687 (new_depth, parent.expanded)
3688 }
3689 None => (fs_depth, true),
3690 }
3691 }
3692 };
3693
3694 if !is_singleton
3695 && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3696 {
3697 outline_panel.push_entry(
3698 &mut generation_state,
3699 track_matches,
3700 PanelEntry::Fs(entry.clone()),
3701 depth,
3702 cx,
3703 );
3704 }
3705
3706 match outline_panel.mode {
3707 ItemsDisplayMode::Search(_) => {
3708 if is_singleton || query.is_some() || (should_add && is_expanded) {
3709 outline_panel.add_search_entries(
3710 &mut generation_state,
3711 &active_editor,
3712 entry.clone(),
3713 depth,
3714 query.clone(),
3715 is_singleton,
3716 cx,
3717 );
3718 }
3719 }
3720 ItemsDisplayMode::Outline => {
3721 let excerpts_to_consider =
3722 if is_singleton || query.is_some() || (should_add && is_expanded) {
3723 match &entry {
3724 FsEntry::File(FsEntryFile {
3725 buffer_id,
3726 excerpts,
3727 ..
3728 })
3729 | FsEntry::ExternalFile(FsEntryExternalFile {
3730 buffer_id,
3731 excerpts,
3732 ..
3733 }) => Some((*buffer_id, excerpts)),
3734 _ => None,
3735 }
3736 } else {
3737 None
3738 };
3739 if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
3740 if !active_editor.read(cx).is_buffer_folded(buffer_id, cx) {
3741 outline_panel.add_excerpt_entries(
3742 &mut generation_state,
3743 buffer_id,
3744 entry_excerpts,
3745 depth,
3746 track_matches,
3747 is_singleton,
3748 query.as_deref(),
3749 cx,
3750 );
3751 }
3752 }
3753 }
3754 }
3755
3756 if is_singleton
3757 && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
3758 && !generation_state.entries.iter().any(|item| {
3759 matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
3760 })
3761 {
3762 outline_panel.push_entry(
3763 &mut generation_state,
3764 track_matches,
3765 PanelEntry::Fs(entry.clone()),
3766 0,
3767 cx,
3768 );
3769 }
3770 }
3771
3772 if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3773 let parent_expanded = parent_dirs
3774 .iter()
3775 .rev()
3776 .find(|parent| {
3777 folded_dirs
3778 .entries
3779 .iter()
3780 .all(|entry| entry.path != parent.path)
3781 })
3782 .map_or(true, |parent| parent.expanded);
3783 if parent_expanded || query.is_some() {
3784 outline_panel.push_entry(
3785 &mut generation_state,
3786 track_matches,
3787 PanelEntry::FoldedDirs(folded_dirs),
3788 folded_depth,
3789 cx,
3790 );
3791 }
3792 }
3793 }) else {
3794 return (Vec::new(), None);
3795 };
3796
3797 let Some(query) = query else {
3798 return (
3799 generation_state.entries,
3800 generation_state
3801 .max_width_estimate_and_index
3802 .map(|(_, index)| index),
3803 );
3804 };
3805
3806 let mut matched_ids = match_strings(
3807 &generation_state.match_candidates,
3808 &query,
3809 true,
3810 usize::MAX,
3811 &AtomicBool::default(),
3812 cx.background_executor().clone(),
3813 )
3814 .await
3815 .into_iter()
3816 .map(|string_match| (string_match.candidate_id, string_match))
3817 .collect::<HashMap<_, _>>();
3818
3819 let mut id = 0;
3820 generation_state.entries.retain_mut(|cached_entry| {
3821 let retain = match matched_ids.remove(&id) {
3822 Some(string_match) => {
3823 cached_entry.string_match = Some(string_match);
3824 true
3825 }
3826 None => false,
3827 };
3828 id += 1;
3829 retain
3830 });
3831
3832 (
3833 generation_state.entries,
3834 generation_state
3835 .max_width_estimate_and_index
3836 .map(|(_, index)| index),
3837 )
3838 })
3839 }
3840
3841 fn push_entry(
3842 &self,
3843 state: &mut GenerationState,
3844 track_matches: bool,
3845 entry: PanelEntry,
3846 depth: usize,
3847 cx: &mut App,
3848 ) {
3849 let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry {
3850 match folded_dirs_entry.entries.len() {
3851 0 => {
3852 debug_panic!("Empty folded dirs receiver");
3853 return;
3854 }
3855 1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3856 worktree_id: folded_dirs_entry.worktree_id,
3857 entry: folded_dirs_entry.entries[0].clone(),
3858 })),
3859 _ => entry,
3860 }
3861 } else {
3862 entry
3863 };
3864
3865 if track_matches {
3866 let id = state.entries.len();
3867 match &entry {
3868 PanelEntry::Fs(fs_entry) => {
3869 if let Some(file_name) =
3870 self.relative_path(fs_entry, cx).as_deref().map(file_name)
3871 {
3872 state
3873 .match_candidates
3874 .push(StringMatchCandidate::new(id, &file_name));
3875 }
3876 }
3877 PanelEntry::FoldedDirs(folded_dir_entry) => {
3878 let dir_names = self.dir_names_string(
3879 &folded_dir_entry.entries,
3880 folded_dir_entry.worktree_id,
3881 cx,
3882 );
3883 {
3884 state
3885 .match_candidates
3886 .push(StringMatchCandidate::new(id, &dir_names));
3887 }
3888 }
3889 PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state
3890 .match_candidates
3891 .push(StringMatchCandidate::new(id, &outline_entry.outline.text)),
3892 PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {}
3893 PanelEntry::Search(new_search_entry) => {
3894 if let Some(search_data) = new_search_entry.render_data.get() {
3895 state
3896 .match_candidates
3897 .push(StringMatchCandidate::new(id, &search_data.context_text));
3898 }
3899 }
3900 }
3901 }
3902
3903 let width_estimate = self.width_estimate(depth, &entry, cx);
3904 if Some(width_estimate)
3905 > state
3906 .max_width_estimate_and_index
3907 .map(|(estimate, _)| estimate)
3908 {
3909 state.max_width_estimate_and_index = Some((width_estimate, state.entries.len()));
3910 }
3911 state.entries.push(CachedEntry {
3912 depth,
3913 entry,
3914 string_match: None,
3915 });
3916 }
3917
3918 fn dir_names_string(&self, entries: &[GitEntry], worktree_id: WorktreeId, cx: &App) -> String {
3919 let dir_names_segment = entries
3920 .iter()
3921 .map(|entry| self.entry_name(&worktree_id, entry, cx))
3922 .collect::<PathBuf>();
3923 dir_names_segment.to_string_lossy().to_string()
3924 }
3925
3926 fn query(&self, cx: &App) -> Option<String> {
3927 let query = self.filter_editor.read(cx).text(cx);
3928 if query.trim().is_empty() {
3929 None
3930 } else {
3931 Some(query)
3932 }
3933 }
3934
3935 fn is_expanded(&self, entry: &FsEntry) -> bool {
3936 let entry_to_check = match entry {
3937 FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3938 CollapsedEntry::ExternalFile(*buffer_id)
3939 }
3940 FsEntry::File(FsEntryFile {
3941 worktree_id,
3942 buffer_id,
3943 ..
3944 }) => CollapsedEntry::File(*worktree_id, *buffer_id),
3945 FsEntry::Directory(FsEntryDirectory {
3946 worktree_id, entry, ..
3947 }) => CollapsedEntry::Dir(*worktree_id, entry.id),
3948 };
3949 !self.collapsed_entries.contains(&entry_to_check)
3950 }
3951
3952 fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) -> bool {
3953 if !self.active {
3954 return false;
3955 }
3956
3957 let mut update_cached_items = false;
3958 update_cached_items |= self.update_search_matches(window, cx);
3959 self.fetch_outdated_outlines(window, cx);
3960 if update_cached_items {
3961 self.selected_entry.invalidate();
3962 }
3963 update_cached_items
3964 }
3965
3966 fn update_search_matches(
3967 &mut self,
3968 window: &mut Window,
3969 cx: &mut Context<OutlinePanel>,
3970 ) -> bool {
3971 if !self.active {
3972 return false;
3973 }
3974
3975 let project_search = self
3976 .active_item()
3977 .and_then(|item| item.downcast::<ProjectSearchView>());
3978 let project_search_matches = project_search
3979 .as_ref()
3980 .map(|project_search| project_search.read(cx).get_matches(cx))
3981 .unwrap_or_default();
3982
3983 let buffer_search = self
3984 .active_item()
3985 .as_deref()
3986 .and_then(|active_item| {
3987 self.workspace
3988 .upgrade()
3989 .and_then(|workspace| workspace.read(cx).pane_for(active_item))
3990 })
3991 .and_then(|pane| {
3992 pane.read(cx)
3993 .toolbar()
3994 .read(cx)
3995 .item_of_type::<BufferSearchBar>()
3996 });
3997 let buffer_search_matches = self
3998 .active_editor()
3999 .map(|active_editor| {
4000 active_editor.update(cx, |editor, cx| editor.get_matches(window, cx))
4001 })
4002 .unwrap_or_default();
4003
4004 let mut update_cached_entries = false;
4005 if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
4006 if matches!(self.mode, ItemsDisplayMode::Search(_)) {
4007 self.mode = ItemsDisplayMode::Outline;
4008 update_cached_entries = true;
4009 }
4010 } else {
4011 let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
4012 (
4013 SearchKind::Project,
4014 project_search_matches,
4015 project_search
4016 .map(|project_search| project_search.read(cx).search_query_text(cx))
4017 .unwrap_or_default(),
4018 )
4019 } else {
4020 (
4021 SearchKind::Buffer,
4022 buffer_search_matches,
4023 buffer_search
4024 .map(|buffer_search| buffer_search.read(cx).query(cx))
4025 .unwrap_or_default(),
4026 )
4027 };
4028
4029 let mut previous_matches = HashMap::default();
4030 update_cached_entries = match &mut self.mode {
4031 ItemsDisplayMode::Search(current_search_state) => {
4032 let update = current_search_state.query != new_search_query
4033 || current_search_state.kind != kind
4034 || current_search_state.matches.is_empty()
4035 || current_search_state.matches.iter().enumerate().any(
4036 |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
4037 );
4038 if current_search_state.kind == kind {
4039 previous_matches.extend(current_search_state.matches.drain(..));
4040 }
4041 update
4042 }
4043 ItemsDisplayMode::Outline => true,
4044 };
4045 self.mode = ItemsDisplayMode::Search(SearchState::new(
4046 kind,
4047 new_search_query,
4048 previous_matches,
4049 new_search_matches,
4050 cx.theme().syntax().clone(),
4051 window,
4052 cx,
4053 ));
4054 }
4055 update_cached_entries
4056 }
4057
4058 fn add_excerpt_entries(
4059 &self,
4060 state: &mut GenerationState,
4061 buffer_id: BufferId,
4062 entries_to_add: &[ExcerptId],
4063 parent_depth: usize,
4064 track_matches: bool,
4065 is_singleton: bool,
4066 query: Option<&str>,
4067 cx: &mut Context<Self>,
4068 ) {
4069 if let Some(excerpts) = self.excerpts.get(&buffer_id) {
4070 for &excerpt_id in entries_to_add {
4071 let Some(excerpt) = excerpts.get(&excerpt_id) else {
4072 continue;
4073 };
4074 let excerpt_depth = parent_depth + 1;
4075 self.push_entry(
4076 state,
4077 track_matches,
4078 PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
4079 buffer_id,
4080 id: excerpt_id,
4081 range: excerpt.range.clone(),
4082 })),
4083 excerpt_depth,
4084 cx,
4085 );
4086
4087 let mut outline_base_depth = excerpt_depth + 1;
4088 if is_singleton {
4089 outline_base_depth = 0;
4090 state.clear();
4091 } else if query.is_none()
4092 && self
4093 .collapsed_entries
4094 .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
4095 {
4096 continue;
4097 }
4098
4099 for outline in excerpt.iter_outlines() {
4100 self.push_entry(
4101 state,
4102 track_matches,
4103 PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
4104 buffer_id,
4105 excerpt_id,
4106 outline: outline.clone(),
4107 })),
4108 outline_base_depth + outline.depth,
4109 cx,
4110 );
4111 }
4112 }
4113 }
4114 }
4115
4116 fn add_search_entries(
4117 &mut self,
4118 state: &mut GenerationState,
4119 active_editor: &Entity<Editor>,
4120 parent_entry: FsEntry,
4121 parent_depth: usize,
4122 filter_query: Option<String>,
4123 is_singleton: bool,
4124 cx: &mut Context<Self>,
4125 ) {
4126 let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
4127 return;
4128 };
4129
4130 let kind = search_state.kind;
4131 let related_excerpts = match &parent_entry {
4132 FsEntry::Directory(_) => return,
4133 FsEntry::ExternalFile(external) => &external.excerpts,
4134 FsEntry::File(file) => &file.excerpts,
4135 }
4136 .iter()
4137 .copied()
4138 .collect::<HashSet<_>>();
4139
4140 let depth = if is_singleton { 0 } else { parent_depth + 1 };
4141 let new_search_matches = search_state
4142 .matches
4143 .iter()
4144 .filter(|(match_range, _)| {
4145 related_excerpts.contains(&match_range.start.excerpt_id)
4146 || related_excerpts.contains(&match_range.end.excerpt_id)
4147 })
4148 .filter(|(match_range, _)| {
4149 let editor = active_editor.read(cx);
4150 if let Some(buffer_id) = match_range.start.buffer_id {
4151 if editor.is_buffer_folded(buffer_id, cx) {
4152 return false;
4153 }
4154 }
4155 if let Some(buffer_id) = match_range.start.buffer_id {
4156 if editor.is_buffer_folded(buffer_id, cx) {
4157 return false;
4158 }
4159 }
4160 true
4161 });
4162
4163 let new_search_entries = new_search_matches
4164 .map(|(match_range, search_data)| SearchEntry {
4165 match_range: match_range.clone(),
4166 kind,
4167 render_data: Arc::clone(search_data),
4168 })
4169 .collect::<Vec<_>>();
4170 for new_search_entry in new_search_entries {
4171 self.push_entry(
4172 state,
4173 filter_query.is_some(),
4174 PanelEntry::Search(new_search_entry),
4175 depth,
4176 cx,
4177 );
4178 }
4179 }
4180
4181 fn active_editor(&self) -> Option<Entity<Editor>> {
4182 self.active_item.as_ref()?.active_editor.upgrade()
4183 }
4184
4185 fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
4186 self.active_item.as_ref()?.item_handle.upgrade()
4187 }
4188
4189 fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
4190 self.active_item().map_or(true, |active_item| {
4191 !self.pinned && active_item.item_id() != new_active_item.item_id()
4192 })
4193 }
4194
4195 pub fn toggle_active_editor_pin(
4196 &mut self,
4197 _: &ToggleActiveEditorPin,
4198 window: &mut Window,
4199 cx: &mut Context<Self>,
4200 ) {
4201 self.pinned = !self.pinned;
4202 if !self.pinned {
4203 if let Some((active_item, active_editor)) = self
4204 .workspace
4205 .upgrade()
4206 .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
4207 {
4208 if self.should_replace_active_item(active_item.as_ref()) {
4209 self.replace_active_editor(active_item, active_editor, window, cx);
4210 }
4211 }
4212 }
4213
4214 cx.notify();
4215 }
4216
4217 fn selected_entry(&self) -> Option<&PanelEntry> {
4218 match &self.selected_entry {
4219 SelectedEntry::Invalidated(entry) => entry.as_ref(),
4220 SelectedEntry::Valid(entry, _) => Some(entry),
4221 SelectedEntry::None => None,
4222 }
4223 }
4224
4225 fn select_entry(
4226 &mut self,
4227 entry: PanelEntry,
4228 focus: bool,
4229 window: &mut Window,
4230 cx: &mut Context<Self>,
4231 ) {
4232 if focus {
4233 self.focus_handle.focus(window);
4234 }
4235 let ix = self
4236 .cached_entries
4237 .iter()
4238 .enumerate()
4239 .find(|(_, cached_entry)| &cached_entry.entry == &entry)
4240 .map(|(i, _)| i)
4241 .unwrap_or_default();
4242
4243 self.selected_entry = SelectedEntry::Valid(entry, ix);
4244
4245 self.autoscroll(cx);
4246 cx.notify();
4247 }
4248
4249 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4250 if !Self::should_show_scrollbar(cx)
4251 || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4252 {
4253 return None;
4254 }
4255 Some(
4256 div()
4257 .occlude()
4258 .id("project-panel-vertical-scroll")
4259 .on_mouse_move(cx.listener(|_, _, _, cx| {
4260 cx.notify();
4261 cx.stop_propagation()
4262 }))
4263 .on_hover(|_, _, cx| {
4264 cx.stop_propagation();
4265 })
4266 .on_any_mouse_down(|_, _, cx| {
4267 cx.stop_propagation();
4268 })
4269 .on_mouse_up(
4270 MouseButton::Left,
4271 cx.listener(|outline_panel, _, window, cx| {
4272 if !outline_panel.vertical_scrollbar_state.is_dragging()
4273 && !outline_panel.focus_handle.contains_focused(window, cx)
4274 {
4275 outline_panel.hide_scrollbar(window, cx);
4276 cx.notify();
4277 }
4278
4279 cx.stop_propagation();
4280 }),
4281 )
4282 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4283 cx.notify();
4284 }))
4285 .h_full()
4286 .absolute()
4287 .right_1()
4288 .top_1()
4289 .bottom_0()
4290 .w(px(12.))
4291 .cursor_default()
4292 .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone())),
4293 )
4294 }
4295
4296 fn render_horizontal_scrollbar(
4297 &self,
4298 _: &mut Window,
4299 cx: &mut Context<Self>,
4300 ) -> Option<Stateful<Div>> {
4301 if !Self::should_show_scrollbar(cx)
4302 || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4303 {
4304 return None;
4305 }
4306
4307 let scroll_handle = self.scroll_handle.0.borrow();
4308 let longest_item_width = scroll_handle
4309 .last_item_size
4310 .filter(|size| size.contents.width > size.item.width)?
4311 .contents
4312 .width
4313 .0 as f64;
4314 if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
4315 return None;
4316 }
4317
4318 Some(
4319 div()
4320 .occlude()
4321 .id("project-panel-horizontal-scroll")
4322 .on_mouse_move(cx.listener(|_, _, _, cx| {
4323 cx.notify();
4324 cx.stop_propagation()
4325 }))
4326 .on_hover(|_, _, cx| {
4327 cx.stop_propagation();
4328 })
4329 .on_any_mouse_down(|_, _, cx| {
4330 cx.stop_propagation();
4331 })
4332 .on_mouse_up(
4333 MouseButton::Left,
4334 cx.listener(|outline_panel, _, window, cx| {
4335 if !outline_panel.horizontal_scrollbar_state.is_dragging()
4336 && !outline_panel.focus_handle.contains_focused(window, cx)
4337 {
4338 outline_panel.hide_scrollbar(window, cx);
4339 cx.notify();
4340 }
4341
4342 cx.stop_propagation();
4343 }),
4344 )
4345 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4346 cx.notify();
4347 }))
4348 .w_full()
4349 .absolute()
4350 .right_1()
4351 .left_1()
4352 .bottom_0()
4353 .h(px(12.))
4354 .cursor_default()
4355 .when(self.width.is_some(), |this| {
4356 this.children(Scrollbar::horizontal(
4357 self.horizontal_scrollbar_state.clone(),
4358 ))
4359 }),
4360 )
4361 }
4362
4363 fn should_show_scrollbar(cx: &App) -> bool {
4364 let show = OutlinePanelSettings::get_global(cx)
4365 .scrollbar
4366 .show
4367 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4368 match show {
4369 ShowScrollbar::Auto => true,
4370 ShowScrollbar::System => true,
4371 ShowScrollbar::Always => true,
4372 ShowScrollbar::Never => false,
4373 }
4374 }
4375
4376 fn should_autohide_scrollbar(cx: &App) -> bool {
4377 let show = OutlinePanelSettings::get_global(cx)
4378 .scrollbar
4379 .show
4380 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4381 match show {
4382 ShowScrollbar::Auto => true,
4383 ShowScrollbar::System => cx
4384 .try_global::<ScrollbarAutoHide>()
4385 .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4386 ShowScrollbar::Always => false,
4387 ShowScrollbar::Never => true,
4388 }
4389 }
4390
4391 fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4392 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4393 if !Self::should_autohide_scrollbar(cx) {
4394 return;
4395 }
4396 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4397 cx.background_executor()
4398 .timer(SCROLLBAR_SHOW_INTERVAL)
4399 .await;
4400 panel
4401 .update(cx, |panel, cx| {
4402 panel.show_scrollbar = false;
4403 cx.notify();
4404 })
4405 .log_err();
4406 }))
4407 }
4408
4409 fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
4410 let item_text_chars = match entry {
4411 PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
4412 .buffer_snapshot_for_id(external.buffer_id, cx)
4413 .and_then(|snapshot| {
4414 Some(snapshot.file()?.path().file_name()?.to_string_lossy().len())
4415 })
4416 .unwrap_or_default(),
4417 PanelEntry::Fs(FsEntry::Directory(directory)) => directory
4418 .entry
4419 .path
4420 .file_name()
4421 .map(|name| name.to_string_lossy().len())
4422 .unwrap_or_default(),
4423 PanelEntry::Fs(FsEntry::File(file)) => file
4424 .entry
4425 .path
4426 .file_name()
4427 .map(|name| name.to_string_lossy().len())
4428 .unwrap_or_default(),
4429 PanelEntry::FoldedDirs(folded_dirs) => {
4430 folded_dirs
4431 .entries
4432 .iter()
4433 .map(|dir| {
4434 dir.path
4435 .file_name()
4436 .map(|name| name.to_string_lossy().len())
4437 .unwrap_or_default()
4438 })
4439 .sum::<usize>()
4440 + folded_dirs.entries.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len()
4441 }
4442 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
4443 .excerpt_label(excerpt.buffer_id, &excerpt.range, cx)
4444 .map(|label| label.len())
4445 .unwrap_or_default(),
4446 PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(),
4447 PanelEntry::Search(search) => search
4448 .render_data
4449 .get()
4450 .map(|data| data.context_text.len())
4451 .unwrap_or_default(),
4452 };
4453
4454 (item_text_chars + depth) as u64
4455 }
4456
4457 fn render_main_contents(
4458 &mut self,
4459 query: Option<String>,
4460 show_indent_guides: bool,
4461 indent_size: f32,
4462 window: &mut Window,
4463 cx: &mut Context<Self>,
4464 ) -> Div {
4465 let contents = if self.cached_entries.is_empty() {
4466 let header = if self.updating_fs_entries || self.updating_cached_entries {
4467 None
4468 } else if query.is_some() {
4469 Some("No matches for query")
4470 } else {
4471 Some("No outlines available")
4472 };
4473
4474 v_flex()
4475 .flex_1()
4476 .justify_center()
4477 .size_full()
4478 .when_some(header, |panel, header| {
4479 panel
4480 .child(h_flex().justify_center().child(Label::new(header)))
4481 .when_some(query.clone(), |panel, query| {
4482 panel.child(h_flex().justify_center().child(Label::new(query)))
4483 })
4484 .child(
4485 h_flex()
4486 .pt(DynamicSpacing::Base04.rems(cx))
4487 .justify_center()
4488 .child({
4489 let keystroke =
4490 match self.position(window, cx) {
4491 DockPosition::Left => window
4492 .keystroke_text_for(&workspace::ToggleLeftDock),
4493 DockPosition::Bottom => window
4494 .keystroke_text_for(&workspace::ToggleBottomDock),
4495 DockPosition::Right => window
4496 .keystroke_text_for(&workspace::ToggleRightDock),
4497 };
4498 Label::new(format!("Toggle this panel with {keystroke}"))
4499 }),
4500 )
4501 })
4502 } else {
4503 let list_contents = {
4504 let items_len = self.cached_entries.len();
4505 let multi_buffer_snapshot = self
4506 .active_editor()
4507 .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
4508 uniform_list(cx.entity().clone(), "entries", items_len, {
4509 move |outline_panel, range, window, cx| {
4510 let entries = outline_panel.cached_entries.get(range);
4511 entries
4512 .map(|entries| entries.to_vec())
4513 .unwrap_or_default()
4514 .into_iter()
4515 .filter_map(|cached_entry| match cached_entry.entry {
4516 PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4517 &entry,
4518 cached_entry.depth,
4519 cached_entry.string_match.as_ref(),
4520 window,
4521 cx,
4522 )),
4523 PanelEntry::FoldedDirs(folded_dirs_entry) => {
4524 Some(outline_panel.render_folded_dirs(
4525 &folded_dirs_entry,
4526 cached_entry.depth,
4527 cached_entry.string_match.as_ref(),
4528 window,
4529 cx,
4530 ))
4531 }
4532 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4533 outline_panel.render_excerpt(
4534 &excerpt,
4535 cached_entry.depth,
4536 window,
4537 cx,
4538 )
4539 }
4540 PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4541 Some(outline_panel.render_outline(
4542 &entry,
4543 cached_entry.depth,
4544 cached_entry.string_match.as_ref(),
4545 window,
4546 cx,
4547 ))
4548 }
4549 PanelEntry::Search(SearchEntry {
4550 match_range,
4551 render_data,
4552 kind,
4553 ..
4554 }) => outline_panel.render_search_match(
4555 multi_buffer_snapshot.as_ref(),
4556 &match_range,
4557 &render_data,
4558 kind,
4559 cached_entry.depth,
4560 cached_entry.string_match.as_ref(),
4561 window,
4562 cx,
4563 ),
4564 })
4565 .collect()
4566 }
4567 })
4568 .with_sizing_behavior(ListSizingBehavior::Infer)
4569 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4570 .with_width_from_item(self.max_width_item_index)
4571 .track_scroll(self.scroll_handle.clone())
4572 .when(show_indent_guides, |list| {
4573 list.with_decoration(
4574 ui::indent_guides(
4575 cx.entity().clone(),
4576 px(indent_size),
4577 IndentGuideColors::panel(cx),
4578 |outline_panel, range, _, _| {
4579 let entries = outline_panel.cached_entries.get(range);
4580 if let Some(entries) = entries {
4581 entries.into_iter().map(|item| item.depth).collect()
4582 } else {
4583 smallvec::SmallVec::new()
4584 }
4585 },
4586 )
4587 .with_render_fn(
4588 cx.entity().clone(),
4589 move |outline_panel, params, _, _| {
4590 const LEFT_OFFSET: Pixels = px(14.);
4591
4592 let indent_size = params.indent_size;
4593 let item_height = params.item_height;
4594 let active_indent_guide_ix = find_active_indent_guide_ix(
4595 outline_panel,
4596 ¶ms.indent_guides,
4597 );
4598
4599 params
4600 .indent_guides
4601 .into_iter()
4602 .enumerate()
4603 .map(|(ix, layout)| {
4604 let bounds = Bounds::new(
4605 point(
4606 layout.offset.x * indent_size + LEFT_OFFSET,
4607 layout.offset.y * item_height,
4608 ),
4609 size(px(1.), layout.length * item_height),
4610 );
4611 ui::RenderedIndentGuide {
4612 bounds,
4613 layout,
4614 is_active: active_indent_guide_ix == Some(ix),
4615 hitbox: None,
4616 }
4617 })
4618 .collect()
4619 },
4620 ),
4621 )
4622 })
4623 };
4624
4625 v_flex()
4626 .flex_shrink()
4627 .size_full()
4628 .child(list_contents.size_full().flex_shrink())
4629 .children(self.render_vertical_scrollbar(cx))
4630 .when_some(
4631 self.render_horizontal_scrollbar(window, cx),
4632 |this, scrollbar| this.pb_4().child(scrollbar),
4633 )
4634 }
4635 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4636 deferred(
4637 anchored()
4638 .position(*position)
4639 .anchor(gpui::Corner::TopLeft)
4640 .child(menu.clone()),
4641 )
4642 .with_priority(1)
4643 }));
4644
4645 v_flex().w_full().flex_1().overflow_hidden().child(contents)
4646 }
4647
4648 fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4649 v_flex().flex_none().child(horizontal_separator(cx)).child(
4650 h_flex()
4651 .p_2()
4652 .w_full()
4653 .child(self.filter_editor.clone())
4654 .child(
4655 div().child(
4656 IconButton::new(
4657 "outline-panel-menu",
4658 if pinned {
4659 IconName::Unpin
4660 } else {
4661 IconName::Pin
4662 },
4663 )
4664 .tooltip(Tooltip::text(if pinned {
4665 "Unpin Outline"
4666 } else {
4667 "Pin Active Outline"
4668 }))
4669 .shape(IconButtonShape::Square)
4670 .on_click(cx.listener(
4671 |outline_panel, _, window, cx| {
4672 outline_panel.toggle_active_editor_pin(
4673 &ToggleActiveEditorPin,
4674 window,
4675 cx,
4676 );
4677 },
4678 )),
4679 ),
4680 ),
4681 )
4682 }
4683
4684 fn buffers_inside_directory(
4685 &self,
4686 dir_worktree: WorktreeId,
4687 dir_entry: &GitEntry,
4688 ) -> HashSet<BufferId> {
4689 if !dir_entry.is_dir() {
4690 debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4691 return HashSet::default();
4692 }
4693
4694 self.fs_entries
4695 .iter()
4696 .skip_while(|fs_entry| match fs_entry {
4697 FsEntry::Directory(directory) => {
4698 directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4699 }
4700 _ => true,
4701 })
4702 .skip(1)
4703 .take_while(|fs_entry| match fs_entry {
4704 FsEntry::ExternalFile(..) => false,
4705 FsEntry::Directory(directory) => {
4706 directory.worktree_id == dir_worktree
4707 && directory.entry.path.starts_with(&dir_entry.path)
4708 }
4709 FsEntry::File(file) => {
4710 file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4711 }
4712 })
4713 .filter_map(|fs_entry| match fs_entry {
4714 FsEntry::File(file) => Some(file.buffer_id),
4715 _ => None,
4716 })
4717 .collect()
4718 }
4719}
4720
4721fn workspace_active_editor(
4722 workspace: &Workspace,
4723 cx: &App,
4724) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4725 let active_item = workspace.active_item(cx)?;
4726 let active_editor = active_item
4727 .act_as::<Editor>(cx)
4728 .filter(|editor| editor.read(cx).mode().is_full())?;
4729 Some((active_item, active_editor))
4730}
4731
4732fn back_to_common_visited_parent(
4733 visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
4734 worktree_id: &WorktreeId,
4735 new_entry: &Entry,
4736) -> Option<(WorktreeId, ProjectEntryId)> {
4737 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4738 match new_entry.path.parent() {
4739 Some(parent_path) => {
4740 if parent_path == visited_path.as_ref() {
4741 return Some((*worktree_id, *visited_dir_id));
4742 }
4743 }
4744 None => {
4745 break;
4746 }
4747 }
4748 visited_dirs.pop();
4749 }
4750 None
4751}
4752
4753fn file_name(path: &Path) -> String {
4754 let mut current_path = path;
4755 loop {
4756 if let Some(file_name) = current_path.file_name() {
4757 return file_name.to_string_lossy().into_owned();
4758 }
4759 match current_path.parent() {
4760 Some(parent) => current_path = parent,
4761 None => return path.to_string_lossy().into_owned(),
4762 }
4763 }
4764}
4765
4766impl Panel for OutlinePanel {
4767 fn persistent_name() -> &'static str {
4768 "Outline Panel"
4769 }
4770
4771 fn position(&self, _: &Window, cx: &App) -> DockPosition {
4772 match OutlinePanelSettings::get_global(cx).dock {
4773 OutlinePanelDockPosition::Left => DockPosition::Left,
4774 OutlinePanelDockPosition::Right => DockPosition::Right,
4775 }
4776 }
4777
4778 fn position_is_valid(&self, position: DockPosition) -> bool {
4779 matches!(position, DockPosition::Left | DockPosition::Right)
4780 }
4781
4782 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4783 settings::update_settings_file::<OutlinePanelSettings>(
4784 self.fs.clone(),
4785 cx,
4786 move |settings, _| {
4787 let dock = match position {
4788 DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
4789 DockPosition::Right => OutlinePanelDockPosition::Right,
4790 };
4791 settings.dock = Some(dock);
4792 },
4793 );
4794 }
4795
4796 fn size(&self, _: &Window, cx: &App) -> Pixels {
4797 self.width
4798 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
4799 }
4800
4801 fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
4802 self.width = size;
4803 self.serialize(cx);
4804 cx.notify();
4805 }
4806
4807 fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4808 OutlinePanelSettings::get_global(cx)
4809 .button
4810 .then_some(IconName::ListTree)
4811 }
4812
4813 fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
4814 Some("Outline Panel")
4815 }
4816
4817 fn toggle_action(&self) -> Box<dyn Action> {
4818 Box::new(ToggleFocus)
4819 }
4820
4821 fn starts_open(&self, _window: &Window, _: &App) -> bool {
4822 self.active
4823 }
4824
4825 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
4826 cx.spawn_in(window, async move |outline_panel, cx| {
4827 outline_panel
4828 .update_in(cx, |outline_panel, window, cx| {
4829 let old_active = outline_panel.active;
4830 outline_panel.active = active;
4831 if old_active != active {
4832 if active {
4833 if let Some((active_item, active_editor)) =
4834 outline_panel.workspace.upgrade().and_then(|workspace| {
4835 workspace_active_editor(workspace.read(cx), cx)
4836 })
4837 {
4838 if outline_panel.should_replace_active_item(active_item.as_ref()) {
4839 outline_panel.replace_active_editor(
4840 active_item,
4841 active_editor,
4842 window,
4843 cx,
4844 );
4845 } else {
4846 outline_panel.update_fs_entries(active_editor, None, window, cx)
4847 }
4848 return;
4849 }
4850 }
4851
4852 if !outline_panel.pinned {
4853 outline_panel.clear_previous(window, cx);
4854 }
4855 }
4856 outline_panel.serialize(cx);
4857 })
4858 .ok();
4859 })
4860 .detach()
4861 }
4862
4863 fn activation_priority(&self) -> u32 {
4864 5
4865 }
4866}
4867
4868impl Focusable for OutlinePanel {
4869 fn focus_handle(&self, cx: &App) -> FocusHandle {
4870 self.filter_editor.focus_handle(cx).clone()
4871 }
4872}
4873
4874impl EventEmitter<Event> for OutlinePanel {}
4875
4876impl EventEmitter<PanelEvent> for OutlinePanel {}
4877
4878impl Render for OutlinePanel {
4879 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4880 let (is_local, is_via_ssh) = self
4881 .project
4882 .read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
4883 let query = self.query(cx);
4884 let pinned = self.pinned;
4885 let settings = OutlinePanelSettings::get_global(cx);
4886 let indent_size = settings.indent_size;
4887 let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
4888
4889 let search_query = match &self.mode {
4890 ItemsDisplayMode::Search(search_query) => Some(search_query),
4891 _ => None,
4892 };
4893
4894 v_flex()
4895 .id("outline-panel")
4896 .size_full()
4897 .overflow_hidden()
4898 .relative()
4899 .on_hover(cx.listener(|this, hovered, window, cx| {
4900 if *hovered {
4901 this.show_scrollbar = true;
4902 this.hide_scrollbar_task.take();
4903 cx.notify();
4904 } else if !this.focus_handle.contains_focused(window, cx) {
4905 this.hide_scrollbar(window, cx);
4906 }
4907 }))
4908 .key_context(self.dispatch_context(window, cx))
4909 .on_action(cx.listener(Self::open))
4910 .on_action(cx.listener(Self::cancel))
4911 .on_action(cx.listener(Self::select_next))
4912 .on_action(cx.listener(Self::select_previous))
4913 .on_action(cx.listener(Self::select_first))
4914 .on_action(cx.listener(Self::select_last))
4915 .on_action(cx.listener(Self::select_parent))
4916 .on_action(cx.listener(Self::expand_selected_entry))
4917 .on_action(cx.listener(Self::collapse_selected_entry))
4918 .on_action(cx.listener(Self::expand_all_entries))
4919 .on_action(cx.listener(Self::collapse_all_entries))
4920 .on_action(cx.listener(Self::copy_path))
4921 .on_action(cx.listener(Self::copy_relative_path))
4922 .on_action(cx.listener(Self::toggle_active_editor_pin))
4923 .on_action(cx.listener(Self::unfold_directory))
4924 .on_action(cx.listener(Self::fold_directory))
4925 .on_action(cx.listener(Self::open_excerpts))
4926 .on_action(cx.listener(Self::open_excerpts_split))
4927 .when(is_local, |el| {
4928 el.on_action(cx.listener(Self::reveal_in_finder))
4929 })
4930 .when(is_local || is_via_ssh, |el| {
4931 el.on_action(cx.listener(Self::open_in_terminal))
4932 })
4933 .on_mouse_down(
4934 MouseButton::Right,
4935 cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
4936 if let Some(entry) = outline_panel.selected_entry().cloned() {
4937 outline_panel.deploy_context_menu(event.position, entry, window, cx)
4938 } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
4939 outline_panel.deploy_context_menu(
4940 event.position,
4941 PanelEntry::Fs(entry),
4942 window,
4943 cx,
4944 )
4945 }
4946 }),
4947 )
4948 .track_focus(&self.focus_handle)
4949 .when_some(search_query, |outline_panel, search_state| {
4950 outline_panel.child(
4951 h_flex()
4952 .py_1p5()
4953 .px_2()
4954 .h(DynamicSpacing::Base32.px(cx))
4955 .flex_shrink_0()
4956 .border_b_1()
4957 .border_color(cx.theme().colors().border)
4958 .gap_0p5()
4959 .child(Label::new("Searching:").color(Color::Muted))
4960 .child(Label::new(format!("'{}'", search_state.query))),
4961 )
4962 })
4963 .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
4964 .child(self.render_filter_footer(pinned, cx))
4965 }
4966}
4967
4968fn find_active_indent_guide_ix(
4969 outline_panel: &OutlinePanel,
4970 candidates: &[IndentGuideLayout],
4971) -> Option<usize> {
4972 let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
4973 return None;
4974 };
4975 let target_depth = outline_panel
4976 .cached_entries
4977 .get(*target_ix)
4978 .map(|cached_entry| cached_entry.depth)?;
4979
4980 let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
4981 .cached_entries
4982 .get(target_ix + 1)
4983 .filter(|cached_entry| cached_entry.depth > target_depth)
4984 .map(|entry| entry.depth)
4985 {
4986 (target_ix + 1, target_depth.saturating_sub(1))
4987 } else {
4988 (*target_ix, target_depth.saturating_sub(1))
4989 };
4990
4991 candidates
4992 .iter()
4993 .enumerate()
4994 .find(|(_, guide)| {
4995 guide.offset.y <= target_ix
4996 && target_ix < guide.offset.y + guide.length
4997 && guide.offset.x == target_depth
4998 })
4999 .map(|(ix, _)| ix)
5000}
5001
5002fn subscribe_for_editor_events(
5003 editor: &Entity<Editor>,
5004 window: &mut Window,
5005 cx: &mut Context<OutlinePanel>,
5006) -> Subscription {
5007 let debounce = Some(UPDATE_DEBOUNCE);
5008 cx.subscribe_in(
5009 editor,
5010 window,
5011 move |outline_panel, editor, e: &EditorEvent, window, cx| {
5012 if !outline_panel.active {
5013 return;
5014 }
5015 match e {
5016 EditorEvent::SelectionsChanged { local: true } => {
5017 outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5018 cx.notify();
5019 }
5020 EditorEvent::ExcerptsAdded { excerpts, .. } => {
5021 outline_panel
5022 .new_entries_for_fs_update
5023 .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
5024 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5025 }
5026 EditorEvent::ExcerptsRemoved { ids } => {
5027 let mut ids = ids.iter().collect::<HashSet<_>>();
5028 for excerpts in outline_panel.excerpts.values_mut() {
5029 excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
5030 if ids.is_empty() {
5031 break;
5032 }
5033 }
5034 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5035 }
5036 EditorEvent::ExcerptsExpanded { ids } => {
5037 outline_panel.invalidate_outlines(ids);
5038 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5039 if update_cached_items {
5040 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5041 }
5042 }
5043 EditorEvent::ExcerptsEdited { ids } => {
5044 outline_panel.invalidate_outlines(ids);
5045 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5046 if update_cached_items {
5047 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5048 }
5049 }
5050 EditorEvent::BufferFoldToggled { ids, .. } => {
5051 outline_panel.invalidate_outlines(ids);
5052 let mut latest_unfolded_buffer_id = None;
5053 let mut latest_folded_buffer_id = None;
5054 let mut ignore_selections_change = false;
5055 outline_panel.new_entries_for_fs_update.extend(
5056 ids.iter()
5057 .filter(|id| {
5058 outline_panel
5059 .excerpts
5060 .iter()
5061 .find_map(|(buffer_id, excerpts)| {
5062 if excerpts.contains_key(id) {
5063 ignore_selections_change |= outline_panel
5064 .preserve_selection_on_buffer_fold_toggles
5065 .remove(buffer_id);
5066 Some(buffer_id)
5067 } else {
5068 None
5069 }
5070 })
5071 .map(|buffer_id| {
5072 if editor.read(cx).is_buffer_folded(*buffer_id, cx) {
5073 latest_folded_buffer_id = Some(*buffer_id);
5074 false
5075 } else {
5076 latest_unfolded_buffer_id = Some(*buffer_id);
5077 true
5078 }
5079 })
5080 .unwrap_or(true)
5081 })
5082 .copied(),
5083 );
5084 if !ignore_selections_change {
5085 if let Some(entry_to_select) = latest_unfolded_buffer_id
5086 .or(latest_folded_buffer_id)
5087 .and_then(|toggled_buffer_id| {
5088 outline_panel.fs_entries.iter().find_map(
5089 |fs_entry| match fs_entry {
5090 FsEntry::ExternalFile(external) => {
5091 if external.buffer_id == toggled_buffer_id {
5092 Some(fs_entry.clone())
5093 } else {
5094 None
5095 }
5096 }
5097 FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5098 if *buffer_id == toggled_buffer_id {
5099 Some(fs_entry.clone())
5100 } else {
5101 None
5102 }
5103 }
5104 FsEntry::Directory(..) => None,
5105 },
5106 )
5107 })
5108 .map(PanelEntry::Fs)
5109 {
5110 outline_panel.select_entry(entry_to_select, true, window, cx);
5111 }
5112 }
5113
5114 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5115 }
5116 EditorEvent::Reparsed(buffer_id) => {
5117 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
5118 for (_, excerpt) in excerpts {
5119 excerpt.invalidate_outlines();
5120 }
5121 }
5122 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5123 if update_cached_items {
5124 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5125 }
5126 }
5127 _ => {}
5128 }
5129 },
5130 )
5131}
5132
5133fn empty_icon() -> AnyElement {
5134 h_flex()
5135 .size(IconSize::default().rems())
5136 .invisible()
5137 .flex_none()
5138 .into_any_element()
5139}
5140
5141fn horizontal_separator(cx: &mut App) -> Div {
5142 div().mx_2().border_primary(cx).border_t_1()
5143}
5144
5145#[derive(Debug, Default)]
5146struct GenerationState {
5147 entries: Vec<CachedEntry>,
5148 match_candidates: Vec<StringMatchCandidate>,
5149 max_width_estimate_and_index: Option<(u64, usize)>,
5150}
5151
5152impl GenerationState {
5153 fn clear(&mut self) {
5154 self.entries.clear();
5155 self.match_candidates.clear();
5156 self.max_width_estimate_and_index = None;
5157 }
5158}
5159
5160#[cfg(test)]
5161mod tests {
5162 use db::indoc;
5163 use gpui::{TestAppContext, VisualTestContext, WindowHandle};
5164 use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
5165 use pretty_assertions::assert_eq;
5166 use project::FakeFs;
5167 use search::project_search::{self, perform_project_search};
5168 use serde_json::json;
5169 use util::path;
5170 use workspace::{OpenOptions, OpenVisible};
5171
5172 use super::*;
5173
5174 const SELECTED_MARKER: &str = " <==== selected";
5175
5176 #[gpui::test(iterations = 10)]
5177 async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5178 init_test(cx);
5179
5180 let fs = FakeFs::new(cx.background_executor.clone());
5181 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5182 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5183 project.read_with(cx, |project, _| {
5184 project.languages().add(Arc::new(rust_lang()))
5185 });
5186 let workspace = add_outline_panel(&project, cx).await;
5187 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5188 let outline_panel = outline_panel(&workspace, cx);
5189 outline_panel.update_in(cx, |outline_panel, window, cx| {
5190 outline_panel.set_active(true, window, cx)
5191 });
5192
5193 workspace
5194 .update(cx, |workspace, window, cx| {
5195 ProjectSearchView::deploy_search(
5196 workspace,
5197 &workspace::DeploySearch::default(),
5198 window,
5199 cx,
5200 )
5201 })
5202 .unwrap();
5203 let search_view = workspace
5204 .update(cx, |workspace, _, cx| {
5205 workspace
5206 .active_pane()
5207 .read(cx)
5208 .items()
5209 .find_map(|item| item.downcast::<ProjectSearchView>())
5210 .expect("Project search view expected to appear after new search event trigger")
5211 })
5212 .unwrap();
5213
5214 let query = "param_names_for_lifetime_elision_hints";
5215 perform_project_search(&search_view, query, cx);
5216 search_view.update(cx, |search_view, cx| {
5217 search_view
5218 .results_editor()
5219 .update(cx, |results_editor, cx| {
5220 assert_eq!(
5221 results_editor.display_text(cx).match_indices(query).count(),
5222 9
5223 );
5224 });
5225 });
5226
5227 let all_matches = r#"/rust-analyzer/
5228 crates/
5229 ide/src/
5230 inlay_hints/
5231 fn_lifetime_fn.rs
5232 search: match config.param_names_for_lifetime_elision_hints {
5233 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5234 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5235 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5236 inlay_hints.rs
5237 search: pub param_names_for_lifetime_elision_hints: bool,
5238 search: param_names_for_lifetime_elision_hints: self
5239 static_index.rs
5240 search: param_names_for_lifetime_elision_hints: false,
5241 rust-analyzer/src/
5242 cli/
5243 analysis_stats.rs
5244 search: param_names_for_lifetime_elision_hints: true,
5245 config.rs
5246 search: param_names_for_lifetime_elision_hints: self"#;
5247 let select_first_in_all_matches = |line_to_select: &str| {
5248 assert!(all_matches.contains(line_to_select));
5249 all_matches.replacen(
5250 line_to_select,
5251 &format!("{line_to_select}{SELECTED_MARKER}"),
5252 1,
5253 )
5254 };
5255
5256 cx.executor()
5257 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5258 cx.run_until_parked();
5259 outline_panel.update(cx, |outline_panel, cx| {
5260 assert_eq!(
5261 display_entries(
5262 &project,
5263 &snapshot(&outline_panel, cx),
5264 &outline_panel.cached_entries,
5265 outline_panel.selected_entry(),
5266 cx,
5267 ),
5268 select_first_in_all_matches(
5269 "search: match config.param_names_for_lifetime_elision_hints {"
5270 )
5271 );
5272 });
5273
5274 outline_panel.update_in(cx, |outline_panel, window, cx| {
5275 outline_panel.select_parent(&SelectParent, window, cx);
5276 assert_eq!(
5277 display_entries(
5278 &project,
5279 &snapshot(&outline_panel, cx),
5280 &outline_panel.cached_entries,
5281 outline_panel.selected_entry(),
5282 cx,
5283 ),
5284 select_first_in_all_matches("fn_lifetime_fn.rs")
5285 );
5286 });
5287 outline_panel.update_in(cx, |outline_panel, window, cx| {
5288 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5289 });
5290 cx.executor()
5291 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5292 cx.run_until_parked();
5293 outline_panel.update(cx, |outline_panel, cx| {
5294 assert_eq!(
5295 display_entries(
5296 &project,
5297 &snapshot(&outline_panel, cx),
5298 &outline_panel.cached_entries,
5299 outline_panel.selected_entry(),
5300 cx,
5301 ),
5302 format!(
5303 r#"/rust-analyzer/
5304 crates/
5305 ide/src/
5306 inlay_hints/
5307 fn_lifetime_fn.rs{SELECTED_MARKER}
5308 inlay_hints.rs
5309 search: pub param_names_for_lifetime_elision_hints: bool,
5310 search: param_names_for_lifetime_elision_hints: self
5311 static_index.rs
5312 search: param_names_for_lifetime_elision_hints: false,
5313 rust-analyzer/src/
5314 cli/
5315 analysis_stats.rs
5316 search: param_names_for_lifetime_elision_hints: true,
5317 config.rs
5318 search: param_names_for_lifetime_elision_hints: self"#,
5319 )
5320 );
5321 });
5322
5323 outline_panel.update_in(cx, |outline_panel, window, cx| {
5324 outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5325 });
5326 cx.executor()
5327 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5328 cx.run_until_parked();
5329 outline_panel.update_in(cx, |outline_panel, window, cx| {
5330 outline_panel.select_parent(&SelectParent, window, cx);
5331 assert_eq!(
5332 display_entries(
5333 &project,
5334 &snapshot(&outline_panel, cx),
5335 &outline_panel.cached_entries,
5336 outline_panel.selected_entry(),
5337 cx,
5338 ),
5339 select_first_in_all_matches("inlay_hints/")
5340 );
5341 });
5342
5343 outline_panel.update_in(cx, |outline_panel, window, cx| {
5344 outline_panel.select_parent(&SelectParent, window, cx);
5345 assert_eq!(
5346 display_entries(
5347 &project,
5348 &snapshot(&outline_panel, cx),
5349 &outline_panel.cached_entries,
5350 outline_panel.selected_entry(),
5351 cx,
5352 ),
5353 select_first_in_all_matches("ide/src/")
5354 );
5355 });
5356
5357 outline_panel.update_in(cx, |outline_panel, window, cx| {
5358 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5359 });
5360 cx.executor()
5361 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5362 cx.run_until_parked();
5363 outline_panel.update(cx, |outline_panel, cx| {
5364 assert_eq!(
5365 display_entries(
5366 &project,
5367 &snapshot(&outline_panel, cx),
5368 &outline_panel.cached_entries,
5369 outline_panel.selected_entry(),
5370 cx,
5371 ),
5372 format!(
5373 r#"/rust-analyzer/
5374 crates/
5375 ide/src/{SELECTED_MARKER}
5376 rust-analyzer/src/
5377 cli/
5378 analysis_stats.rs
5379 search: param_names_for_lifetime_elision_hints: true,
5380 config.rs
5381 search: param_names_for_lifetime_elision_hints: self"#,
5382 )
5383 );
5384 });
5385 outline_panel.update_in(cx, |outline_panel, window, cx| {
5386 outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5387 });
5388 cx.executor()
5389 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5390 cx.run_until_parked();
5391 outline_panel.update(cx, |outline_panel, cx| {
5392 assert_eq!(
5393 display_entries(
5394 &project,
5395 &snapshot(&outline_panel, cx),
5396 &outline_panel.cached_entries,
5397 outline_panel.selected_entry(),
5398 cx,
5399 ),
5400 select_first_in_all_matches("ide/src/")
5401 );
5402 });
5403 }
5404
5405 #[gpui::test(iterations = 10)]
5406 async fn test_item_filtering(cx: &mut TestAppContext) {
5407 init_test(cx);
5408
5409 let fs = FakeFs::new(cx.background_executor.clone());
5410 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5411 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5412 project.read_with(cx, |project, _| {
5413 project.languages().add(Arc::new(rust_lang()))
5414 });
5415 let workspace = add_outline_panel(&project, cx).await;
5416 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5417 let outline_panel = outline_panel(&workspace, cx);
5418 outline_panel.update_in(cx, |outline_panel, window, cx| {
5419 outline_panel.set_active(true, window, cx)
5420 });
5421
5422 workspace
5423 .update(cx, |workspace, window, cx| {
5424 ProjectSearchView::deploy_search(
5425 workspace,
5426 &workspace::DeploySearch::default(),
5427 window,
5428 cx,
5429 )
5430 })
5431 .unwrap();
5432 let search_view = workspace
5433 .update(cx, |workspace, _, cx| {
5434 workspace
5435 .active_pane()
5436 .read(cx)
5437 .items()
5438 .find_map(|item| item.downcast::<ProjectSearchView>())
5439 .expect("Project search view expected to appear after new search event trigger")
5440 })
5441 .unwrap();
5442
5443 let query = "param_names_for_lifetime_elision_hints";
5444 perform_project_search(&search_view, query, cx);
5445 search_view.update(cx, |search_view, cx| {
5446 search_view
5447 .results_editor()
5448 .update(cx, |results_editor, cx| {
5449 assert_eq!(
5450 results_editor.display_text(cx).match_indices(query).count(),
5451 9
5452 );
5453 });
5454 });
5455 let all_matches = r#"/rust-analyzer/
5456 crates/
5457 ide/src/
5458 inlay_hints/
5459 fn_lifetime_fn.rs
5460 search: match config.param_names_for_lifetime_elision_hints {
5461 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5462 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5463 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5464 inlay_hints.rs
5465 search: pub param_names_for_lifetime_elision_hints: bool,
5466 search: param_names_for_lifetime_elision_hints: self
5467 static_index.rs
5468 search: param_names_for_lifetime_elision_hints: false,
5469 rust-analyzer/src/
5470 cli/
5471 analysis_stats.rs
5472 search: param_names_for_lifetime_elision_hints: true,
5473 config.rs
5474 search: param_names_for_lifetime_elision_hints: self"#;
5475
5476 cx.executor()
5477 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5478 cx.run_until_parked();
5479 outline_panel.update(cx, |outline_panel, cx| {
5480 assert_eq!(
5481 display_entries(
5482 &project,
5483 &snapshot(&outline_panel, cx),
5484 &outline_panel.cached_entries,
5485 None,
5486 cx,
5487 ),
5488 all_matches,
5489 );
5490 });
5491
5492 let filter_text = "a";
5493 outline_panel.update_in(cx, |outline_panel, window, cx| {
5494 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5495 filter_editor.set_text(filter_text, window, cx);
5496 });
5497 });
5498 cx.executor()
5499 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5500 cx.run_until_parked();
5501
5502 outline_panel.update(cx, |outline_panel, cx| {
5503 assert_eq!(
5504 display_entries(
5505 &project,
5506 &snapshot(&outline_panel, cx),
5507 &outline_panel.cached_entries,
5508 None,
5509 cx,
5510 ),
5511 all_matches
5512 .lines()
5513 .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5514 .filter(|item| item.contains(filter_text))
5515 .collect::<Vec<_>>()
5516 .join("\n"),
5517 );
5518 });
5519
5520 outline_panel.update_in(cx, |outline_panel, window, cx| {
5521 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5522 filter_editor.set_text("", window, cx);
5523 });
5524 });
5525 cx.executor()
5526 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5527 cx.run_until_parked();
5528 outline_panel.update(cx, |outline_panel, cx| {
5529 assert_eq!(
5530 display_entries(
5531 &project,
5532 &snapshot(&outline_panel, cx),
5533 &outline_panel.cached_entries,
5534 None,
5535 cx,
5536 ),
5537 all_matches,
5538 );
5539 });
5540 }
5541
5542 #[gpui::test(iterations = 10)]
5543 async fn test_item_opening(cx: &mut TestAppContext) {
5544 init_test(cx);
5545
5546 let fs = FakeFs::new(cx.background_executor.clone());
5547 populate_with_test_ra_project(&fs, path!("/rust-analyzer")).await;
5548 let project = Project::test(fs.clone(), [path!("/rust-analyzer").as_ref()], cx).await;
5549 project.read_with(cx, |project, _| {
5550 project.languages().add(Arc::new(rust_lang()))
5551 });
5552 let workspace = add_outline_panel(&project, cx).await;
5553 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5554 let outline_panel = outline_panel(&workspace, cx);
5555 outline_panel.update_in(cx, |outline_panel, window, cx| {
5556 outline_panel.set_active(true, window, cx)
5557 });
5558
5559 workspace
5560 .update(cx, |workspace, window, cx| {
5561 ProjectSearchView::deploy_search(
5562 workspace,
5563 &workspace::DeploySearch::default(),
5564 window,
5565 cx,
5566 )
5567 })
5568 .unwrap();
5569 let search_view = workspace
5570 .update(cx, |workspace, _, cx| {
5571 workspace
5572 .active_pane()
5573 .read(cx)
5574 .items()
5575 .find_map(|item| item.downcast::<ProjectSearchView>())
5576 .expect("Project search view expected to appear after new search event trigger")
5577 })
5578 .unwrap();
5579
5580 let query = "param_names_for_lifetime_elision_hints";
5581 perform_project_search(&search_view, query, cx);
5582 search_view.update(cx, |search_view, cx| {
5583 search_view
5584 .results_editor()
5585 .update(cx, |results_editor, cx| {
5586 assert_eq!(
5587 results_editor.display_text(cx).match_indices(query).count(),
5588 9
5589 );
5590 });
5591 });
5592 let root_path = format!("{}/", path!("/rust-analyzer"));
5593 let all_matches = format!(
5594 r#"{root_path}
5595 crates/
5596 ide/src/
5597 inlay_hints/
5598 fn_lifetime_fn.rs
5599 search: match config.param_names_for_lifetime_elision_hints {{
5600 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
5601 search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
5602 search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
5603 inlay_hints.rs
5604 search: pub param_names_for_lifetime_elision_hints: bool,
5605 search: param_names_for_lifetime_elision_hints: self
5606 static_index.rs
5607 search: param_names_for_lifetime_elision_hints: false,
5608 rust-analyzer/src/
5609 cli/
5610 analysis_stats.rs
5611 search: param_names_for_lifetime_elision_hints: true,
5612 config.rs
5613 search: param_names_for_lifetime_elision_hints: self"#
5614 );
5615 let select_first_in_all_matches = |line_to_select: &str| {
5616 assert!(all_matches.contains(line_to_select));
5617 all_matches.replacen(
5618 line_to_select,
5619 &format!("{line_to_select}{SELECTED_MARKER}"),
5620 1,
5621 )
5622 };
5623 cx.executor()
5624 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5625 cx.run_until_parked();
5626
5627 let active_editor = outline_panel.update(cx, |outline_panel, _| {
5628 outline_panel
5629 .active_editor()
5630 .expect("should have an active editor open")
5631 });
5632 let initial_outline_selection =
5633 "search: match config.param_names_for_lifetime_elision_hints {";
5634 outline_panel.update_in(cx, |outline_panel, window, cx| {
5635 assert_eq!(
5636 display_entries(
5637 &project,
5638 &snapshot(&outline_panel, cx),
5639 &outline_panel.cached_entries,
5640 outline_panel.selected_entry(),
5641 cx,
5642 ),
5643 select_first_in_all_matches(initial_outline_selection)
5644 );
5645 assert_eq!(
5646 selected_row_text(&active_editor, cx),
5647 initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5648 "Should place the initial editor selection on the corresponding search result"
5649 );
5650
5651 outline_panel.select_next(&SelectNext, window, cx);
5652 outline_panel.select_next(&SelectNext, window, cx);
5653 });
5654
5655 let navigated_outline_selection =
5656 "search: Some(it) if config.param_names_for_lifetime_elision_hints => {";
5657 outline_panel.update(cx, |outline_panel, cx| {
5658 assert_eq!(
5659 display_entries(
5660 &project,
5661 &snapshot(&outline_panel, cx),
5662 &outline_panel.cached_entries,
5663 outline_panel.selected_entry(),
5664 cx,
5665 ),
5666 select_first_in_all_matches(navigated_outline_selection)
5667 );
5668 });
5669 cx.executor()
5670 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5671 outline_panel.update(cx, |_, cx| {
5672 assert_eq!(
5673 selected_row_text(&active_editor, cx),
5674 navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5675 "Should still have the initial caret position after SelectNext calls"
5676 );
5677 });
5678
5679 outline_panel.update_in(cx, |outline_panel, window, cx| {
5680 outline_panel.open(&Open, window, cx);
5681 });
5682 outline_panel.update(cx, |_outline_panel, cx| {
5683 assert_eq!(
5684 selected_row_text(&active_editor, cx),
5685 navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5686 "After opening, should move the caret to the opened outline entry's position"
5687 );
5688 });
5689
5690 outline_panel.update_in(cx, |outline_panel, window, cx| {
5691 outline_panel.select_next(&SelectNext, window, cx);
5692 });
5693 let next_navigated_outline_selection = "search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },";
5694 outline_panel.update(cx, |outline_panel, cx| {
5695 assert_eq!(
5696 display_entries(
5697 &project,
5698 &snapshot(&outline_panel, cx),
5699 &outline_panel.cached_entries,
5700 outline_panel.selected_entry(),
5701 cx,
5702 ),
5703 select_first_in_all_matches(next_navigated_outline_selection)
5704 );
5705 });
5706 cx.executor()
5707 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5708 outline_panel.update(cx, |_outline_panel, cx| {
5709 assert_eq!(
5710 selected_row_text(&active_editor, cx),
5711 next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5712 "Should again preserve the selection after another SelectNext call"
5713 );
5714 });
5715
5716 outline_panel.update_in(cx, |outline_panel, window, cx| {
5717 outline_panel.open_excerpts(&editor::OpenExcerpts, window, cx);
5718 });
5719 cx.executor()
5720 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5721 cx.run_until_parked();
5722 let new_active_editor = outline_panel.update(cx, |outline_panel, _| {
5723 outline_panel
5724 .active_editor()
5725 .expect("should have an active editor open")
5726 });
5727 outline_panel.update(cx, |outline_panel, cx| {
5728 assert_ne!(
5729 active_editor, new_active_editor,
5730 "After opening an excerpt, new editor should be open"
5731 );
5732 assert_eq!(
5733 display_entries(
5734 &project,
5735 &snapshot(&outline_panel, cx),
5736 &outline_panel.cached_entries,
5737 outline_panel.selected_entry(),
5738 cx,
5739 ),
5740 "fn_lifetime_fn.rs <==== selected"
5741 );
5742 assert_eq!(
5743 selected_row_text(&new_active_editor, cx),
5744 next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5745 "When opening the excerpt, should navigate to the place corresponding the outline entry"
5746 );
5747 });
5748 }
5749
5750 #[gpui::test]
5751 async fn test_multiple_workrees(cx: &mut TestAppContext) {
5752 init_test(cx);
5753
5754 let fs = FakeFs::new(cx.background_executor.clone());
5755 fs.insert_tree(
5756 "/root",
5757 json!({
5758 "one": {
5759 "a.txt": "aaa aaa"
5760 },
5761 "two": {
5762 "b.txt": "a aaa"
5763 }
5764
5765 }),
5766 )
5767 .await;
5768 let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await;
5769 let workspace = add_outline_panel(&project, cx).await;
5770 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5771 let outline_panel = outline_panel(&workspace, cx);
5772 outline_panel.update_in(cx, |outline_panel, window, cx| {
5773 outline_panel.set_active(true, window, cx)
5774 });
5775
5776 let items = workspace
5777 .update(cx, |workspace, window, cx| {
5778 workspace.open_paths(
5779 vec![PathBuf::from("/root/two")],
5780 OpenOptions {
5781 visible: Some(OpenVisible::OnlyDirectories),
5782 ..Default::default()
5783 },
5784 None,
5785 window,
5786 cx,
5787 )
5788 })
5789 .unwrap()
5790 .await;
5791 assert_eq!(items.len(), 1, "Were opening another worktree directory");
5792 assert!(
5793 items[0].is_none(),
5794 "Directory should be opened successfully"
5795 );
5796
5797 workspace
5798 .update(cx, |workspace, window, cx| {
5799 ProjectSearchView::deploy_search(
5800 workspace,
5801 &workspace::DeploySearch::default(),
5802 window,
5803 cx,
5804 )
5805 })
5806 .unwrap();
5807 let search_view = workspace
5808 .update(cx, |workspace, _, cx| {
5809 workspace
5810 .active_pane()
5811 .read(cx)
5812 .items()
5813 .find_map(|item| item.downcast::<ProjectSearchView>())
5814 .expect("Project search view expected to appear after new search event trigger")
5815 })
5816 .unwrap();
5817
5818 let query = "aaa";
5819 perform_project_search(&search_view, query, cx);
5820 search_view.update(cx, |search_view, cx| {
5821 search_view
5822 .results_editor()
5823 .update(cx, |results_editor, cx| {
5824 assert_eq!(
5825 results_editor.display_text(cx).match_indices(query).count(),
5826 3
5827 );
5828 });
5829 });
5830
5831 cx.executor()
5832 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5833 cx.run_until_parked();
5834 outline_panel.update(cx, |outline_panel, cx| {
5835 assert_eq!(
5836 display_entries(
5837 &project,
5838 &snapshot(&outline_panel, cx),
5839 &outline_panel.cached_entries,
5840 outline_panel.selected_entry(),
5841 cx,
5842 ),
5843 r#"/root/one/
5844 a.txt
5845 search: aaa aaa <==== selected
5846 search: aaa aaa
5847/root/two/
5848 b.txt
5849 search: a aaa"#
5850 );
5851 });
5852
5853 outline_panel.update_in(cx, |outline_panel, window, cx| {
5854 outline_panel.select_previous(&SelectPrevious, window, cx);
5855 outline_panel.open(&Open, window, cx);
5856 });
5857 cx.executor()
5858 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5859 cx.run_until_parked();
5860 outline_panel.update(cx, |outline_panel, cx| {
5861 assert_eq!(
5862 display_entries(
5863 &project,
5864 &snapshot(&outline_panel, cx),
5865 &outline_panel.cached_entries,
5866 outline_panel.selected_entry(),
5867 cx,
5868 ),
5869 r#"/root/one/
5870 a.txt <==== selected
5871/root/two/
5872 b.txt
5873 search: a aaa"#
5874 );
5875 });
5876
5877 outline_panel.update_in(cx, |outline_panel, window, cx| {
5878 outline_panel.select_next(&SelectNext, window, cx);
5879 outline_panel.open(&Open, window, cx);
5880 });
5881 cx.executor()
5882 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5883 cx.run_until_parked();
5884 outline_panel.update(cx, |outline_panel, cx| {
5885 assert_eq!(
5886 display_entries(
5887 &project,
5888 &snapshot(&outline_panel, cx),
5889 &outline_panel.cached_entries,
5890 outline_panel.selected_entry(),
5891 cx,
5892 ),
5893 r#"/root/one/
5894 a.txt
5895/root/two/ <==== selected"#
5896 );
5897 });
5898
5899 outline_panel.update_in(cx, |outline_panel, window, cx| {
5900 outline_panel.open(&Open, window, cx);
5901 });
5902 cx.executor()
5903 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5904 cx.run_until_parked();
5905 outline_panel.update(cx, |outline_panel, cx| {
5906 assert_eq!(
5907 display_entries(
5908 &project,
5909 &snapshot(&outline_panel, cx),
5910 &outline_panel.cached_entries,
5911 outline_panel.selected_entry(),
5912 cx,
5913 ),
5914 r#"/root/one/
5915 a.txt
5916/root/two/ <==== selected
5917 b.txt
5918 search: a aaa"#
5919 );
5920 });
5921 }
5922
5923 #[gpui::test]
5924 async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
5925 init_test(cx);
5926
5927 let root = path!("/root");
5928 let fs = FakeFs::new(cx.background_executor.clone());
5929 fs.insert_tree(
5930 root,
5931 json!({
5932 "src": {
5933 "lib.rs": indoc!("
5934#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5935struct OutlineEntryExcerpt {
5936 id: ExcerptId,
5937 buffer_id: BufferId,
5938 range: ExcerptRange<language::Anchor>,
5939}"),
5940 }
5941 }),
5942 )
5943 .await;
5944 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
5945 project.read_with(cx, |project, _| {
5946 project.languages().add(Arc::new(
5947 rust_lang()
5948 .with_outline_query(
5949 r#"
5950 (struct_item
5951 (visibility_modifier)? @context
5952 "struct" @context
5953 name: (_) @name) @item
5954
5955 (field_declaration
5956 (visibility_modifier)? @context
5957 name: (_) @name) @item
5958"#,
5959 )
5960 .unwrap(),
5961 ))
5962 });
5963 let workspace = add_outline_panel(&project, cx).await;
5964 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5965 let outline_panel = outline_panel(&workspace, cx);
5966 cx.update(|window, cx| {
5967 outline_panel.update(cx, |outline_panel, cx| {
5968 outline_panel.set_active(true, window, cx)
5969 });
5970 });
5971
5972 let _editor = workspace
5973 .update(cx, |workspace, window, cx| {
5974 workspace.open_abs_path(
5975 PathBuf::from(path!("/root/src/lib.rs")),
5976 OpenOptions {
5977 visible: Some(OpenVisible::All),
5978 ..Default::default()
5979 },
5980 window,
5981 cx,
5982 )
5983 })
5984 .unwrap()
5985 .await
5986 .expect("Failed to open Rust source file")
5987 .downcast::<Editor>()
5988 .expect("Should open an editor for Rust source file");
5989
5990 cx.executor()
5991 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5992 cx.run_until_parked();
5993 outline_panel.update(cx, |outline_panel, cx| {
5994 assert_eq!(
5995 display_entries(
5996 &project,
5997 &snapshot(&outline_panel, cx),
5998 &outline_panel.cached_entries,
5999 outline_panel.selected_entry(),
6000 cx,
6001 ),
6002 indoc!(
6003 "
6004outline: struct OutlineEntryExcerpt
6005 outline: id
6006 outline: buffer_id
6007 outline: range"
6008 )
6009 );
6010 });
6011
6012 cx.update(|window, cx| {
6013 outline_panel.update(cx, |outline_panel, cx| {
6014 outline_panel.select_next(&SelectNext, window, cx);
6015 });
6016 });
6017 cx.executor()
6018 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6019 cx.run_until_parked();
6020 outline_panel.update(cx, |outline_panel, cx| {
6021 assert_eq!(
6022 display_entries(
6023 &project,
6024 &snapshot(&outline_panel, cx),
6025 &outline_panel.cached_entries,
6026 outline_panel.selected_entry(),
6027 cx,
6028 ),
6029 indoc!(
6030 "
6031outline: struct OutlineEntryExcerpt <==== selected
6032 outline: id
6033 outline: buffer_id
6034 outline: range"
6035 )
6036 );
6037 });
6038
6039 cx.update(|window, cx| {
6040 outline_panel.update(cx, |outline_panel, cx| {
6041 outline_panel.select_next(&SelectNext, window, cx);
6042 });
6043 });
6044 cx.executor()
6045 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6046 cx.run_until_parked();
6047 outline_panel.update(cx, |outline_panel, cx| {
6048 assert_eq!(
6049 display_entries(
6050 &project,
6051 &snapshot(&outline_panel, cx),
6052 &outline_panel.cached_entries,
6053 outline_panel.selected_entry(),
6054 cx,
6055 ),
6056 indoc!(
6057 "
6058outline: struct OutlineEntryExcerpt
6059 outline: id <==== selected
6060 outline: buffer_id
6061 outline: range"
6062 )
6063 );
6064 });
6065
6066 cx.update(|window, cx| {
6067 outline_panel.update(cx, |outline_panel, cx| {
6068 outline_panel.select_next(&SelectNext, window, cx);
6069 });
6070 });
6071 cx.executor()
6072 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6073 cx.run_until_parked();
6074 outline_panel.update(cx, |outline_panel, cx| {
6075 assert_eq!(
6076 display_entries(
6077 &project,
6078 &snapshot(&outline_panel, cx),
6079 &outline_panel.cached_entries,
6080 outline_panel.selected_entry(),
6081 cx,
6082 ),
6083 indoc!(
6084 "
6085outline: struct OutlineEntryExcerpt
6086 outline: id
6087 outline: buffer_id <==== selected
6088 outline: range"
6089 )
6090 );
6091 });
6092
6093 cx.update(|window, cx| {
6094 outline_panel.update(cx, |outline_panel, cx| {
6095 outline_panel.select_next(&SelectNext, window, cx);
6096 });
6097 });
6098 cx.executor()
6099 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6100 cx.run_until_parked();
6101 outline_panel.update(cx, |outline_panel, cx| {
6102 assert_eq!(
6103 display_entries(
6104 &project,
6105 &snapshot(&outline_panel, cx),
6106 &outline_panel.cached_entries,
6107 outline_panel.selected_entry(),
6108 cx,
6109 ),
6110 indoc!(
6111 "
6112outline: struct OutlineEntryExcerpt
6113 outline: id
6114 outline: buffer_id
6115 outline: range <==== selected"
6116 )
6117 );
6118 });
6119
6120 cx.update(|window, cx| {
6121 outline_panel.update(cx, |outline_panel, cx| {
6122 outline_panel.select_next(&SelectNext, window, cx);
6123 });
6124 });
6125 cx.executor()
6126 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6127 cx.run_until_parked();
6128 outline_panel.update(cx, |outline_panel, cx| {
6129 assert_eq!(
6130 display_entries(
6131 &project,
6132 &snapshot(&outline_panel, cx),
6133 &outline_panel.cached_entries,
6134 outline_panel.selected_entry(),
6135 cx,
6136 ),
6137 indoc!(
6138 "
6139outline: struct OutlineEntryExcerpt <==== selected
6140 outline: id
6141 outline: buffer_id
6142 outline: range"
6143 )
6144 );
6145 });
6146
6147 cx.update(|window, cx| {
6148 outline_panel.update(cx, |outline_panel, cx| {
6149 outline_panel.select_previous(&SelectPrevious, window, cx);
6150 });
6151 });
6152 cx.executor()
6153 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6154 cx.run_until_parked();
6155 outline_panel.update(cx, |outline_panel, cx| {
6156 assert_eq!(
6157 display_entries(
6158 &project,
6159 &snapshot(&outline_panel, cx),
6160 &outline_panel.cached_entries,
6161 outline_panel.selected_entry(),
6162 cx,
6163 ),
6164 indoc!(
6165 "
6166outline: struct OutlineEntryExcerpt
6167 outline: id
6168 outline: buffer_id
6169 outline: range <==== selected"
6170 )
6171 );
6172 });
6173
6174 cx.update(|window, cx| {
6175 outline_panel.update(cx, |outline_panel, cx| {
6176 outline_panel.select_previous(&SelectPrevious, window, cx);
6177 });
6178 });
6179 cx.executor()
6180 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6181 cx.run_until_parked();
6182 outline_panel.update(cx, |outline_panel, cx| {
6183 assert_eq!(
6184 display_entries(
6185 &project,
6186 &snapshot(&outline_panel, cx),
6187 &outline_panel.cached_entries,
6188 outline_panel.selected_entry(),
6189 cx,
6190 ),
6191 indoc!(
6192 "
6193outline: struct OutlineEntryExcerpt
6194 outline: id
6195 outline: buffer_id <==== selected
6196 outline: range"
6197 )
6198 );
6199 });
6200
6201 cx.update(|window, cx| {
6202 outline_panel.update(cx, |outline_panel, cx| {
6203 outline_panel.select_previous(&SelectPrevious, window, cx);
6204 });
6205 });
6206 cx.executor()
6207 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6208 cx.run_until_parked();
6209 outline_panel.update(cx, |outline_panel, cx| {
6210 assert_eq!(
6211 display_entries(
6212 &project,
6213 &snapshot(&outline_panel, cx),
6214 &outline_panel.cached_entries,
6215 outline_panel.selected_entry(),
6216 cx,
6217 ),
6218 indoc!(
6219 "
6220outline: struct OutlineEntryExcerpt
6221 outline: id <==== selected
6222 outline: buffer_id
6223 outline: range"
6224 )
6225 );
6226 });
6227
6228 cx.update(|window, cx| {
6229 outline_panel.update(cx, |outline_panel, cx| {
6230 outline_panel.select_previous(&SelectPrevious, window, cx);
6231 });
6232 });
6233 cx.executor()
6234 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6235 cx.run_until_parked();
6236 outline_panel.update(cx, |outline_panel, cx| {
6237 assert_eq!(
6238 display_entries(
6239 &project,
6240 &snapshot(&outline_panel, cx),
6241 &outline_panel.cached_entries,
6242 outline_panel.selected_entry(),
6243 cx,
6244 ),
6245 indoc!(
6246 "
6247outline: struct OutlineEntryExcerpt <==== selected
6248 outline: id
6249 outline: buffer_id
6250 outline: range"
6251 )
6252 );
6253 });
6254
6255 cx.update(|window, cx| {
6256 outline_panel.update(cx, |outline_panel, cx| {
6257 outline_panel.select_previous(&SelectPrevious, window, cx);
6258 });
6259 });
6260 cx.executor()
6261 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6262 cx.run_until_parked();
6263 outline_panel.update(cx, |outline_panel, cx| {
6264 assert_eq!(
6265 display_entries(
6266 &project,
6267 &snapshot(&outline_panel, cx),
6268 &outline_panel.cached_entries,
6269 outline_panel.selected_entry(),
6270 cx,
6271 ),
6272 indoc!(
6273 "
6274outline: struct OutlineEntryExcerpt
6275 outline: id
6276 outline: buffer_id
6277 outline: range <==== selected"
6278 )
6279 );
6280 });
6281 }
6282
6283 #[gpui::test(iterations = 10)]
6284 async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6285 init_test(cx);
6286
6287 let root = "/frontend-project";
6288 let fs = FakeFs::new(cx.background_executor.clone());
6289 fs.insert_tree(
6290 root,
6291 json!({
6292 "public": {
6293 "lottie": {
6294 "syntax-tree.json": r#"{ "something": "static" }"#
6295 }
6296 },
6297 "src": {
6298 "app": {
6299 "(site)": {
6300 "(about)": {
6301 "jobs": {
6302 "[slug]": {
6303 "page.tsx": r#"static"#
6304 }
6305 }
6306 },
6307 "(blog)": {
6308 "post": {
6309 "[slug]": {
6310 "page.tsx": r#"static"#
6311 }
6312 }
6313 },
6314 }
6315 },
6316 "components": {
6317 "ErrorBoundary.tsx": r#"static"#,
6318 }
6319 }
6320
6321 }),
6322 )
6323 .await;
6324 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
6325 let workspace = add_outline_panel(&project, cx).await;
6326 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6327 let outline_panel = outline_panel(&workspace, cx);
6328 outline_panel.update_in(cx, |outline_panel, window, cx| {
6329 outline_panel.set_active(true, window, cx)
6330 });
6331
6332 workspace
6333 .update(cx, |workspace, window, cx| {
6334 ProjectSearchView::deploy_search(
6335 workspace,
6336 &workspace::DeploySearch::default(),
6337 window,
6338 cx,
6339 )
6340 })
6341 .unwrap();
6342 let search_view = workspace
6343 .update(cx, |workspace, _, cx| {
6344 workspace
6345 .active_pane()
6346 .read(cx)
6347 .items()
6348 .find_map(|item| item.downcast::<ProjectSearchView>())
6349 .expect("Project search view expected to appear after new search event trigger")
6350 })
6351 .unwrap();
6352
6353 let query = "static";
6354 perform_project_search(&search_view, query, cx);
6355 search_view.update(cx, |search_view, cx| {
6356 search_view
6357 .results_editor()
6358 .update(cx, |results_editor, cx| {
6359 assert_eq!(
6360 results_editor.display_text(cx).match_indices(query).count(),
6361 4
6362 );
6363 });
6364 });
6365
6366 cx.executor()
6367 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6368 cx.run_until_parked();
6369 outline_panel.update(cx, |outline_panel, cx| {
6370 assert_eq!(
6371 display_entries(
6372 &project,
6373 &snapshot(&outline_panel, cx),
6374 &outline_panel.cached_entries,
6375 outline_panel.selected_entry(),
6376 cx,
6377 ),
6378 r#"/frontend-project/
6379 public/lottie/
6380 syntax-tree.json
6381 search: { "something": "static" } <==== selected
6382 src/
6383 app/(site)/
6384 (about)/jobs/[slug]/
6385 page.tsx
6386 search: static
6387 (blog)/post/[slug]/
6388 page.tsx
6389 search: static
6390 components/
6391 ErrorBoundary.tsx
6392 search: static"#
6393 );
6394 });
6395
6396 outline_panel.update_in(cx, |outline_panel, window, cx| {
6397 // Move to 5th element in the list, 3 items down.
6398 for _ in 0..2 {
6399 outline_panel.select_next(&SelectNext, window, cx);
6400 }
6401 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6402 });
6403 cx.executor()
6404 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6405 cx.run_until_parked();
6406 outline_panel.update(cx, |outline_panel, cx| {
6407 assert_eq!(
6408 display_entries(
6409 &project,
6410 &snapshot(&outline_panel, cx),
6411 &outline_panel.cached_entries,
6412 outline_panel.selected_entry(),
6413 cx,
6414 ),
6415 r#"/frontend-project/
6416 public/lottie/
6417 syntax-tree.json
6418 search: { "something": "static" }
6419 src/
6420 app/(site)/ <==== selected
6421 components/
6422 ErrorBoundary.tsx
6423 search: static"#
6424 );
6425 });
6426
6427 outline_panel.update_in(cx, |outline_panel, window, cx| {
6428 // Move to the next visible non-FS entry
6429 for _ in 0..3 {
6430 outline_panel.select_next(&SelectNext, window, cx);
6431 }
6432 });
6433 cx.run_until_parked();
6434 outline_panel.update(cx, |outline_panel, cx| {
6435 assert_eq!(
6436 display_entries(
6437 &project,
6438 &snapshot(&outline_panel, cx),
6439 &outline_panel.cached_entries,
6440 outline_panel.selected_entry(),
6441 cx,
6442 ),
6443 r#"/frontend-project/
6444 public/lottie/
6445 syntax-tree.json
6446 search: { "something": "static" }
6447 src/
6448 app/(site)/
6449 components/
6450 ErrorBoundary.tsx
6451 search: static <==== selected"#
6452 );
6453 });
6454
6455 outline_panel.update_in(cx, |outline_panel, window, cx| {
6456 outline_panel
6457 .active_editor()
6458 .expect("Should have an active editor")
6459 .update(cx, |editor, cx| {
6460 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6461 });
6462 });
6463 cx.executor()
6464 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6465 cx.run_until_parked();
6466 outline_panel.update(cx, |outline_panel, cx| {
6467 assert_eq!(
6468 display_entries(
6469 &project,
6470 &snapshot(&outline_panel, cx),
6471 &outline_panel.cached_entries,
6472 outline_panel.selected_entry(),
6473 cx,
6474 ),
6475 r#"/frontend-project/
6476 public/lottie/
6477 syntax-tree.json
6478 search: { "something": "static" }
6479 src/
6480 app/(site)/
6481 components/
6482 ErrorBoundary.tsx <==== selected"#
6483 );
6484 });
6485
6486 outline_panel.update_in(cx, |outline_panel, window, cx| {
6487 outline_panel
6488 .active_editor()
6489 .expect("Should have an active editor")
6490 .update(cx, |editor, cx| {
6491 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6492 });
6493 });
6494 cx.executor()
6495 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6496 cx.run_until_parked();
6497 outline_panel.update(cx, |outline_panel, cx| {
6498 assert_eq!(
6499 display_entries(
6500 &project,
6501 &snapshot(&outline_panel, cx),
6502 &outline_panel.cached_entries,
6503 outline_panel.selected_entry(),
6504 cx,
6505 ),
6506 r#"/frontend-project/
6507 public/lottie/
6508 syntax-tree.json
6509 search: { "something": "static" }
6510 src/
6511 app/(site)/
6512 components/
6513 ErrorBoundary.tsx <==== selected
6514 search: static"#
6515 );
6516 });
6517 }
6518
6519 async fn add_outline_panel(
6520 project: &Entity<Project>,
6521 cx: &mut TestAppContext,
6522 ) -> WindowHandle<Workspace> {
6523 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6524
6525 let outline_panel = window
6526 .update(cx, |_, window, cx| {
6527 cx.spawn_in(window, async |this, cx| {
6528 OutlinePanel::load(this, cx.clone()).await
6529 })
6530 })
6531 .unwrap()
6532 .await
6533 .expect("Failed to load outline panel");
6534
6535 window
6536 .update(cx, |workspace, window, cx| {
6537 workspace.add_panel(outline_panel, window, cx);
6538 })
6539 .unwrap();
6540 window
6541 }
6542
6543 fn outline_panel(
6544 workspace: &WindowHandle<Workspace>,
6545 cx: &mut TestAppContext,
6546 ) -> Entity<OutlinePanel> {
6547 workspace
6548 .update(cx, |workspace, _, cx| {
6549 workspace
6550 .panel::<OutlinePanel>(cx)
6551 .expect("no outline panel")
6552 })
6553 .unwrap()
6554 }
6555
6556 fn display_entries(
6557 project: &Entity<Project>,
6558 multi_buffer_snapshot: &MultiBufferSnapshot,
6559 cached_entries: &[CachedEntry],
6560 selected_entry: Option<&PanelEntry>,
6561 cx: &mut App,
6562 ) -> String {
6563 let mut display_string = String::new();
6564 for entry in cached_entries {
6565 if !display_string.is_empty() {
6566 display_string += "\n";
6567 }
6568 for _ in 0..entry.depth {
6569 display_string += " ";
6570 }
6571 display_string += &match &entry.entry {
6572 PanelEntry::Fs(entry) => match entry {
6573 FsEntry::ExternalFile(_) => {
6574 panic!("Did not cover external files with tests")
6575 }
6576 FsEntry::Directory(directory) => {
6577 match project
6578 .read(cx)
6579 .worktree_for_id(directory.worktree_id, cx)
6580 .and_then(|worktree| {
6581 if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
6582 Some(worktree.read(cx).abs_path())
6583 } else {
6584 None
6585 }
6586 }) {
6587 Some(root_path) => format!(
6588 "{}/{}",
6589 root_path.display(),
6590 directory.entry.path.display(),
6591 ),
6592 None => format!(
6593 "{}/",
6594 directory
6595 .entry
6596 .path
6597 .file_name()
6598 .unwrap_or_default()
6599 .to_string_lossy()
6600 ),
6601 }
6602 }
6603 FsEntry::File(file) => file
6604 .entry
6605 .path
6606 .file_name()
6607 .map(|name| name.to_string_lossy().to_string())
6608 .unwrap_or_default(),
6609 },
6610 PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6611 .entries
6612 .iter()
6613 .filter_map(|dir| dir.path.file_name())
6614 .map(|name| name.to_string_lossy().to_string() + "/")
6615 .collect(),
6616 PanelEntry::Outline(outline_entry) => match outline_entry {
6617 OutlineEntry::Excerpt(_) => continue,
6618 OutlineEntry::Outline(outline_entry) => {
6619 format!("outline: {}", outline_entry.outline.text)
6620 }
6621 },
6622 PanelEntry::Search(search_entry) => {
6623 format!(
6624 "search: {}",
6625 search_entry
6626 .render_data
6627 .get_or_init(|| SearchData::new(
6628 &search_entry.match_range,
6629 &multi_buffer_snapshot
6630 ))
6631 .context_text
6632 )
6633 }
6634 };
6635
6636 if Some(&entry.entry) == selected_entry {
6637 display_string += SELECTED_MARKER;
6638 }
6639 }
6640 display_string
6641 }
6642
6643 fn init_test(cx: &mut TestAppContext) {
6644 cx.update(|cx| {
6645 let settings = SettingsStore::test(cx);
6646 cx.set_global(settings);
6647
6648 theme::init(theme::LoadThemes::JustBase, cx);
6649
6650 language::init(cx);
6651 editor::init(cx);
6652 workspace::init_settings(cx);
6653 Project::init_settings(cx);
6654 project_search::init(cx);
6655 super::init(cx);
6656 });
6657 }
6658
6659 // Based on https://github.com/rust-lang/rust-analyzer/
6660 async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6661 fs.insert_tree(
6662 root,
6663 json!({
6664 "crates": {
6665 "ide": {
6666 "src": {
6667 "inlay_hints": {
6668 "fn_lifetime_fn.rs": r##"
6669 pub(super) fn hints(
6670 acc: &mut Vec<InlayHint>,
6671 config: &InlayHintsConfig,
6672 func: ast::Fn,
6673 ) -> Option<()> {
6674 // ... snip
6675
6676 let mut used_names: FxHashMap<SmolStr, usize> =
6677 match config.param_names_for_lifetime_elision_hints {
6678 true => generic_param_list
6679 .iter()
6680 .flat_map(|gpl| gpl.lifetime_params())
6681 .filter_map(|param| param.lifetime())
6682 .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6683 .collect(),
6684 false => Default::default(),
6685 };
6686 {
6687 let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6688 if self_param.is_some() && potential_lt_refs.next().is_some() {
6689 allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6690 // self can't be used as a lifetime, so no need to check for collisions
6691 "'self".into()
6692 } else {
6693 gen_idx_name()
6694 });
6695 }
6696 potential_lt_refs.for_each(|(name, ..)| {
6697 let name = match name {
6698 Some(it) if config.param_names_for_lifetime_elision_hints => {
6699 if let Some(c) = used_names.get_mut(it.text().as_str()) {
6700 *c += 1;
6701 SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6702 } else {
6703 used_names.insert(it.text().as_str().into(), 0);
6704 SmolStr::from_iter(["\'", it.text().as_str()])
6705 }
6706 }
6707 _ => gen_idx_name(),
6708 };
6709 allocated_lifetimes.push(name);
6710 });
6711 }
6712
6713 // ... snip
6714 }
6715
6716 // ... snip
6717
6718 #[test]
6719 fn hints_lifetimes_named() {
6720 check_with_config(
6721 InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6722 r#"
6723 fn nested_in<'named>(named: & &X< &()>) {}
6724 // ^'named1, 'named2, 'named3, $
6725 //^'named1 ^'named2 ^'named3
6726 "#,
6727 );
6728 }
6729
6730 // ... snip
6731 "##,
6732 },
6733 "inlay_hints.rs": r#"
6734 #[derive(Clone, Debug, PartialEq, Eq)]
6735 pub struct InlayHintsConfig {
6736 // ... snip
6737 pub param_names_for_lifetime_elision_hints: bool,
6738 pub max_length: Option<usize>,
6739 // ... snip
6740 }
6741
6742 impl Config {
6743 pub fn inlay_hints(&self) -> InlayHintsConfig {
6744 InlayHintsConfig {
6745 // ... snip
6746 param_names_for_lifetime_elision_hints: self
6747 .inlayHints_lifetimeElisionHints_useParameterNames()
6748 .to_owned(),
6749 max_length: self.inlayHints_maxLength().to_owned(),
6750 // ... snip
6751 }
6752 }
6753 }
6754 "#,
6755 "static_index.rs": r#"
6756// ... snip
6757 fn add_file(&mut self, file_id: FileId) {
6758 let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6759 let folds = self.analysis.folding_ranges(file_id).unwrap();
6760 let inlay_hints = self
6761 .analysis
6762 .inlay_hints(
6763 &InlayHintsConfig {
6764 // ... snip
6765 closure_style: hir::ClosureStyle::ImplFn,
6766 param_names_for_lifetime_elision_hints: false,
6767 binding_mode_hints: false,
6768 max_length: Some(25),
6769 closure_capture_hints: false,
6770 // ... snip
6771 },
6772 file_id,
6773 None,
6774 )
6775 .unwrap();
6776 // ... snip
6777 }
6778// ... snip
6779 "#
6780 }
6781 },
6782 "rust-analyzer": {
6783 "src": {
6784 "cli": {
6785 "analysis_stats.rs": r#"
6786 // ... snip
6787 for &file_id in &file_ids {
6788 _ = analysis.inlay_hints(
6789 &InlayHintsConfig {
6790 // ... snip
6791 implicit_drop_hints: true,
6792 lifetime_elision_hints: ide::LifetimeElisionHints::Always,
6793 param_names_for_lifetime_elision_hints: true,
6794 hide_named_constructor_hints: false,
6795 hide_closure_initialization_hints: false,
6796 closure_style: hir::ClosureStyle::ImplFn,
6797 max_length: Some(25),
6798 closing_brace_hints_min_lines: Some(20),
6799 fields_to_resolve: InlayFieldsToResolve::empty(),
6800 range_exclusive_hints: true,
6801 },
6802 file_id.into(),
6803 None,
6804 );
6805 }
6806 // ... snip
6807 "#,
6808 },
6809 "config.rs": r#"
6810 config_data! {
6811 /// Configs that only make sense when they are set by a client. As such they can only be defined
6812 /// by setting them using client's settings (e.g `settings.json` on VS Code).
6813 client: struct ClientDefaultConfigData <- ClientConfigInput -> {
6814 // ... snip
6815 /// Maximum length for inlay hints. Set to null to have an unlimited length.
6816 inlayHints_maxLength: Option<usize> = Some(25),
6817 // ... snip
6818 /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
6819 inlayHints_lifetimeElisionHints_useParameterNames: bool = false,
6820 // ... snip
6821 }
6822 }
6823
6824 impl Config {
6825 // ... snip
6826 pub fn inlay_hints(&self) -> InlayHintsConfig {
6827 InlayHintsConfig {
6828 // ... snip
6829 param_names_for_lifetime_elision_hints: self
6830 .inlayHints_lifetimeElisionHints_useParameterNames()
6831 .to_owned(),
6832 max_length: self.inlayHints_maxLength().to_owned(),
6833 // ... snip
6834 }
6835 }
6836 // ... snip
6837 }
6838 "#
6839 }
6840 }
6841 }
6842 }),
6843 )
6844 .await;
6845 }
6846
6847 fn rust_lang() -> Language {
6848 Language::new(
6849 LanguageConfig {
6850 name: "Rust".into(),
6851 matcher: LanguageMatcher {
6852 path_suffixes: vec!["rs".to_string()],
6853 ..Default::default()
6854 },
6855 ..Default::default()
6856 },
6857 Some(tree_sitter_rust::LANGUAGE.into()),
6858 )
6859 .with_highlights_query(
6860 r#"
6861 (field_identifier) @field
6862 (struct_expression) @struct
6863 "#,
6864 )
6865 .unwrap()
6866 .with_injection_query(
6867 r#"
6868 (macro_invocation
6869 (token_tree) @injection.content
6870 (#set! injection.language "rust"))
6871 "#,
6872 )
6873 .unwrap()
6874 }
6875
6876 fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
6877 outline_panel
6878 .active_editor()
6879 .unwrap()
6880 .read(cx)
6881 .buffer()
6882 .read(cx)
6883 .snapshot(cx)
6884 }
6885
6886 fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
6887 editor.update(cx, |editor, cx| {
6888 let selections = editor.selections.all::<language::Point>(cx);
6889 assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
6890 let selection = selections.first().unwrap();
6891 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
6892 let line_start = language::Point::new(selection.start.row, 0);
6893 let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
6894 multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
6895 })
6896 }
6897}