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