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