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