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