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