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(cx.entity().clone(), "entries", items_len, {
4501 move |outline_panel, range, window, cx| {
4502 let entries = outline_panel.cached_entries.get(range);
4503 entries
4504 .map(|entries| entries.to_vec())
4505 .unwrap_or_default()
4506 .into_iter()
4507 .filter_map(|cached_entry| match cached_entry.entry {
4508 PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4509 &entry,
4510 cached_entry.depth,
4511 cached_entry.string_match.as_ref(),
4512 window,
4513 cx,
4514 )),
4515 PanelEntry::FoldedDirs(folded_dirs_entry) => {
4516 Some(outline_panel.render_folded_dirs(
4517 &folded_dirs_entry,
4518 cached_entry.depth,
4519 cached_entry.string_match.as_ref(),
4520 window,
4521 cx,
4522 ))
4523 }
4524 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4525 outline_panel.render_excerpt(
4526 &excerpt,
4527 cached_entry.depth,
4528 window,
4529 cx,
4530 )
4531 }
4532 PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4533 Some(outline_panel.render_outline(
4534 &entry,
4535 cached_entry.depth,
4536 cached_entry.string_match.as_ref(),
4537 window,
4538 cx,
4539 ))
4540 }
4541 PanelEntry::Search(SearchEntry {
4542 match_range,
4543 render_data,
4544 kind,
4545 ..
4546 }) => outline_panel.render_search_match(
4547 multi_buffer_snapshot.as_ref(),
4548 &match_range,
4549 &render_data,
4550 kind,
4551 cached_entry.depth,
4552 cached_entry.string_match.as_ref(),
4553 window,
4554 cx,
4555 ),
4556 })
4557 .collect()
4558 }
4559 })
4560 .with_sizing_behavior(ListSizingBehavior::Infer)
4561 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4562 .with_width_from_item(self.max_width_item_index)
4563 .track_scroll(self.scroll_handle.clone())
4564 .when(show_indent_guides, |list| {
4565 list.with_decoration(
4566 ui::indent_guides(
4567 cx.entity().clone(),
4568 px(indent_size),
4569 IndentGuideColors::panel(cx),
4570 |outline_panel, range, _, _| {
4571 let entries = outline_panel.cached_entries.get(range);
4572 if let Some(entries) = entries {
4573 entries.into_iter().map(|item| item.depth).collect()
4574 } else {
4575 smallvec::SmallVec::new()
4576 }
4577 },
4578 )
4579 .with_render_fn(
4580 cx.entity().clone(),
4581 move |outline_panel, params, _, _| {
4582 const LEFT_OFFSET: Pixels = px(14.);
4583
4584 let indent_size = params.indent_size;
4585 let item_height = params.item_height;
4586 let active_indent_guide_ix = find_active_indent_guide_ix(
4587 outline_panel,
4588 ¶ms.indent_guides,
4589 );
4590
4591 params
4592 .indent_guides
4593 .into_iter()
4594 .enumerate()
4595 .map(|(ix, layout)| {
4596 let bounds = Bounds::new(
4597 point(
4598 layout.offset.x * indent_size + LEFT_OFFSET,
4599 layout.offset.y * item_height,
4600 ),
4601 size(px(1.), layout.length * item_height),
4602 );
4603 ui::RenderedIndentGuide {
4604 bounds,
4605 layout,
4606 is_active: active_indent_guide_ix == Some(ix),
4607 hitbox: None,
4608 }
4609 })
4610 .collect()
4611 },
4612 ),
4613 )
4614 })
4615 };
4616
4617 v_flex()
4618 .flex_shrink()
4619 .size_full()
4620 .child(list_contents.size_full().flex_shrink())
4621 .children(self.render_vertical_scrollbar(cx))
4622 .when_some(
4623 self.render_horizontal_scrollbar(window, cx),
4624 |this, scrollbar| this.pb_4().child(scrollbar),
4625 )
4626 }
4627 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4628 deferred(
4629 anchored()
4630 .position(*position)
4631 .anchor(gpui::Corner::TopLeft)
4632 .child(menu.clone()),
4633 )
4634 .with_priority(1)
4635 }));
4636
4637 v_flex().w_full().flex_1().overflow_hidden().child(contents)
4638 }
4639
4640 fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4641 v_flex().flex_none().child(horizontal_separator(cx)).child(
4642 h_flex()
4643 .p_2()
4644 .w_full()
4645 .child(self.filter_editor.clone())
4646 .child(
4647 div().child(
4648 IconButton::new(
4649 "outline-panel-menu",
4650 if pinned {
4651 IconName::Unpin
4652 } else {
4653 IconName::Pin
4654 },
4655 )
4656 .tooltip(Tooltip::text(if pinned {
4657 "Unpin Outline"
4658 } else {
4659 "Pin Active Outline"
4660 }))
4661 .shape(IconButtonShape::Square)
4662 .on_click(cx.listener(
4663 |outline_panel, _, window, cx| {
4664 outline_panel.toggle_active_editor_pin(
4665 &ToggleActiveEditorPin,
4666 window,
4667 cx,
4668 );
4669 },
4670 )),
4671 ),
4672 ),
4673 )
4674 }
4675
4676 fn buffers_inside_directory(
4677 &self,
4678 dir_worktree: WorktreeId,
4679 dir_entry: &GitEntry,
4680 ) -> HashSet<BufferId> {
4681 if !dir_entry.is_dir() {
4682 debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4683 return HashSet::default();
4684 }
4685
4686 self.fs_entries
4687 .iter()
4688 .skip_while(|fs_entry| match fs_entry {
4689 FsEntry::Directory(directory) => {
4690 directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4691 }
4692 _ => true,
4693 })
4694 .skip(1)
4695 .take_while(|fs_entry| match fs_entry {
4696 FsEntry::ExternalFile(..) => false,
4697 FsEntry::Directory(directory) => {
4698 directory.worktree_id == dir_worktree
4699 && directory.entry.path.starts_with(&dir_entry.path)
4700 }
4701 FsEntry::File(file) => {
4702 file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4703 }
4704 })
4705 .filter_map(|fs_entry| match fs_entry {
4706 FsEntry::File(file) => Some(file.buffer_id),
4707 _ => None,
4708 })
4709 .collect()
4710 }
4711}
4712
4713fn workspace_active_editor(
4714 workspace: &Workspace,
4715 cx: &App,
4716) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4717 let active_item = workspace.active_item(cx)?;
4718 let active_editor = active_item
4719 .act_as::<Editor>(cx)
4720 .filter(|editor| editor.read(cx).mode().is_full())?;
4721 Some((active_item, active_editor))
4722}
4723
4724fn back_to_common_visited_parent(
4725 visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
4726 worktree_id: &WorktreeId,
4727 new_entry: &Entry,
4728) -> Option<(WorktreeId, ProjectEntryId)> {
4729 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4730 match new_entry.path.parent() {
4731 Some(parent_path) => {
4732 if parent_path == visited_path.as_ref() {
4733 return Some((*worktree_id, *visited_dir_id));
4734 }
4735 }
4736 None => {
4737 break;
4738 }
4739 }
4740 visited_dirs.pop();
4741 }
4742 None
4743}
4744
4745fn file_name(path: &Path) -> String {
4746 let mut current_path = path;
4747 loop {
4748 if let Some(file_name) = current_path.file_name() {
4749 return file_name.to_string_lossy().into_owned();
4750 }
4751 match current_path.parent() {
4752 Some(parent) => current_path = parent,
4753 None => return path.to_string_lossy().into_owned(),
4754 }
4755 }
4756}
4757
4758impl Panel for OutlinePanel {
4759 fn persistent_name() -> &'static str {
4760 "Outline Panel"
4761 }
4762
4763 fn position(&self, _: &Window, cx: &App) -> DockPosition {
4764 match OutlinePanelSettings::get_global(cx).dock {
4765 OutlinePanelDockPosition::Left => DockPosition::Left,
4766 OutlinePanelDockPosition::Right => DockPosition::Right,
4767 }
4768 }
4769
4770 fn position_is_valid(&self, position: DockPosition) -> bool {
4771 matches!(position, DockPosition::Left | DockPosition::Right)
4772 }
4773
4774 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4775 settings::update_settings_file::<OutlinePanelSettings>(
4776 self.fs.clone(),
4777 cx,
4778 move |settings, _| {
4779 let dock = match position {
4780 DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
4781 DockPosition::Right => OutlinePanelDockPosition::Right,
4782 };
4783 settings.dock = Some(dock);
4784 },
4785 );
4786 }
4787
4788 fn size(&self, _: &Window, cx: &App) -> Pixels {
4789 self.width
4790 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
4791 }
4792
4793 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
4794 self.width = size;
4795 cx.notify();
4796 cx.defer_in(window, |this, _, cx| {
4797 this.serialize(cx);
4798 });
4799 }
4800
4801 fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4802 OutlinePanelSettings::get_global(cx)
4803 .button
4804 .then_some(IconName::ListTree)
4805 }
4806
4807 fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
4808 Some("Outline Panel")
4809 }
4810
4811 fn toggle_action(&self) -> Box<dyn Action> {
4812 Box::new(ToggleFocus)
4813 }
4814
4815 fn starts_open(&self, _window: &Window, _: &App) -> bool {
4816 self.active
4817 }
4818
4819 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
4820 cx.spawn_in(window, async move |outline_panel, cx| {
4821 outline_panel
4822 .update_in(cx, |outline_panel, window, cx| {
4823 let old_active = outline_panel.active;
4824 outline_panel.active = active;
4825 if old_active != active {
4826 if active {
4827 if let Some((active_item, active_editor)) =
4828 outline_panel.workspace.upgrade().and_then(|workspace| {
4829 workspace_active_editor(workspace.read(cx), cx)
4830 })
4831 {
4832 if outline_panel.should_replace_active_item(active_item.as_ref()) {
4833 outline_panel.replace_active_editor(
4834 active_item,
4835 active_editor,
4836 window,
4837 cx,
4838 );
4839 } else {
4840 outline_panel.update_fs_entries(active_editor, None, window, cx)
4841 }
4842 return;
4843 }
4844 }
4845
4846 if !outline_panel.pinned {
4847 outline_panel.clear_previous(window, cx);
4848 }
4849 }
4850 outline_panel.serialize(cx);
4851 })
4852 .ok();
4853 })
4854 .detach()
4855 }
4856
4857 fn activation_priority(&self) -> u32 {
4858 5
4859 }
4860}
4861
4862impl Focusable for OutlinePanel {
4863 fn focus_handle(&self, cx: &App) -> FocusHandle {
4864 self.filter_editor.focus_handle(cx).clone()
4865 }
4866}
4867
4868impl EventEmitter<Event> for OutlinePanel {}
4869
4870impl EventEmitter<PanelEvent> for OutlinePanel {}
4871
4872impl Render for OutlinePanel {
4873 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4874 let (is_local, is_via_ssh) = self
4875 .project
4876 .read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
4877 let query = self.query(cx);
4878 let pinned = self.pinned;
4879 let settings = OutlinePanelSettings::get_global(cx);
4880 let indent_size = settings.indent_size;
4881 let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
4882
4883 let search_query = match &self.mode {
4884 ItemsDisplayMode::Search(search_query) => Some(search_query),
4885 _ => None,
4886 };
4887
4888 v_flex()
4889 .id("outline-panel")
4890 .size_full()
4891 .overflow_hidden()
4892 .relative()
4893 .on_hover(cx.listener(|this, hovered, window, cx| {
4894 if *hovered {
4895 this.show_scrollbar = true;
4896 this.hide_scrollbar_task.take();
4897 cx.notify();
4898 } else if !this.focus_handle.contains_focused(window, cx) {
4899 this.hide_scrollbar(window, cx);
4900 }
4901 }))
4902 .key_context(self.dispatch_context(window, cx))
4903 .on_action(cx.listener(Self::open_selected_entry))
4904 .on_action(cx.listener(Self::cancel))
4905 .on_action(cx.listener(Self::select_next))
4906 .on_action(cx.listener(Self::select_previous))
4907 .on_action(cx.listener(Self::select_first))
4908 .on_action(cx.listener(Self::select_last))
4909 .on_action(cx.listener(Self::select_parent))
4910 .on_action(cx.listener(Self::expand_selected_entry))
4911 .on_action(cx.listener(Self::collapse_selected_entry))
4912 .on_action(cx.listener(Self::expand_all_entries))
4913 .on_action(cx.listener(Self::collapse_all_entries))
4914 .on_action(cx.listener(Self::copy_path))
4915 .on_action(cx.listener(Self::copy_relative_path))
4916 .on_action(cx.listener(Self::toggle_active_editor_pin))
4917 .on_action(cx.listener(Self::unfold_directory))
4918 .on_action(cx.listener(Self::fold_directory))
4919 .on_action(cx.listener(Self::open_excerpts))
4920 .on_action(cx.listener(Self::open_excerpts_split))
4921 .when(is_local, |el| {
4922 el.on_action(cx.listener(Self::reveal_in_finder))
4923 })
4924 .when(is_local || is_via_ssh, |el| {
4925 el.on_action(cx.listener(Self::open_in_terminal))
4926 })
4927 .on_mouse_down(
4928 MouseButton::Right,
4929 cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
4930 if let Some(entry) = outline_panel.selected_entry().cloned() {
4931 outline_panel.deploy_context_menu(event.position, entry, window, cx)
4932 } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
4933 outline_panel.deploy_context_menu(
4934 event.position,
4935 PanelEntry::Fs(entry),
4936 window,
4937 cx,
4938 )
4939 }
4940 }),
4941 )
4942 .track_focus(&self.focus_handle)
4943 .when_some(search_query, |outline_panel, search_state| {
4944 outline_panel.child(
4945 h_flex()
4946 .py_1p5()
4947 .px_2()
4948 .h(DynamicSpacing::Base32.px(cx))
4949 .flex_shrink_0()
4950 .border_b_1()
4951 .border_color(cx.theme().colors().border)
4952 .gap_0p5()
4953 .child(Label::new("Searching:").color(Color::Muted))
4954 .child(Label::new(search_state.query.to_string())),
4955 )
4956 })
4957 .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
4958 .child(self.render_filter_footer(pinned, cx))
4959 }
4960}
4961
4962fn find_active_indent_guide_ix(
4963 outline_panel: &OutlinePanel,
4964 candidates: &[IndentGuideLayout],
4965) -> Option<usize> {
4966 let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
4967 return None;
4968 };
4969 let target_depth = outline_panel
4970 .cached_entries
4971 .get(*target_ix)
4972 .map(|cached_entry| cached_entry.depth)?;
4973
4974 let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
4975 .cached_entries
4976 .get(target_ix + 1)
4977 .filter(|cached_entry| cached_entry.depth > target_depth)
4978 .map(|entry| entry.depth)
4979 {
4980 (target_ix + 1, target_depth.saturating_sub(1))
4981 } else {
4982 (*target_ix, target_depth.saturating_sub(1))
4983 };
4984
4985 candidates
4986 .iter()
4987 .enumerate()
4988 .find(|(_, guide)| {
4989 guide.offset.y <= target_ix
4990 && target_ix < guide.offset.y + guide.length
4991 && guide.offset.x == target_depth
4992 })
4993 .map(|(ix, _)| ix)
4994}
4995
4996fn subscribe_for_editor_events(
4997 editor: &Entity<Editor>,
4998 window: &mut Window,
4999 cx: &mut Context<OutlinePanel>,
5000) -> Subscription {
5001 let debounce = Some(UPDATE_DEBOUNCE);
5002 cx.subscribe_in(
5003 editor,
5004 window,
5005 move |outline_panel, editor, e: &EditorEvent, window, cx| {
5006 if !outline_panel.active {
5007 return;
5008 }
5009 match e {
5010 EditorEvent::SelectionsChanged { local: true } => {
5011 outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5012 cx.notify();
5013 }
5014 EditorEvent::ExcerptsAdded { excerpts, .. } => {
5015 outline_panel
5016 .new_entries_for_fs_update
5017 .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
5018 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5019 }
5020 EditorEvent::ExcerptsRemoved { ids, .. } => {
5021 let mut ids = ids.iter().collect::<HashSet<_>>();
5022 for excerpts in outline_panel.excerpts.values_mut() {
5023 excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
5024 if ids.is_empty() {
5025 break;
5026 }
5027 }
5028 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5029 }
5030 EditorEvent::ExcerptsExpanded { ids } => {
5031 outline_panel.invalidate_outlines(ids);
5032 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5033 if update_cached_items {
5034 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5035 }
5036 }
5037 EditorEvent::ExcerptsEdited { ids } => {
5038 outline_panel.invalidate_outlines(ids);
5039 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5040 if update_cached_items {
5041 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5042 }
5043 }
5044 EditorEvent::BufferFoldToggled { ids, .. } => {
5045 outline_panel.invalidate_outlines(ids);
5046 let mut latest_unfolded_buffer_id = None;
5047 let mut latest_folded_buffer_id = None;
5048 let mut ignore_selections_change = false;
5049 outline_panel.new_entries_for_fs_update.extend(
5050 ids.iter()
5051 .filter(|id| {
5052 outline_panel
5053 .excerpts
5054 .iter()
5055 .find_map(|(buffer_id, excerpts)| {
5056 if excerpts.contains_key(id) {
5057 ignore_selections_change |= outline_panel
5058 .preserve_selection_on_buffer_fold_toggles
5059 .remove(buffer_id);
5060 Some(buffer_id)
5061 } else {
5062 None
5063 }
5064 })
5065 .map(|buffer_id| {
5066 if editor.read(cx).is_buffer_folded(*buffer_id, cx) {
5067 latest_folded_buffer_id = Some(*buffer_id);
5068 false
5069 } else {
5070 latest_unfolded_buffer_id = Some(*buffer_id);
5071 true
5072 }
5073 })
5074 .unwrap_or(true)
5075 })
5076 .copied(),
5077 );
5078 if !ignore_selections_change {
5079 if let Some(entry_to_select) = latest_unfolded_buffer_id
5080 .or(latest_folded_buffer_id)
5081 .and_then(|toggled_buffer_id| {
5082 outline_panel.fs_entries.iter().find_map(
5083 |fs_entry| match fs_entry {
5084 FsEntry::ExternalFile(external) => {
5085 if external.buffer_id == toggled_buffer_id {
5086 Some(fs_entry.clone())
5087 } else {
5088 None
5089 }
5090 }
5091 FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5092 if *buffer_id == toggled_buffer_id {
5093 Some(fs_entry.clone())
5094 } else {
5095 None
5096 }
5097 }
5098 FsEntry::Directory(..) => None,
5099 },
5100 )
5101 })
5102 .map(PanelEntry::Fs)
5103 {
5104 outline_panel.select_entry(entry_to_select, true, window, cx);
5105 }
5106 }
5107
5108 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5109 }
5110 EditorEvent::Reparsed(buffer_id) => {
5111 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
5112 for (_, excerpt) in excerpts {
5113 excerpt.invalidate_outlines();
5114 }
5115 }
5116 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5117 if update_cached_items {
5118 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5119 }
5120 }
5121 _ => {}
5122 }
5123 },
5124 )
5125}
5126
5127fn empty_icon() -> AnyElement {
5128 h_flex()
5129 .size(IconSize::default().rems())
5130 .invisible()
5131 .flex_none()
5132 .into_any_element()
5133}
5134
5135fn horizontal_separator(cx: &mut App) -> Div {
5136 div().mx_2().border_primary(cx).border_t_1()
5137}
5138
5139#[derive(Debug, Default)]
5140struct GenerationState {
5141 entries: Vec<CachedEntry>,
5142 match_candidates: Vec<StringMatchCandidate>,
5143 max_width_estimate_and_index: Option<(u64, usize)>,
5144}
5145
5146impl GenerationState {
5147 fn clear(&mut self) {
5148 self.entries.clear();
5149 self.match_candidates.clear();
5150 self.max_width_estimate_and_index = None;
5151 }
5152}
5153
5154#[cfg(test)]
5155mod tests {
5156 use db::indoc;
5157 use gpui::{TestAppContext, VisualTestContext, WindowHandle};
5158 use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
5159 use pretty_assertions::assert_eq;
5160 use project::FakeFs;
5161 use search::project_search::{self, perform_project_search};
5162 use serde_json::json;
5163 use util::path;
5164 use workspace::{OpenOptions, OpenVisible};
5165
5166 use super::*;
5167
5168 const SELECTED_MARKER: &str = " <==== selected";
5169
5170 #[gpui::test(iterations = 10)]
5171 async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5172 init_test(cx);
5173
5174 let fs = FakeFs::new(cx.background_executor.clone());
5175 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5176 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5177 project.read_with(cx, |project, _| {
5178 project.languages().add(Arc::new(rust_lang()))
5179 });
5180 let workspace = add_outline_panel(&project, cx).await;
5181 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5182 let outline_panel = outline_panel(&workspace, cx);
5183 outline_panel.update_in(cx, |outline_panel, window, cx| {
5184 outline_panel.set_active(true, window, cx)
5185 });
5186
5187 workspace
5188 .update(cx, |workspace, window, cx| {
5189 ProjectSearchView::deploy_search(
5190 workspace,
5191 &workspace::DeploySearch::default(),
5192 window,
5193 cx,
5194 )
5195 })
5196 .unwrap();
5197 let search_view = workspace
5198 .update(cx, |workspace, _, cx| {
5199 workspace
5200 .active_pane()
5201 .read(cx)
5202 .items()
5203 .find_map(|item| item.downcast::<ProjectSearchView>())
5204 .expect("Project search view expected to appear after new search event trigger")
5205 })
5206 .unwrap();
5207
5208 let query = "param_names_for_lifetime_elision_hints";
5209 perform_project_search(&search_view, query, cx);
5210 search_view.update(cx, |search_view, cx| {
5211 search_view
5212 .results_editor()
5213 .update(cx, |results_editor, cx| {
5214 assert_eq!(
5215 results_editor.display_text(cx).match_indices(query).count(),
5216 9
5217 );
5218 });
5219 });
5220
5221 let all_matches = r#"/rust-analyzer/
5222 crates/
5223 ide/src/
5224 inlay_hints/
5225 fn_lifetime_fn.rs
5226 search: match config.param_names_for_lifetime_elision_hints {
5227 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5228 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5229 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5230 inlay_hints.rs
5231 search: pub param_names_for_lifetime_elision_hints: bool,
5232 search: param_names_for_lifetime_elision_hints: self
5233 static_index.rs
5234 search: param_names_for_lifetime_elision_hints: false,
5235 rust-analyzer/src/
5236 cli/
5237 analysis_stats.rs
5238 search: param_names_for_lifetime_elision_hints: true,
5239 config.rs
5240 search: param_names_for_lifetime_elision_hints: self"#;
5241 let select_first_in_all_matches = |line_to_select: &str| {
5242 assert!(all_matches.contains(line_to_select));
5243 all_matches.replacen(
5244 line_to_select,
5245 &format!("{line_to_select}{SELECTED_MARKER}"),
5246 1,
5247 )
5248 };
5249
5250 cx.executor()
5251 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5252 cx.run_until_parked();
5253 outline_panel.update(cx, |outline_panel, cx| {
5254 assert_eq!(
5255 display_entries(
5256 &project,
5257 &snapshot(&outline_panel, cx),
5258 &outline_panel.cached_entries,
5259 outline_panel.selected_entry(),
5260 cx,
5261 ),
5262 select_first_in_all_matches(
5263 "search: match config.param_names_for_lifetime_elision_hints {"
5264 )
5265 );
5266 });
5267
5268 outline_panel.update_in(cx, |outline_panel, window, cx| {
5269 outline_panel.select_parent(&SelectParent, window, cx);
5270 assert_eq!(
5271 display_entries(
5272 &project,
5273 &snapshot(&outline_panel, cx),
5274 &outline_panel.cached_entries,
5275 outline_panel.selected_entry(),
5276 cx,
5277 ),
5278 select_first_in_all_matches("fn_lifetime_fn.rs")
5279 );
5280 });
5281 outline_panel.update_in(cx, |outline_panel, window, cx| {
5282 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5283 });
5284 cx.executor()
5285 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5286 cx.run_until_parked();
5287 outline_panel.update(cx, |outline_panel, cx| {
5288 assert_eq!(
5289 display_entries(
5290 &project,
5291 &snapshot(&outline_panel, cx),
5292 &outline_panel.cached_entries,
5293 outline_panel.selected_entry(),
5294 cx,
5295 ),
5296 format!(
5297 r#"/rust-analyzer/
5298 crates/
5299 ide/src/
5300 inlay_hints/
5301 fn_lifetime_fn.rs{SELECTED_MARKER}
5302 inlay_hints.rs
5303 search: pub param_names_for_lifetime_elision_hints: bool,
5304 search: param_names_for_lifetime_elision_hints: self
5305 static_index.rs
5306 search: param_names_for_lifetime_elision_hints: false,
5307 rust-analyzer/src/
5308 cli/
5309 analysis_stats.rs
5310 search: param_names_for_lifetime_elision_hints: true,
5311 config.rs
5312 search: param_names_for_lifetime_elision_hints: self"#,
5313 )
5314 );
5315 });
5316
5317 outline_panel.update_in(cx, |outline_panel, window, cx| {
5318 outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5319 });
5320 cx.executor()
5321 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5322 cx.run_until_parked();
5323 outline_panel.update_in(cx, |outline_panel, window, cx| {
5324 outline_panel.select_parent(&SelectParent, window, cx);
5325 assert_eq!(
5326 display_entries(
5327 &project,
5328 &snapshot(&outline_panel, cx),
5329 &outline_panel.cached_entries,
5330 outline_panel.selected_entry(),
5331 cx,
5332 ),
5333 select_first_in_all_matches("inlay_hints/")
5334 );
5335 });
5336
5337 outline_panel.update_in(cx, |outline_panel, window, cx| {
5338 outline_panel.select_parent(&SelectParent, window, cx);
5339 assert_eq!(
5340 display_entries(
5341 &project,
5342 &snapshot(&outline_panel, cx),
5343 &outline_panel.cached_entries,
5344 outline_panel.selected_entry(),
5345 cx,
5346 ),
5347 select_first_in_all_matches("ide/src/")
5348 );
5349 });
5350
5351 outline_panel.update_in(cx, |outline_panel, window, cx| {
5352 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5353 });
5354 cx.executor()
5355 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5356 cx.run_until_parked();
5357 outline_panel.update(cx, |outline_panel, cx| {
5358 assert_eq!(
5359 display_entries(
5360 &project,
5361 &snapshot(&outline_panel, cx),
5362 &outline_panel.cached_entries,
5363 outline_panel.selected_entry(),
5364 cx,
5365 ),
5366 format!(
5367 r#"/rust-analyzer/
5368 crates/
5369 ide/src/{SELECTED_MARKER}
5370 rust-analyzer/src/
5371 cli/
5372 analysis_stats.rs
5373 search: param_names_for_lifetime_elision_hints: true,
5374 config.rs
5375 search: param_names_for_lifetime_elision_hints: self"#,
5376 )
5377 );
5378 });
5379 outline_panel.update_in(cx, |outline_panel, window, cx| {
5380 outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5381 });
5382 cx.executor()
5383 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5384 cx.run_until_parked();
5385 outline_panel.update(cx, |outline_panel, cx| {
5386 assert_eq!(
5387 display_entries(
5388 &project,
5389 &snapshot(&outline_panel, cx),
5390 &outline_panel.cached_entries,
5391 outline_panel.selected_entry(),
5392 cx,
5393 ),
5394 select_first_in_all_matches("ide/src/")
5395 );
5396 });
5397 }
5398
5399 #[gpui::test(iterations = 10)]
5400 async fn test_item_filtering(cx: &mut TestAppContext) {
5401 init_test(cx);
5402
5403 let fs = FakeFs::new(cx.background_executor.clone());
5404 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5405 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5406 project.read_with(cx, |project, _| {
5407 project.languages().add(Arc::new(rust_lang()))
5408 });
5409 let workspace = add_outline_panel(&project, cx).await;
5410 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5411 let outline_panel = outline_panel(&workspace, cx);
5412 outline_panel.update_in(cx, |outline_panel, window, cx| {
5413 outline_panel.set_active(true, window, cx)
5414 });
5415
5416 workspace
5417 .update(cx, |workspace, window, cx| {
5418 ProjectSearchView::deploy_search(
5419 workspace,
5420 &workspace::DeploySearch::default(),
5421 window,
5422 cx,
5423 )
5424 })
5425 .unwrap();
5426 let search_view = workspace
5427 .update(cx, |workspace, _, cx| {
5428 workspace
5429 .active_pane()
5430 .read(cx)
5431 .items()
5432 .find_map(|item| item.downcast::<ProjectSearchView>())
5433 .expect("Project search view expected to appear after new search event trigger")
5434 })
5435 .unwrap();
5436
5437 let query = "param_names_for_lifetime_elision_hints";
5438 perform_project_search(&search_view, query, cx);
5439 search_view.update(cx, |search_view, cx| {
5440 search_view
5441 .results_editor()
5442 .update(cx, |results_editor, cx| {
5443 assert_eq!(
5444 results_editor.display_text(cx).match_indices(query).count(),
5445 9
5446 );
5447 });
5448 });
5449 let all_matches = r#"/rust-analyzer/
5450 crates/
5451 ide/src/
5452 inlay_hints/
5453 fn_lifetime_fn.rs
5454 search: match config.param_names_for_lifetime_elision_hints {
5455 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5456 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5457 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5458 inlay_hints.rs
5459 search: pub param_names_for_lifetime_elision_hints: bool,
5460 search: param_names_for_lifetime_elision_hints: self
5461 static_index.rs
5462 search: param_names_for_lifetime_elision_hints: false,
5463 rust-analyzer/src/
5464 cli/
5465 analysis_stats.rs
5466 search: param_names_for_lifetime_elision_hints: true,
5467 config.rs
5468 search: param_names_for_lifetime_elision_hints: self"#;
5469
5470 cx.executor()
5471 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5472 cx.run_until_parked();
5473 outline_panel.update(cx, |outline_panel, cx| {
5474 assert_eq!(
5475 display_entries(
5476 &project,
5477 &snapshot(&outline_panel, cx),
5478 &outline_panel.cached_entries,
5479 None,
5480 cx,
5481 ),
5482 all_matches,
5483 );
5484 });
5485
5486 let filter_text = "a";
5487 outline_panel.update_in(cx, |outline_panel, window, cx| {
5488 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5489 filter_editor.set_text(filter_text, window, cx);
5490 });
5491 });
5492 cx.executor()
5493 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5494 cx.run_until_parked();
5495
5496 outline_panel.update(cx, |outline_panel, cx| {
5497 assert_eq!(
5498 display_entries(
5499 &project,
5500 &snapshot(&outline_panel, cx),
5501 &outline_panel.cached_entries,
5502 None,
5503 cx,
5504 ),
5505 all_matches
5506 .lines()
5507 .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5508 .filter(|item| item.contains(filter_text))
5509 .collect::<Vec<_>>()
5510 .join("\n"),
5511 );
5512 });
5513
5514 outline_panel.update_in(cx, |outline_panel, window, cx| {
5515 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5516 filter_editor.set_text("", window, cx);
5517 });
5518 });
5519 cx.executor()
5520 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5521 cx.run_until_parked();
5522 outline_panel.update(cx, |outline_panel, cx| {
5523 assert_eq!(
5524 display_entries(
5525 &project,
5526 &snapshot(&outline_panel, cx),
5527 &outline_panel.cached_entries,
5528 None,
5529 cx,
5530 ),
5531 all_matches,
5532 );
5533 });
5534 }
5535
5536 #[gpui::test(iterations = 10)]
5537 async fn test_item_opening(cx: &mut TestAppContext) {
5538 init_test(cx);
5539
5540 let fs = FakeFs::new(cx.background_executor.clone());
5541 populate_with_test_ra_project(&fs, path!("/rust-analyzer")).await;
5542 let project = Project::test(fs.clone(), [path!("/rust-analyzer").as_ref()], cx).await;
5543 project.read_with(cx, |project, _| {
5544 project.languages().add(Arc::new(rust_lang()))
5545 });
5546 let workspace = add_outline_panel(&project, cx).await;
5547 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5548 let outline_panel = outline_panel(&workspace, cx);
5549 outline_panel.update_in(cx, |outline_panel, window, cx| {
5550 outline_panel.set_active(true, window, cx)
5551 });
5552
5553 workspace
5554 .update(cx, |workspace, window, cx| {
5555 ProjectSearchView::deploy_search(
5556 workspace,
5557 &workspace::DeploySearch::default(),
5558 window,
5559 cx,
5560 )
5561 })
5562 .unwrap();
5563 let search_view = workspace
5564 .update(cx, |workspace, _, cx| {
5565 workspace
5566 .active_pane()
5567 .read(cx)
5568 .items()
5569 .find_map(|item| item.downcast::<ProjectSearchView>())
5570 .expect("Project search view expected to appear after new search event trigger")
5571 })
5572 .unwrap();
5573
5574 let query = "param_names_for_lifetime_elision_hints";
5575 perform_project_search(&search_view, query, cx);
5576 search_view.update(cx, |search_view, cx| {
5577 search_view
5578 .results_editor()
5579 .update(cx, |results_editor, cx| {
5580 assert_eq!(
5581 results_editor.display_text(cx).match_indices(query).count(),
5582 9
5583 );
5584 });
5585 });
5586 let root_path = format!("{}/", path!("/rust-analyzer"));
5587 let all_matches = format!(
5588 r#"{root_path}
5589 crates/
5590 ide/src/
5591 inlay_hints/
5592 fn_lifetime_fn.rs
5593 search: match config.param_names_for_lifetime_elision_hints {{
5594 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
5595 search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
5596 search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
5597 inlay_hints.rs
5598 search: pub param_names_for_lifetime_elision_hints: bool,
5599 search: param_names_for_lifetime_elision_hints: self
5600 static_index.rs
5601 search: param_names_for_lifetime_elision_hints: false,
5602 rust-analyzer/src/
5603 cli/
5604 analysis_stats.rs
5605 search: param_names_for_lifetime_elision_hints: true,
5606 config.rs
5607 search: param_names_for_lifetime_elision_hints: self"#
5608 );
5609 let select_first_in_all_matches = |line_to_select: &str| {
5610 assert!(all_matches.contains(line_to_select));
5611 all_matches.replacen(
5612 line_to_select,
5613 &format!("{line_to_select}{SELECTED_MARKER}"),
5614 1,
5615 )
5616 };
5617 cx.executor()
5618 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5619 cx.run_until_parked();
5620
5621 let active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5622 outline_panel
5623 .active_editor()
5624 .expect("should have an active editor open")
5625 });
5626 let initial_outline_selection =
5627 "search: match config.param_names_for_lifetime_elision_hints {";
5628 outline_panel.update_in(cx, |outline_panel, window, cx| {
5629 assert_eq!(
5630 display_entries(
5631 &project,
5632 &snapshot(&outline_panel, cx),
5633 &outline_panel.cached_entries,
5634 outline_panel.selected_entry(),
5635 cx,
5636 ),
5637 select_first_in_all_matches(initial_outline_selection)
5638 );
5639 assert_eq!(
5640 selected_row_text(&active_editor, cx),
5641 initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5642 "Should place the initial editor selection on the corresponding search result"
5643 );
5644
5645 outline_panel.select_next(&SelectNext, window, cx);
5646 outline_panel.select_next(&SelectNext, window, cx);
5647 });
5648
5649 let navigated_outline_selection =
5650 "search: Some(it) if config.param_names_for_lifetime_elision_hints => {";
5651 outline_panel.update(cx, |outline_panel, cx| {
5652 assert_eq!(
5653 display_entries(
5654 &project,
5655 &snapshot(&outline_panel, cx),
5656 &outline_panel.cached_entries,
5657 outline_panel.selected_entry(),
5658 cx,
5659 ),
5660 select_first_in_all_matches(navigated_outline_selection)
5661 );
5662 });
5663 cx.executor()
5664 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5665 outline_panel.update(cx, |_, cx| {
5666 assert_eq!(
5667 selected_row_text(&active_editor, cx),
5668 navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5669 "Should still have the initial caret position after SelectNext calls"
5670 );
5671 });
5672
5673 outline_panel.update_in(cx, |outline_panel, window, cx| {
5674 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5675 });
5676 outline_panel.update(cx, |_outline_panel, cx| {
5677 assert_eq!(
5678 selected_row_text(&active_editor, cx),
5679 navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5680 "After opening, should move the caret to the opened outline entry's position"
5681 );
5682 });
5683
5684 outline_panel.update_in(cx, |outline_panel, window, cx| {
5685 outline_panel.select_next(&SelectNext, window, cx);
5686 });
5687 let next_navigated_outline_selection = "search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },";
5688 outline_panel.update(cx, |outline_panel, cx| {
5689 assert_eq!(
5690 display_entries(
5691 &project,
5692 &snapshot(&outline_panel, cx),
5693 &outline_panel.cached_entries,
5694 outline_panel.selected_entry(),
5695 cx,
5696 ),
5697 select_first_in_all_matches(next_navigated_outline_selection)
5698 );
5699 });
5700 cx.executor()
5701 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5702 outline_panel.update(cx, |_outline_panel, cx| {
5703 assert_eq!(
5704 selected_row_text(&active_editor, cx),
5705 next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5706 "Should again preserve the selection after another SelectNext call"
5707 );
5708 });
5709
5710 outline_panel.update_in(cx, |outline_panel, window, cx| {
5711 outline_panel.open_excerpts(&editor::OpenExcerpts, window, cx);
5712 });
5713 cx.executor()
5714 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5715 cx.run_until_parked();
5716 let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5717 outline_panel
5718 .active_editor()
5719 .expect("should have an active editor open")
5720 });
5721 outline_panel.update(cx, |outline_panel, cx| {
5722 assert_ne!(
5723 active_editor, new_active_editor,
5724 "After opening an excerpt, new editor should be open"
5725 );
5726 assert_eq!(
5727 display_entries(
5728 &project,
5729 &snapshot(&outline_panel, cx),
5730 &outline_panel.cached_entries,
5731 outline_panel.selected_entry(),
5732 cx,
5733 ),
5734 "fn_lifetime_fn.rs <==== selected"
5735 );
5736 assert_eq!(
5737 selected_row_text(&new_active_editor, cx),
5738 next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5739 "When opening the excerpt, should navigate to the place corresponding the outline entry"
5740 );
5741 });
5742 }
5743
5744 #[gpui::test]
5745 async fn test_multiple_workrees(cx: &mut TestAppContext) {
5746 init_test(cx);
5747
5748 let fs = FakeFs::new(cx.background_executor.clone());
5749 fs.insert_tree(
5750 "/root",
5751 json!({
5752 "one": {
5753 "a.txt": "aaa aaa"
5754 },
5755 "two": {
5756 "b.txt": "a aaa"
5757 }
5758
5759 }),
5760 )
5761 .await;
5762 let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await;
5763 let workspace = add_outline_panel(&project, cx).await;
5764 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5765 let outline_panel = outline_panel(&workspace, cx);
5766 outline_panel.update_in(cx, |outline_panel, window, cx| {
5767 outline_panel.set_active(true, window, cx)
5768 });
5769
5770 let items = workspace
5771 .update(cx, |workspace, window, cx| {
5772 workspace.open_paths(
5773 vec![PathBuf::from("/root/two")],
5774 OpenOptions {
5775 visible: Some(OpenVisible::OnlyDirectories),
5776 ..Default::default()
5777 },
5778 None,
5779 window,
5780 cx,
5781 )
5782 })
5783 .unwrap()
5784 .await;
5785 assert_eq!(items.len(), 1, "Were opening another worktree directory");
5786 assert!(
5787 items[0].is_none(),
5788 "Directory should be opened successfully"
5789 );
5790
5791 workspace
5792 .update(cx, |workspace, window, cx| {
5793 ProjectSearchView::deploy_search(
5794 workspace,
5795 &workspace::DeploySearch::default(),
5796 window,
5797 cx,
5798 )
5799 })
5800 .unwrap();
5801 let search_view = workspace
5802 .update(cx, |workspace, _, cx| {
5803 workspace
5804 .active_pane()
5805 .read(cx)
5806 .items()
5807 .find_map(|item| item.downcast::<ProjectSearchView>())
5808 .expect("Project search view expected to appear after new search event trigger")
5809 })
5810 .unwrap();
5811
5812 let query = "aaa";
5813 perform_project_search(&search_view, query, cx);
5814 search_view.update(cx, |search_view, cx| {
5815 search_view
5816 .results_editor()
5817 .update(cx, |results_editor, cx| {
5818 assert_eq!(
5819 results_editor.display_text(cx).match_indices(query).count(),
5820 3
5821 );
5822 });
5823 });
5824
5825 cx.executor()
5826 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5827 cx.run_until_parked();
5828 outline_panel.update(cx, |outline_panel, cx| {
5829 assert_eq!(
5830 display_entries(
5831 &project,
5832 &snapshot(&outline_panel, cx),
5833 &outline_panel.cached_entries,
5834 outline_panel.selected_entry(),
5835 cx,
5836 ),
5837 r#"/root/one/
5838 a.txt
5839 search: aaa aaa <==== selected
5840 search: aaa aaa
5841/root/two/
5842 b.txt
5843 search: a aaa"#
5844 );
5845 });
5846
5847 outline_panel.update_in(cx, |outline_panel, window, cx| {
5848 outline_panel.select_previous(&SelectPrevious, window, cx);
5849 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5850 });
5851 cx.executor()
5852 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5853 cx.run_until_parked();
5854 outline_panel.update(cx, |outline_panel, cx| {
5855 assert_eq!(
5856 display_entries(
5857 &project,
5858 &snapshot(&outline_panel, cx),
5859 &outline_panel.cached_entries,
5860 outline_panel.selected_entry(),
5861 cx,
5862 ),
5863 r#"/root/one/
5864 a.txt <==== selected
5865/root/two/
5866 b.txt
5867 search: a aaa"#
5868 );
5869 });
5870
5871 outline_panel.update_in(cx, |outline_panel, window, cx| {
5872 outline_panel.select_next(&SelectNext, window, cx);
5873 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5874 });
5875 cx.executor()
5876 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5877 cx.run_until_parked();
5878 outline_panel.update(cx, |outline_panel, cx| {
5879 assert_eq!(
5880 display_entries(
5881 &project,
5882 &snapshot(&outline_panel, cx),
5883 &outline_panel.cached_entries,
5884 outline_panel.selected_entry(),
5885 cx,
5886 ),
5887 r#"/root/one/
5888 a.txt
5889/root/two/ <==== selected"#
5890 );
5891 });
5892
5893 outline_panel.update_in(cx, |outline_panel, window, cx| {
5894 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5895 });
5896 cx.executor()
5897 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5898 cx.run_until_parked();
5899 outline_panel.update(cx, |outline_panel, cx| {
5900 assert_eq!(
5901 display_entries(
5902 &project,
5903 &snapshot(&outline_panel, cx),
5904 &outline_panel.cached_entries,
5905 outline_panel.selected_entry(),
5906 cx,
5907 ),
5908 r#"/root/one/
5909 a.txt
5910/root/two/ <==== selected
5911 b.txt
5912 search: a aaa"#
5913 );
5914 });
5915 }
5916
5917 #[gpui::test]
5918 async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
5919 init_test(cx);
5920
5921 let root = path!("/root");
5922 let fs = FakeFs::new(cx.background_executor.clone());
5923 fs.insert_tree(
5924 root,
5925 json!({
5926 "src": {
5927 "lib.rs": indoc!("
5928#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5929struct OutlineEntryExcerpt {
5930 id: ExcerptId,
5931 buffer_id: BufferId,
5932 range: ExcerptRange<language::Anchor>,
5933}"),
5934 }
5935 }),
5936 )
5937 .await;
5938 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
5939 project.read_with(cx, |project, _| {
5940 project.languages().add(Arc::new(
5941 rust_lang()
5942 .with_outline_query(
5943 r#"
5944 (struct_item
5945 (visibility_modifier)? @context
5946 "struct" @context
5947 name: (_) @name) @item
5948
5949 (field_declaration
5950 (visibility_modifier)? @context
5951 name: (_) @name) @item
5952"#,
5953 )
5954 .unwrap(),
5955 ))
5956 });
5957 let workspace = add_outline_panel(&project, cx).await;
5958 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5959 let outline_panel = outline_panel(&workspace, cx);
5960 cx.update(|window, cx| {
5961 outline_panel.update(cx, |outline_panel, cx| {
5962 outline_panel.set_active(true, window, cx)
5963 });
5964 });
5965
5966 let _editor = workspace
5967 .update(cx, |workspace, window, cx| {
5968 workspace.open_abs_path(
5969 PathBuf::from(path!("/root/src/lib.rs")),
5970 OpenOptions {
5971 visible: Some(OpenVisible::All),
5972 ..Default::default()
5973 },
5974 window,
5975 cx,
5976 )
5977 })
5978 .unwrap()
5979 .await
5980 .expect("Failed to open Rust source file")
5981 .downcast::<Editor>()
5982 .expect("Should open an editor for Rust source file");
5983
5984 cx.executor()
5985 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5986 cx.run_until_parked();
5987 outline_panel.update(cx, |outline_panel, cx| {
5988 assert_eq!(
5989 display_entries(
5990 &project,
5991 &snapshot(&outline_panel, cx),
5992 &outline_panel.cached_entries,
5993 outline_panel.selected_entry(),
5994 cx,
5995 ),
5996 indoc!(
5997 "
5998outline: struct OutlineEntryExcerpt
5999 outline: id
6000 outline: buffer_id
6001 outline: range"
6002 )
6003 );
6004 });
6005
6006 cx.update(|window, cx| {
6007 outline_panel.update(cx, |outline_panel, cx| {
6008 outline_panel.select_next(&SelectNext, window, cx);
6009 });
6010 });
6011 cx.executor()
6012 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6013 cx.run_until_parked();
6014 outline_panel.update(cx, |outline_panel, cx| {
6015 assert_eq!(
6016 display_entries(
6017 &project,
6018 &snapshot(&outline_panel, cx),
6019 &outline_panel.cached_entries,
6020 outline_panel.selected_entry(),
6021 cx,
6022 ),
6023 indoc!(
6024 "
6025outline: struct OutlineEntryExcerpt <==== selected
6026 outline: id
6027 outline: buffer_id
6028 outline: range"
6029 )
6030 );
6031 });
6032
6033 cx.update(|window, cx| {
6034 outline_panel.update(cx, |outline_panel, cx| {
6035 outline_panel.select_next(&SelectNext, window, cx);
6036 });
6037 });
6038 cx.executor()
6039 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6040 cx.run_until_parked();
6041 outline_panel.update(cx, |outline_panel, cx| {
6042 assert_eq!(
6043 display_entries(
6044 &project,
6045 &snapshot(&outline_panel, cx),
6046 &outline_panel.cached_entries,
6047 outline_panel.selected_entry(),
6048 cx,
6049 ),
6050 indoc!(
6051 "
6052outline: struct OutlineEntryExcerpt
6053 outline: id <==== selected
6054 outline: buffer_id
6055 outline: range"
6056 )
6057 );
6058 });
6059
6060 cx.update(|window, cx| {
6061 outline_panel.update(cx, |outline_panel, cx| {
6062 outline_panel.select_next(&SelectNext, window, cx);
6063 });
6064 });
6065 cx.executor()
6066 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6067 cx.run_until_parked();
6068 outline_panel.update(cx, |outline_panel, cx| {
6069 assert_eq!(
6070 display_entries(
6071 &project,
6072 &snapshot(&outline_panel, cx),
6073 &outline_panel.cached_entries,
6074 outline_panel.selected_entry(),
6075 cx,
6076 ),
6077 indoc!(
6078 "
6079outline: struct OutlineEntryExcerpt
6080 outline: id
6081 outline: buffer_id <==== selected
6082 outline: range"
6083 )
6084 );
6085 });
6086
6087 cx.update(|window, cx| {
6088 outline_panel.update(cx, |outline_panel, cx| {
6089 outline_panel.select_next(&SelectNext, window, cx);
6090 });
6091 });
6092 cx.executor()
6093 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6094 cx.run_until_parked();
6095 outline_panel.update(cx, |outline_panel, cx| {
6096 assert_eq!(
6097 display_entries(
6098 &project,
6099 &snapshot(&outline_panel, cx),
6100 &outline_panel.cached_entries,
6101 outline_panel.selected_entry(),
6102 cx,
6103 ),
6104 indoc!(
6105 "
6106outline: struct OutlineEntryExcerpt
6107 outline: id
6108 outline: buffer_id
6109 outline: range <==== selected"
6110 )
6111 );
6112 });
6113
6114 cx.update(|window, cx| {
6115 outline_panel.update(cx, |outline_panel, cx| {
6116 outline_panel.select_next(&SelectNext, window, cx);
6117 });
6118 });
6119 cx.executor()
6120 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6121 cx.run_until_parked();
6122 outline_panel.update(cx, |outline_panel, cx| {
6123 assert_eq!(
6124 display_entries(
6125 &project,
6126 &snapshot(&outline_panel, cx),
6127 &outline_panel.cached_entries,
6128 outline_panel.selected_entry(),
6129 cx,
6130 ),
6131 indoc!(
6132 "
6133outline: struct OutlineEntryExcerpt <==== selected
6134 outline: id
6135 outline: buffer_id
6136 outline: range"
6137 )
6138 );
6139 });
6140
6141 cx.update(|window, cx| {
6142 outline_panel.update(cx, |outline_panel, cx| {
6143 outline_panel.select_previous(&SelectPrevious, window, cx);
6144 });
6145 });
6146 cx.executor()
6147 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6148 cx.run_until_parked();
6149 outline_panel.update(cx, |outline_panel, cx| {
6150 assert_eq!(
6151 display_entries(
6152 &project,
6153 &snapshot(&outline_panel, cx),
6154 &outline_panel.cached_entries,
6155 outline_panel.selected_entry(),
6156 cx,
6157 ),
6158 indoc!(
6159 "
6160outline: struct OutlineEntryExcerpt
6161 outline: id
6162 outline: buffer_id
6163 outline: range <==== selected"
6164 )
6165 );
6166 });
6167
6168 cx.update(|window, cx| {
6169 outline_panel.update(cx, |outline_panel, cx| {
6170 outline_panel.select_previous(&SelectPrevious, window, cx);
6171 });
6172 });
6173 cx.executor()
6174 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6175 cx.run_until_parked();
6176 outline_panel.update(cx, |outline_panel, cx| {
6177 assert_eq!(
6178 display_entries(
6179 &project,
6180 &snapshot(&outline_panel, cx),
6181 &outline_panel.cached_entries,
6182 outline_panel.selected_entry(),
6183 cx,
6184 ),
6185 indoc!(
6186 "
6187outline: struct OutlineEntryExcerpt
6188 outline: id
6189 outline: buffer_id <==== selected
6190 outline: range"
6191 )
6192 );
6193 });
6194
6195 cx.update(|window, cx| {
6196 outline_panel.update(cx, |outline_panel, cx| {
6197 outline_panel.select_previous(&SelectPrevious, window, cx);
6198 });
6199 });
6200 cx.executor()
6201 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6202 cx.run_until_parked();
6203 outline_panel.update(cx, |outline_panel, cx| {
6204 assert_eq!(
6205 display_entries(
6206 &project,
6207 &snapshot(&outline_panel, cx),
6208 &outline_panel.cached_entries,
6209 outline_panel.selected_entry(),
6210 cx,
6211 ),
6212 indoc!(
6213 "
6214outline: struct OutlineEntryExcerpt
6215 outline: id <==== selected
6216 outline: buffer_id
6217 outline: range"
6218 )
6219 );
6220 });
6221
6222 cx.update(|window, cx| {
6223 outline_panel.update(cx, |outline_panel, cx| {
6224 outline_panel.select_previous(&SelectPrevious, window, cx);
6225 });
6226 });
6227 cx.executor()
6228 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6229 cx.run_until_parked();
6230 outline_panel.update(cx, |outline_panel, cx| {
6231 assert_eq!(
6232 display_entries(
6233 &project,
6234 &snapshot(&outline_panel, cx),
6235 &outline_panel.cached_entries,
6236 outline_panel.selected_entry(),
6237 cx,
6238 ),
6239 indoc!(
6240 "
6241outline: struct OutlineEntryExcerpt <==== selected
6242 outline: id
6243 outline: buffer_id
6244 outline: range"
6245 )
6246 );
6247 });
6248
6249 cx.update(|window, cx| {
6250 outline_panel.update(cx, |outline_panel, cx| {
6251 outline_panel.select_previous(&SelectPrevious, window, cx);
6252 });
6253 });
6254 cx.executor()
6255 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6256 cx.run_until_parked();
6257 outline_panel.update(cx, |outline_panel, cx| {
6258 assert_eq!(
6259 display_entries(
6260 &project,
6261 &snapshot(&outline_panel, cx),
6262 &outline_panel.cached_entries,
6263 outline_panel.selected_entry(),
6264 cx,
6265 ),
6266 indoc!(
6267 "
6268outline: struct OutlineEntryExcerpt
6269 outline: id
6270 outline: buffer_id
6271 outline: range <==== selected"
6272 )
6273 );
6274 });
6275 }
6276
6277 #[gpui::test(iterations = 10)]
6278 async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6279 init_test(cx);
6280
6281 let root = "/frontend-project";
6282 let fs = FakeFs::new(cx.background_executor.clone());
6283 fs.insert_tree(
6284 root,
6285 json!({
6286 "public": {
6287 "lottie": {
6288 "syntax-tree.json": r#"{ "something": "static" }"#
6289 }
6290 },
6291 "src": {
6292 "app": {
6293 "(site)": {
6294 "(about)": {
6295 "jobs": {
6296 "[slug]": {
6297 "page.tsx": r#"static"#
6298 }
6299 }
6300 },
6301 "(blog)": {
6302 "post": {
6303 "[slug]": {
6304 "page.tsx": r#"static"#
6305 }
6306 }
6307 },
6308 }
6309 },
6310 "components": {
6311 "ErrorBoundary.tsx": r#"static"#,
6312 }
6313 }
6314
6315 }),
6316 )
6317 .await;
6318 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
6319 let workspace = add_outline_panel(&project, cx).await;
6320 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6321 let outline_panel = outline_panel(&workspace, cx);
6322 outline_panel.update_in(cx, |outline_panel, window, cx| {
6323 outline_panel.set_active(true, window, cx)
6324 });
6325
6326 workspace
6327 .update(cx, |workspace, window, cx| {
6328 ProjectSearchView::deploy_search(
6329 workspace,
6330 &workspace::DeploySearch::default(),
6331 window,
6332 cx,
6333 )
6334 })
6335 .unwrap();
6336 let search_view = workspace
6337 .update(cx, |workspace, _, cx| {
6338 workspace
6339 .active_pane()
6340 .read(cx)
6341 .items()
6342 .find_map(|item| item.downcast::<ProjectSearchView>())
6343 .expect("Project search view expected to appear after new search event trigger")
6344 })
6345 .unwrap();
6346
6347 let query = "static";
6348 perform_project_search(&search_view, query, cx);
6349 search_view.update(cx, |search_view, cx| {
6350 search_view
6351 .results_editor()
6352 .update(cx, |results_editor, cx| {
6353 assert_eq!(
6354 results_editor.display_text(cx).match_indices(query).count(),
6355 4
6356 );
6357 });
6358 });
6359
6360 cx.executor()
6361 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6362 cx.run_until_parked();
6363 outline_panel.update(cx, |outline_panel, cx| {
6364 assert_eq!(
6365 display_entries(
6366 &project,
6367 &snapshot(&outline_panel, cx),
6368 &outline_panel.cached_entries,
6369 outline_panel.selected_entry(),
6370 cx,
6371 ),
6372 r#"/frontend-project/
6373 public/lottie/
6374 syntax-tree.json
6375 search: { "something": "static" } <==== selected
6376 src/
6377 app/(site)/
6378 (about)/jobs/[slug]/
6379 page.tsx
6380 search: static
6381 (blog)/post/[slug]/
6382 page.tsx
6383 search: static
6384 components/
6385 ErrorBoundary.tsx
6386 search: static"#
6387 );
6388 });
6389
6390 outline_panel.update_in(cx, |outline_panel, window, cx| {
6391 // Move to 5th element in the list, 3 items down.
6392 for _ in 0..2 {
6393 outline_panel.select_next(&SelectNext, window, cx);
6394 }
6395 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6396 });
6397 cx.executor()
6398 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6399 cx.run_until_parked();
6400 outline_panel.update(cx, |outline_panel, cx| {
6401 assert_eq!(
6402 display_entries(
6403 &project,
6404 &snapshot(&outline_panel, cx),
6405 &outline_panel.cached_entries,
6406 outline_panel.selected_entry(),
6407 cx,
6408 ),
6409 r#"/frontend-project/
6410 public/lottie/
6411 syntax-tree.json
6412 search: { "something": "static" }
6413 src/
6414 app/(site)/ <==== selected
6415 components/
6416 ErrorBoundary.tsx
6417 search: static"#
6418 );
6419 });
6420
6421 outline_panel.update_in(cx, |outline_panel, window, cx| {
6422 // Move to the next visible non-FS entry
6423 for _ in 0..3 {
6424 outline_panel.select_next(&SelectNext, window, cx);
6425 }
6426 });
6427 cx.run_until_parked();
6428 outline_panel.update(cx, |outline_panel, cx| {
6429 assert_eq!(
6430 display_entries(
6431 &project,
6432 &snapshot(&outline_panel, cx),
6433 &outline_panel.cached_entries,
6434 outline_panel.selected_entry(),
6435 cx,
6436 ),
6437 r#"/frontend-project/
6438 public/lottie/
6439 syntax-tree.json
6440 search: { "something": "static" }
6441 src/
6442 app/(site)/
6443 components/
6444 ErrorBoundary.tsx
6445 search: static <==== selected"#
6446 );
6447 });
6448
6449 outline_panel.update_in(cx, |outline_panel, window, cx| {
6450 outline_panel
6451 .active_editor()
6452 .expect("Should have an active editor")
6453 .update(cx, |editor, cx| {
6454 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6455 });
6456 });
6457 cx.executor()
6458 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6459 cx.run_until_parked();
6460 outline_panel.update(cx, |outline_panel, cx| {
6461 assert_eq!(
6462 display_entries(
6463 &project,
6464 &snapshot(&outline_panel, cx),
6465 &outline_panel.cached_entries,
6466 outline_panel.selected_entry(),
6467 cx,
6468 ),
6469 r#"/frontend-project/
6470 public/lottie/
6471 syntax-tree.json
6472 search: { "something": "static" }
6473 src/
6474 app/(site)/
6475 components/
6476 ErrorBoundary.tsx <==== selected"#
6477 );
6478 });
6479
6480 outline_panel.update_in(cx, |outline_panel, window, cx| {
6481 outline_panel
6482 .active_editor()
6483 .expect("Should have an active editor")
6484 .update(cx, |editor, cx| {
6485 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6486 });
6487 });
6488 cx.executor()
6489 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6490 cx.run_until_parked();
6491 outline_panel.update(cx, |outline_panel, cx| {
6492 assert_eq!(
6493 display_entries(
6494 &project,
6495 &snapshot(&outline_panel, cx),
6496 &outline_panel.cached_entries,
6497 outline_panel.selected_entry(),
6498 cx,
6499 ),
6500 r#"/frontend-project/
6501 public/lottie/
6502 syntax-tree.json
6503 search: { "something": "static" }
6504 src/
6505 app/(site)/
6506 components/
6507 ErrorBoundary.tsx <==== selected
6508 search: static"#
6509 );
6510 });
6511 }
6512
6513 async fn add_outline_panel(
6514 project: &Entity<Project>,
6515 cx: &mut TestAppContext,
6516 ) -> WindowHandle<Workspace> {
6517 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6518
6519 let outline_panel = window
6520 .update(cx, |_, window, cx| {
6521 cx.spawn_in(window, async |this, cx| {
6522 OutlinePanel::load(this, cx.clone()).await
6523 })
6524 })
6525 .unwrap()
6526 .await
6527 .expect("Failed to load outline panel");
6528
6529 window
6530 .update(cx, |workspace, window, cx| {
6531 workspace.add_panel(outline_panel, window, cx);
6532 })
6533 .unwrap();
6534 window
6535 }
6536
6537 fn outline_panel(
6538 workspace: &WindowHandle<Workspace>,
6539 cx: &mut TestAppContext,
6540 ) -> Entity<OutlinePanel> {
6541 workspace
6542 .update(cx, |workspace, _, cx| {
6543 workspace
6544 .panel::<OutlinePanel>(cx)
6545 .expect("no outline panel")
6546 })
6547 .unwrap()
6548 }
6549
6550 fn display_entries(
6551 project: &Entity<Project>,
6552 multi_buffer_snapshot: &MultiBufferSnapshot,
6553 cached_entries: &[CachedEntry],
6554 selected_entry: Option<&PanelEntry>,
6555 cx: &mut App,
6556 ) -> String {
6557 let mut display_string = String::new();
6558 for entry in cached_entries {
6559 if !display_string.is_empty() {
6560 display_string += "\n";
6561 }
6562 for _ in 0..entry.depth {
6563 display_string += " ";
6564 }
6565 display_string += &match &entry.entry {
6566 PanelEntry::Fs(entry) => match entry {
6567 FsEntry::ExternalFile(_) => {
6568 panic!("Did not cover external files with tests")
6569 }
6570 FsEntry::Directory(directory) => {
6571 match project
6572 .read(cx)
6573 .worktree_for_id(directory.worktree_id, cx)
6574 .and_then(|worktree| {
6575 if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
6576 Some(worktree.read(cx).abs_path())
6577 } else {
6578 None
6579 }
6580 }) {
6581 Some(root_path) => format!(
6582 "{}/{}",
6583 root_path.display(),
6584 directory.entry.path.display(),
6585 ),
6586 None => format!(
6587 "{}/",
6588 directory
6589 .entry
6590 .path
6591 .file_name()
6592 .unwrap_or_default()
6593 .to_string_lossy()
6594 ),
6595 }
6596 }
6597 FsEntry::File(file) => file
6598 .entry
6599 .path
6600 .file_name()
6601 .map(|name| name.to_string_lossy().to_string())
6602 .unwrap_or_default(),
6603 },
6604 PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6605 .entries
6606 .iter()
6607 .filter_map(|dir| dir.path.file_name())
6608 .map(|name| name.to_string_lossy().to_string() + "/")
6609 .collect(),
6610 PanelEntry::Outline(outline_entry) => match outline_entry {
6611 OutlineEntry::Excerpt(_) => continue,
6612 OutlineEntry::Outline(outline_entry) => {
6613 format!("outline: {}", outline_entry.outline.text)
6614 }
6615 },
6616 PanelEntry::Search(search_entry) => {
6617 format!(
6618 "search: {}",
6619 search_entry
6620 .render_data
6621 .get_or_init(|| SearchData::new(
6622 &search_entry.match_range,
6623 &multi_buffer_snapshot
6624 ))
6625 .context_text
6626 )
6627 }
6628 };
6629
6630 if Some(&entry.entry) == selected_entry {
6631 display_string += SELECTED_MARKER;
6632 }
6633 }
6634 display_string
6635 }
6636
6637 fn init_test(cx: &mut TestAppContext) {
6638 cx.update(|cx| {
6639 let settings = SettingsStore::test(cx);
6640 cx.set_global(settings);
6641
6642 theme::init(theme::LoadThemes::JustBase, cx);
6643
6644 language::init(cx);
6645 editor::init(cx);
6646 workspace::init_settings(cx);
6647 Project::init_settings(cx);
6648 project_search::init(cx);
6649 super::init(cx);
6650 });
6651 }
6652
6653 // Based on https://github.com/rust-lang/rust-analyzer/
6654 async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6655 fs.insert_tree(
6656 root,
6657 json!({
6658 "crates": {
6659 "ide": {
6660 "src": {
6661 "inlay_hints": {
6662 "fn_lifetime_fn.rs": r##"
6663 pub(super) fn hints(
6664 acc: &mut Vec<InlayHint>,
6665 config: &InlayHintsConfig,
6666 func: ast::Fn,
6667 ) -> Option<()> {
6668 // ... snip
6669
6670 let mut used_names: FxHashMap<SmolStr, usize> =
6671 match config.param_names_for_lifetime_elision_hints {
6672 true => generic_param_list
6673 .iter()
6674 .flat_map(|gpl| gpl.lifetime_params())
6675 .filter_map(|param| param.lifetime())
6676 .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6677 .collect(),
6678 false => Default::default(),
6679 };
6680 {
6681 let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6682 if self_param.is_some() && potential_lt_refs.next().is_some() {
6683 allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6684 // self can't be used as a lifetime, so no need to check for collisions
6685 "'self".into()
6686 } else {
6687 gen_idx_name()
6688 });
6689 }
6690 potential_lt_refs.for_each(|(name, ..)| {
6691 let name = match name {
6692 Some(it) if config.param_names_for_lifetime_elision_hints => {
6693 if let Some(c) = used_names.get_mut(it.text().as_str()) {
6694 *c += 1;
6695 SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6696 } else {
6697 used_names.insert(it.text().as_str().into(), 0);
6698 SmolStr::from_iter(["\'", it.text().as_str()])
6699 }
6700 }
6701 _ => gen_idx_name(),
6702 };
6703 allocated_lifetimes.push(name);
6704 });
6705 }
6706
6707 // ... snip
6708 }
6709
6710 // ... snip
6711
6712 #[test]
6713 fn hints_lifetimes_named() {
6714 check_with_config(
6715 InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6716 r#"
6717 fn nested_in<'named>(named: & &X< &()>) {}
6718 // ^'named1, 'named2, 'named3, $
6719 //^'named1 ^'named2 ^'named3
6720 "#,
6721 );
6722 }
6723
6724 // ... snip
6725 "##,
6726 },
6727 "inlay_hints.rs": r#"
6728 #[derive(Clone, Debug, PartialEq, Eq)]
6729 pub struct InlayHintsConfig {
6730 // ... snip
6731 pub param_names_for_lifetime_elision_hints: bool,
6732 pub max_length: Option<usize>,
6733 // ... snip
6734 }
6735
6736 impl Config {
6737 pub fn inlay_hints(&self) -> InlayHintsConfig {
6738 InlayHintsConfig {
6739 // ... snip
6740 param_names_for_lifetime_elision_hints: self
6741 .inlayHints_lifetimeElisionHints_useParameterNames()
6742 .to_owned(),
6743 max_length: self.inlayHints_maxLength().to_owned(),
6744 // ... snip
6745 }
6746 }
6747 }
6748 "#,
6749 "static_index.rs": r#"
6750// ... snip
6751 fn add_file(&mut self, file_id: FileId) {
6752 let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6753 let folds = self.analysis.folding_ranges(file_id).unwrap();
6754 let inlay_hints = self
6755 .analysis
6756 .inlay_hints(
6757 &InlayHintsConfig {
6758 // ... snip
6759 closure_style: hir::ClosureStyle::ImplFn,
6760 param_names_for_lifetime_elision_hints: false,
6761 binding_mode_hints: false,
6762 max_length: Some(25),
6763 closure_capture_hints: false,
6764 // ... snip
6765 },
6766 file_id,
6767 None,
6768 )
6769 .unwrap();
6770 // ... snip
6771 }
6772// ... snip
6773 "#
6774 }
6775 },
6776 "rust-analyzer": {
6777 "src": {
6778 "cli": {
6779 "analysis_stats.rs": r#"
6780 // ... snip
6781 for &file_id in &file_ids {
6782 _ = analysis.inlay_hints(
6783 &InlayHintsConfig {
6784 // ... snip
6785 implicit_drop_hints: true,
6786 lifetime_elision_hints: ide::LifetimeElisionHints::Always,
6787 param_names_for_lifetime_elision_hints: true,
6788 hide_named_constructor_hints: false,
6789 hide_closure_initialization_hints: false,
6790 closure_style: hir::ClosureStyle::ImplFn,
6791 max_length: Some(25),
6792 closing_brace_hints_min_lines: Some(20),
6793 fields_to_resolve: InlayFieldsToResolve::empty(),
6794 range_exclusive_hints: true,
6795 },
6796 file_id.into(),
6797 None,
6798 );
6799 }
6800 // ... snip
6801 "#,
6802 },
6803 "config.rs": r#"
6804 config_data! {
6805 /// Configs that only make sense when they are set by a client. As such they can only be defined
6806 /// by setting them using client's settings (e.g `settings.json` on VS Code).
6807 client: struct ClientDefaultConfigData <- ClientConfigInput -> {
6808 // ... snip
6809 /// Maximum length for inlay hints. Set to null to have an unlimited length.
6810 inlayHints_maxLength: Option<usize> = Some(25),
6811 // ... snip
6812 /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
6813 inlayHints_lifetimeElisionHints_useParameterNames: bool = false,
6814 // ... snip
6815 }
6816 }
6817
6818 impl Config {
6819 // ... snip
6820 pub fn inlay_hints(&self) -> InlayHintsConfig {
6821 InlayHintsConfig {
6822 // ... snip
6823 param_names_for_lifetime_elision_hints: self
6824 .inlayHints_lifetimeElisionHints_useParameterNames()
6825 .to_owned(),
6826 max_length: self.inlayHints_maxLength().to_owned(),
6827 // ... snip
6828 }
6829 }
6830 // ... snip
6831 }
6832 "#
6833 }
6834 }
6835 }
6836 }),
6837 )
6838 .await;
6839 }
6840
6841 fn rust_lang() -> Language {
6842 Language::new(
6843 LanguageConfig {
6844 name: "Rust".into(),
6845 matcher: LanguageMatcher {
6846 path_suffixes: vec!["rs".to_string()],
6847 ..Default::default()
6848 },
6849 ..Default::default()
6850 },
6851 Some(tree_sitter_rust::LANGUAGE.into()),
6852 )
6853 .with_highlights_query(
6854 r#"
6855 (field_identifier) @field
6856 (struct_expression) @struct
6857 "#,
6858 )
6859 .unwrap()
6860 .with_injection_query(
6861 r#"
6862 (macro_invocation
6863 (token_tree) @injection.content
6864 (#set! injection.language "rust"))
6865 "#,
6866 )
6867 .unwrap()
6868 }
6869
6870 fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
6871 outline_panel
6872 .active_editor()
6873 .unwrap()
6874 .read(cx)
6875 .buffer()
6876 .read(cx)
6877 .snapshot(cx)
6878 }
6879
6880 fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
6881 editor.update(cx, |editor, cx| {
6882 let selections = editor.selections.all::<language::Point>(cx);
6883 assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
6884 let selection = selections.first().unwrap();
6885 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
6886 let line_start = language::Point::new(selection.start.row, 0);
6887 let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
6888 multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
6889 })
6890 }
6891}