1mod outline_panel_settings;
2
3use std::{
4 cmp,
5 collections::BTreeMap,
6 hash::Hash,
7 ops::Range,
8 path::{MAIN_SEPARATOR_STR, Path, PathBuf},
9 sync::{
10 Arc, OnceLock,
11 atomic::{self, AtomicBool},
12 },
13 time::Duration,
14 u32,
15};
16
17use anyhow::Context as _;
18use collections::{BTreeSet, HashMap, HashSet, hash_map};
19use db::kvp::KEY_VALUE_STORE;
20use editor::{
21 AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorSettings, ExcerptId,
22 ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, ShowScrollbar,
23 display_map::ToDisplayPoint,
24 items::{entry_git_aware_label_color, entry_label_color},
25 scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor, ScrollbarAutoHide},
26};
27use file_icons::FileIcons;
28use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
29use gpui::{
30 Action, AnyElement, App, AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context,
31 DismissEvent, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
32 InteractiveElement, IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
33 MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy,
34 SharedString, Stateful, StatefulInteractiveElement as _, Styled, Subscription, Task,
35 UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, point, px, size,
36 uniform_list,
37};
38use itertools::Itertools;
39use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
40use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};
41
42use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings, ShowIndentGuides};
43use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem};
44use search::{BufferSearchBar, ProjectSearchView};
45use serde::{Deserialize, Serialize};
46use settings::{Settings, SettingsStore};
47use smol::channel;
48use theme::{SyntaxTheme, ThemeSettings};
49use ui::{DynamicSpacing, IndentGuideColors, IndentGuideLayout};
50use util::{RangeExt, ResultExt, TryFutureExt, debug_panic};
51use workspace::{
52 OpenInTerminal, WeakItemHandle, Workspace,
53 dock::{DockPosition, Panel, PanelEvent},
54 item::ItemHandle,
55 searchable::{SearchEvent, SearchableItem},
56 ui::{
57 ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder, HighlightedLabel,
58 Icon, IconButton, IconButtonShape, IconName, IconSize, Label, LabelCommon, ListItem,
59 Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable, Tooltip, h_flex,
60 v_flex,
61 },
62};
63use worktree::{Entry, ProjectEntryId, WorktreeId};
64
65actions!(
66 outline_panel,
67 [
68 CollapseAllEntries,
69 CollapseSelectedEntry,
70 ExpandAllEntries,
71 ExpandSelectedEntry,
72 FoldDirectory,
73 OpenSelectedEntry,
74 RevealInFileManager,
75 SelectParent,
76 ToggleActiveEditorPin,
77 ToggleFocus,
78 UnfoldDirectory,
79 ]
80);
81
82const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
83const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
84
85type Outline = OutlineItem<language::Anchor>;
86type HighlightStyleData = Arc<OnceLock<Vec<(Range<usize>, HighlightStyle)>>>;
87
88pub struct OutlinePanel {
89 fs: Arc<dyn Fs>,
90 width: Option<Pixels>,
91 project: Entity<Project>,
92 workspace: WeakEntity<Workspace>,
93 active: bool,
94 pinned: bool,
95 scroll_handle: UniformListScrollHandle,
96 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
97 focus_handle: FocusHandle,
98 pending_serialization: Task<Option<()>>,
99 fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
100 fs_entries: Vec<FsEntry>,
101 fs_children_count: HashMap<WorktreeId, HashMap<Arc<Path>, FsChildren>>,
102 collapsed_entries: HashSet<CollapsedEntry>,
103 unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
104 selected_entry: SelectedEntry,
105 active_item: Option<ActiveItem>,
106 _subscriptions: Vec<Subscription>,
107 updating_fs_entries: bool,
108 updating_cached_entries: bool,
109 new_entries_for_fs_update: HashSet<ExcerptId>,
110 fs_entries_update_task: Task<()>,
111 cached_entries_update_task: Task<()>,
112 reveal_selection_task: Task<anyhow::Result<()>>,
113 outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
114 excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
115 cached_entries: Vec<CachedEntry>,
116 filter_editor: Entity<Editor>,
117 mode: ItemsDisplayMode,
118 show_scrollbar: bool,
119 vertical_scrollbar_state: ScrollbarState,
120 horizontal_scrollbar_state: ScrollbarState,
121 hide_scrollbar_task: Option<Task<()>>,
122 max_width_item_index: Option<usize>,
123 preserve_selection_on_buffer_fold_toggles: HashSet<BufferId>,
124}
125
126#[derive(Debug)]
127enum ItemsDisplayMode {
128 Search(SearchState),
129 Outline,
130}
131
132#[derive(Debug)]
133struct SearchState {
134 kind: SearchKind,
135 query: String,
136 matches: Vec<(Range<editor::Anchor>, Arc<OnceLock<SearchData>>)>,
137 highlight_search_match_tx: channel::Sender<HighlightArguments>,
138 _search_match_highlighter: Task<()>,
139 _search_match_notify: Task<()>,
140}
141
142struct HighlightArguments {
143 multi_buffer_snapshot: MultiBufferSnapshot,
144 match_range: Range<editor::Anchor>,
145 search_data: Arc<OnceLock<SearchData>>,
146}
147
148impl SearchState {
149 fn new(
150 kind: SearchKind,
151 query: String,
152 previous_matches: HashMap<Range<editor::Anchor>, Arc<OnceLock<SearchData>>>,
153 new_matches: Vec<Range<editor::Anchor>>,
154 theme: Arc<SyntaxTheme>,
155 window: &mut Window,
156 cx: &mut Context<OutlinePanel>,
157 ) -> Self {
158 let (highlight_search_match_tx, highlight_search_match_rx) = channel::unbounded();
159 let (notify_tx, notify_rx) = channel::unbounded::<()>();
160 Self {
161 kind,
162 query,
163 matches: new_matches
164 .into_iter()
165 .map(|range| {
166 let search_data = previous_matches
167 .get(&range)
168 .map(Arc::clone)
169 .unwrap_or_default();
170 (range, search_data)
171 })
172 .collect(),
173 highlight_search_match_tx,
174 _search_match_highlighter: cx.background_spawn(async move {
175 while let Ok(highlight_arguments) = highlight_search_match_rx.recv().await {
176 let needs_init = highlight_arguments.search_data.get().is_none();
177 let search_data = highlight_arguments.search_data.get_or_init(|| {
178 SearchData::new(
179 &highlight_arguments.match_range,
180 &highlight_arguments.multi_buffer_snapshot,
181 )
182 });
183 if needs_init {
184 notify_tx.try_send(()).ok();
185 }
186
187 let highlight_data = &search_data.highlights_data;
188 if highlight_data.get().is_some() {
189 continue;
190 }
191 let mut left_whitespaces_count = 0;
192 let mut non_whitespace_symbol_occurred = false;
193 let context_offset_range = search_data
194 .context_range
195 .to_offset(&highlight_arguments.multi_buffer_snapshot);
196 let mut offset = context_offset_range.start;
197 let mut context_text = String::new();
198 let mut highlight_ranges = Vec::new();
199 for mut chunk in highlight_arguments
200 .multi_buffer_snapshot
201 .chunks(context_offset_range.start..context_offset_range.end, true)
202 {
203 if !non_whitespace_symbol_occurred {
204 for c in chunk.text.chars() {
205 if c.is_whitespace() {
206 left_whitespaces_count += c.len_utf8();
207 } else {
208 non_whitespace_symbol_occurred = true;
209 break;
210 }
211 }
212 }
213
214 if chunk.text.len() > context_offset_range.end - offset {
215 chunk.text = &chunk.text[0..(context_offset_range.end - offset)];
216 offset = context_offset_range.end;
217 } else {
218 offset += chunk.text.len();
219 }
220 let style = chunk
221 .syntax_highlight_id
222 .and_then(|highlight| highlight.style(&theme));
223 if let Some(style) = style {
224 let start = context_text.len();
225 let end = start + chunk.text.len();
226 highlight_ranges.push((start..end, style));
227 }
228 context_text.push_str(chunk.text);
229 if offset >= context_offset_range.end {
230 break;
231 }
232 }
233
234 highlight_ranges.iter_mut().for_each(|(range, _)| {
235 range.start = range.start.saturating_sub(left_whitespaces_count);
236 range.end = range.end.saturating_sub(left_whitespaces_count);
237 });
238 if highlight_data.set(highlight_ranges).ok().is_some() {
239 notify_tx.try_send(()).ok();
240 }
241
242 let trimmed_text = context_text[left_whitespaces_count..].to_owned();
243 debug_assert_eq!(
244 trimmed_text, search_data.context_text,
245 "Highlighted text that does not match the buffer text"
246 );
247 }
248 }),
249 _search_match_notify: cx.spawn_in(window, async move |outline_panel, cx| {
250 loop {
251 match notify_rx.recv().await {
252 Ok(()) => {}
253 Err(_) => break,
254 };
255 while let Ok(()) = notify_rx.try_recv() {
256 //
257 }
258 let update_result = outline_panel.update(cx, |_, cx| {
259 cx.notify();
260 });
261 if update_result.is_err() {
262 break;
263 }
264 }
265 }),
266 }
267 }
268}
269
270#[derive(Debug)]
271enum SelectedEntry {
272 Invalidated(Option<PanelEntry>),
273 Valid(PanelEntry, usize),
274 None,
275}
276
277impl SelectedEntry {
278 fn invalidate(&mut self) {
279 match std::mem::replace(self, SelectedEntry::None) {
280 Self::Valid(entry, _) => *self = Self::Invalidated(Some(entry)),
281 Self::None => *self = Self::Invalidated(None),
282 other => *self = other,
283 }
284 }
285
286 fn is_invalidated(&self) -> bool {
287 matches!(self, Self::Invalidated(_))
288 }
289}
290
291#[derive(Debug, Clone, Copy, Default)]
292struct FsChildren {
293 files: usize,
294 dirs: usize,
295}
296
297impl FsChildren {
298 fn may_be_fold_part(&self) -> bool {
299 self.dirs == 0 || (self.dirs == 1 && self.files == 0)
300 }
301}
302
303#[derive(Clone, Debug)]
304struct CachedEntry {
305 depth: usize,
306 string_match: Option<StringMatch>,
307 entry: PanelEntry,
308}
309
310#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
311enum CollapsedEntry {
312 Dir(WorktreeId, ProjectEntryId),
313 File(WorktreeId, BufferId),
314 ExternalFile(BufferId),
315 Excerpt(BufferId, ExcerptId),
316}
317
318#[derive(Debug)]
319struct Excerpt {
320 range: ExcerptRange<language::Anchor>,
321 outlines: ExcerptOutlines,
322}
323
324impl Excerpt {
325 fn invalidate_outlines(&mut self) {
326 if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines {
327 self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines));
328 }
329 }
330
331 fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {
332 match &self.outlines {
333 ExcerptOutlines::Outlines(outlines) => outlines.iter(),
334 ExcerptOutlines::Invalidated(outlines) => outlines.iter(),
335 ExcerptOutlines::NotFetched => [].iter(),
336 }
337 }
338
339 fn should_fetch_outlines(&self) -> bool {
340 match &self.outlines {
341 ExcerptOutlines::Outlines(_) => false,
342 ExcerptOutlines::Invalidated(_) => true,
343 ExcerptOutlines::NotFetched => true,
344 }
345 }
346}
347
348#[derive(Debug)]
349enum ExcerptOutlines {
350 Outlines(Vec<Outline>),
351 Invalidated(Vec<Outline>),
352 NotFetched,
353}
354
355#[derive(Clone, Debug, PartialEq, Eq)]
356struct FoldedDirsEntry {
357 worktree_id: WorktreeId,
358 entries: Vec<GitEntry>,
359}
360
361// TODO: collapse the inner enums into panel entry
362#[derive(Clone, Debug)]
363enum PanelEntry {
364 Fs(FsEntry),
365 FoldedDirs(FoldedDirsEntry),
366 Outline(OutlineEntry),
367 Search(SearchEntry),
368}
369
370#[derive(Clone, Debug)]
371struct SearchEntry {
372 match_range: Range<editor::Anchor>,
373 kind: SearchKind,
374 render_data: Arc<OnceLock<SearchData>>,
375}
376
377#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
378enum SearchKind {
379 Project,
380 Buffer,
381}
382
383#[derive(Clone, Debug)]
384struct SearchData {
385 context_range: Range<editor::Anchor>,
386 context_text: String,
387 truncated_left: bool,
388 truncated_right: bool,
389 search_match_indices: Vec<Range<usize>>,
390 highlights_data: HighlightStyleData,
391}
392
393impl PartialEq for PanelEntry {
394 fn eq(&self, other: &Self) -> bool {
395 match (self, other) {
396 (Self::Fs(a), Self::Fs(b)) => a == b,
397 (
398 Self::FoldedDirs(FoldedDirsEntry {
399 worktree_id: worktree_id_a,
400 entries: entries_a,
401 }),
402 Self::FoldedDirs(FoldedDirsEntry {
403 worktree_id: worktree_id_b,
404 entries: entries_b,
405 }),
406 ) => worktree_id_a == worktree_id_b && entries_a == entries_b,
407 (Self::Outline(a), Self::Outline(b)) => a == b,
408 (
409 Self::Search(SearchEntry {
410 match_range: match_range_a,
411 kind: kind_a,
412 ..
413 }),
414 Self::Search(SearchEntry {
415 match_range: match_range_b,
416 kind: kind_b,
417 ..
418 }),
419 ) => match_range_a == match_range_b && kind_a == kind_b,
420 _ => false,
421 }
422 }
423}
424
425impl Eq for PanelEntry {}
426
427const SEARCH_MATCH_CONTEXT_SIZE: u32 = 40;
428const TRUNCATED_CONTEXT_MARK: &str = "…";
429
430impl SearchData {
431 fn new(
432 match_range: &Range<editor::Anchor>,
433 multi_buffer_snapshot: &MultiBufferSnapshot,
434 ) -> Self {
435 let match_point_range = match_range.to_point(multi_buffer_snapshot);
436 let context_left_border = multi_buffer_snapshot.clip_point(
437 language::Point::new(
438 match_point_range.start.row,
439 match_point_range
440 .start
441 .column
442 .saturating_sub(SEARCH_MATCH_CONTEXT_SIZE),
443 ),
444 Bias::Left,
445 );
446 let context_right_border = multi_buffer_snapshot.clip_point(
447 language::Point::new(
448 match_point_range.end.row,
449 match_point_range.end.column + SEARCH_MATCH_CONTEXT_SIZE,
450 ),
451 Bias::Right,
452 );
453
454 let context_anchor_range =
455 (context_left_border..context_right_border).to_anchors(multi_buffer_snapshot);
456 let context_offset_range = context_anchor_range.to_offset(multi_buffer_snapshot);
457 let match_offset_range = match_range.to_offset(multi_buffer_snapshot);
458
459 let mut search_match_indices = vec![
460 multi_buffer_snapshot.clip_offset(
461 match_offset_range.start - context_offset_range.start,
462 Bias::Left,
463 )
464 ..multi_buffer_snapshot.clip_offset(
465 match_offset_range.end - context_offset_range.start,
466 Bias::Right,
467 ),
468 ];
469
470 let entire_context_text = multi_buffer_snapshot
471 .text_for_range(context_offset_range.clone())
472 .collect::<String>();
473 let left_whitespaces_offset = entire_context_text
474 .chars()
475 .take_while(|c| c.is_whitespace())
476 .map(|c| c.len_utf8())
477 .sum::<usize>();
478
479 let mut extended_context_left_border = context_left_border;
480 extended_context_left_border.column = extended_context_left_border.column.saturating_sub(1);
481 let extended_context_left_border =
482 multi_buffer_snapshot.clip_point(extended_context_left_border, Bias::Left);
483 let mut extended_context_right_border = context_right_border;
484 extended_context_right_border.column += 1;
485 let extended_context_right_border =
486 multi_buffer_snapshot.clip_point(extended_context_right_border, Bias::Right);
487
488 let truncated_left = left_whitespaces_offset == 0
489 && extended_context_left_border < context_left_border
490 && multi_buffer_snapshot
491 .chars_at(extended_context_left_border)
492 .last()
493 .map_or(false, |c| !c.is_whitespace());
494 let truncated_right = entire_context_text
495 .chars()
496 .last()
497 .map_or(true, |c| !c.is_whitespace())
498 && extended_context_right_border > context_right_border
499 && multi_buffer_snapshot
500 .chars_at(extended_context_right_border)
501 .next()
502 .map_or(false, |c| !c.is_whitespace());
503 search_match_indices.iter_mut().for_each(|range| {
504 range.start = multi_buffer_snapshot.clip_offset(
505 range.start.saturating_sub(left_whitespaces_offset),
506 Bias::Left,
507 );
508 range.end = multi_buffer_snapshot.clip_offset(
509 range.end.saturating_sub(left_whitespaces_offset),
510 Bias::Right,
511 );
512 });
513
514 let trimmed_row_offset_range =
515 context_offset_range.start + left_whitespaces_offset..context_offset_range.end;
516 let trimmed_text = entire_context_text[left_whitespaces_offset..].to_owned();
517 Self {
518 highlights_data: Arc::default(),
519 search_match_indices,
520 context_range: trimmed_row_offset_range.to_anchors(multi_buffer_snapshot),
521 context_text: trimmed_text,
522 truncated_left,
523 truncated_right,
524 }
525 }
526}
527
528#[derive(Clone, Debug, PartialEq, Eq, Hash)]
529struct OutlineEntryExcerpt {
530 id: ExcerptId,
531 buffer_id: BufferId,
532 range: ExcerptRange<language::Anchor>,
533}
534
535#[derive(Clone, Debug, Eq)]
536struct OutlineEntryOutline {
537 buffer_id: BufferId,
538 excerpt_id: ExcerptId,
539 outline: Outline,
540}
541
542impl PartialEq for OutlineEntryOutline {
543 fn eq(&self, other: &Self) -> bool {
544 self.buffer_id == other.buffer_id
545 && self.excerpt_id == other.excerpt_id
546 && self.outline.depth == other.outline.depth
547 && self.outline.range == other.outline.range
548 && self.outline.text == other.outline.text
549 }
550}
551
552impl Hash for OutlineEntryOutline {
553 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
554 (
555 self.buffer_id,
556 self.excerpt_id,
557 self.outline.depth,
558 &self.outline.range,
559 &self.outline.text,
560 )
561 .hash(state);
562 }
563}
564
565#[derive(Clone, Debug, PartialEq, Eq)]
566enum OutlineEntry {
567 Excerpt(OutlineEntryExcerpt),
568 Outline(OutlineEntryOutline),
569}
570
571impl OutlineEntry {
572 fn ids(&self) -> (BufferId, ExcerptId) {
573 match self {
574 OutlineEntry::Excerpt(excerpt) => (excerpt.buffer_id, excerpt.id),
575 OutlineEntry::Outline(outline) => (outline.buffer_id, outline.excerpt_id),
576 }
577 }
578}
579
580#[derive(Debug, Clone, Eq)]
581struct FsEntryFile {
582 worktree_id: WorktreeId,
583 entry: GitEntry,
584 buffer_id: BufferId,
585 excerpts: Vec<ExcerptId>,
586}
587
588impl PartialEq for FsEntryFile {
589 fn eq(&self, other: &Self) -> bool {
590 self.worktree_id == other.worktree_id
591 && self.entry.id == other.entry.id
592 && self.buffer_id == other.buffer_id
593 }
594}
595
596impl Hash for FsEntryFile {
597 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
598 (self.buffer_id, self.entry.id, self.worktree_id).hash(state);
599 }
600}
601
602#[derive(Debug, Clone, Eq)]
603struct FsEntryDirectory {
604 worktree_id: WorktreeId,
605 entry: GitEntry,
606}
607
608impl PartialEq for FsEntryDirectory {
609 fn eq(&self, other: &Self) -> bool {
610 self.worktree_id == other.worktree_id && self.entry.id == other.entry.id
611 }
612}
613
614impl Hash for FsEntryDirectory {
615 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
616 (self.worktree_id, self.entry.id).hash(state);
617 }
618}
619
620#[derive(Debug, Clone, Eq)]
621struct FsEntryExternalFile {
622 buffer_id: BufferId,
623 excerpts: Vec<ExcerptId>,
624}
625
626impl PartialEq for FsEntryExternalFile {
627 fn eq(&self, other: &Self) -> bool {
628 self.buffer_id == other.buffer_id
629 }
630}
631
632impl Hash for FsEntryExternalFile {
633 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
634 self.buffer_id.hash(state);
635 }
636}
637
638#[derive(Clone, Debug, Eq, PartialEq)]
639enum FsEntry {
640 ExternalFile(FsEntryExternalFile),
641 Directory(FsEntryDirectory),
642 File(FsEntryFile),
643}
644
645struct ActiveItem {
646 item_handle: Box<dyn WeakItemHandle>,
647 active_editor: WeakEntity<Editor>,
648 _buffer_search_subscription: Subscription,
649 _editor_subscription: Subscription,
650}
651
652#[derive(Debug)]
653pub enum Event {
654 Focus,
655}
656
657#[derive(Serialize, Deserialize)]
658struct SerializedOutlinePanel {
659 width: Option<Pixels>,
660 active: Option<bool>,
661}
662
663pub fn init_settings(cx: &mut App) {
664 OutlinePanelSettings::register(cx);
665}
666
667pub fn init(cx: &mut App) {
668 init_settings(cx);
669
670 cx.observe_new(|workspace: &mut Workspace, _, _| {
671 workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
672 workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
673 });
674 })
675 .detach();
676}
677
678impl OutlinePanel {
679 pub async fn load(
680 workspace: WeakEntity<Workspace>,
681 mut cx: AsyncWindowContext,
682 ) -> anyhow::Result<Entity<Self>> {
683 let serialized_panel = match workspace
684 .read_with(&cx, |workspace, _| {
685 OutlinePanel::serialization_key(workspace)
686 })
687 .ok()
688 .flatten()
689 {
690 Some(serialization_key) => cx
691 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })
692 .await
693 .context("loading outline panel")
694 .log_err()
695 .flatten()
696 .map(|panel| serde_json::from_str::<SerializedOutlinePanel>(&panel))
697 .transpose()
698 .log_err()
699 .flatten(),
700 None => None,
701 };
702
703 workspace.update_in(&mut cx, |workspace, window, cx| {
704 let panel = Self::new(workspace, window, cx);
705 if let Some(serialized_panel) = serialized_panel {
706 panel.update(cx, |panel, cx| {
707 panel.width = serialized_panel.width.map(|px| px.round());
708 panel.active = serialized_panel.active.unwrap_or(false);
709 cx.notify();
710 });
711 }
712 panel
713 })
714 }
715
716 fn new(
717 workspace: &mut Workspace,
718 window: &mut Window,
719 cx: &mut Context<Workspace>,
720 ) -> Entity<Self> {
721 let project = workspace.project().clone();
722 let workspace_handle = cx.entity().downgrade();
723 let outline_panel = cx.new(|cx| {
724 let filter_editor = cx.new(|cx| {
725 let mut editor = Editor::single_line(window, cx);
726 editor.set_placeholder_text("Filter...", cx);
727 editor
728 });
729 let filter_update_subscription = cx.subscribe_in(
730 &filter_editor,
731 window,
732 |outline_panel: &mut Self, _, event, window, cx| {
733 if let editor::EditorEvent::BufferEdited = event {
734 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
735 }
736 },
737 );
738
739 let focus_handle = cx.focus_handle();
740 let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in);
741 let focus_out_subscription =
742 cx.on_focus_out(&focus_handle, window, |outline_panel, _, window, cx| {
743 outline_panel.hide_scrollbar(window, cx);
744 });
745 let workspace_subscription = cx.subscribe_in(
746 &workspace
747 .weak_handle()
748 .upgrade()
749 .expect("have a &mut Workspace"),
750 window,
751 move |outline_panel, workspace, event, window, cx| {
752 if let workspace::Event::ActiveItemChanged = event {
753 if let Some((new_active_item, new_active_editor)) =
754 workspace_active_editor(workspace.read(cx), cx)
755 {
756 if outline_panel.should_replace_active_item(new_active_item.as_ref()) {
757 outline_panel.replace_active_editor(
758 new_active_item,
759 new_active_editor,
760 window,
761 cx,
762 );
763 }
764 } else {
765 outline_panel.clear_previous(window, cx);
766 cx.notify();
767 }
768 }
769 },
770 );
771
772 let icons_subscription = cx.observe_global::<FileIcons>(|_, cx| {
773 cx.notify();
774 });
775
776 let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx);
777 let mut current_theme = ThemeSettings::get_global(cx).clone();
778 let settings_subscription =
779 cx.observe_global_in::<SettingsStore>(window, move |outline_panel, window, cx| {
780 let new_settings = OutlinePanelSettings::get_global(cx);
781 let new_theme = ThemeSettings::get_global(cx);
782 if ¤t_theme != new_theme {
783 outline_panel_settings = *new_settings;
784 current_theme = new_theme.clone();
785 for excerpts in outline_panel.excerpts.values_mut() {
786 for excerpt in excerpts.values_mut() {
787 excerpt.invalidate_outlines();
788 }
789 }
790 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
791 if update_cached_items {
792 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
793 }
794 } else if &outline_panel_settings != new_settings {
795 outline_panel_settings = *new_settings;
796 cx.notify();
797 }
798 });
799
800 let scroll_handle = UniformListScrollHandle::new();
801
802 let mut outline_panel = Self {
803 mode: ItemsDisplayMode::Outline,
804 active: false,
805 pinned: false,
806 workspace: workspace_handle,
807 project,
808 fs: workspace.app_state().fs.clone(),
809 show_scrollbar: !Self::should_autohide_scrollbar(cx),
810 hide_scrollbar_task: None,
811 vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
812 .parent_entity(&cx.entity()),
813 horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
814 .parent_entity(&cx.entity()),
815 max_width_item_index: None,
816 scroll_handle,
817 focus_handle,
818 filter_editor,
819 fs_entries: Vec::new(),
820 fs_entries_depth: HashMap::default(),
821 fs_children_count: HashMap::default(),
822 collapsed_entries: HashSet::default(),
823 unfolded_dirs: HashMap::default(),
824 selected_entry: SelectedEntry::None,
825 context_menu: None,
826 width: None,
827 active_item: None,
828 pending_serialization: Task::ready(None),
829 updating_fs_entries: false,
830 updating_cached_entries: false,
831 new_entries_for_fs_update: HashSet::default(),
832 preserve_selection_on_buffer_fold_toggles: HashSet::default(),
833 fs_entries_update_task: Task::ready(()),
834 cached_entries_update_task: Task::ready(()),
835 reveal_selection_task: Task::ready(Ok(())),
836 outline_fetch_tasks: HashMap::default(),
837 excerpts: HashMap::default(),
838 cached_entries: Vec::new(),
839 _subscriptions: vec![
840 settings_subscription,
841 icons_subscription,
842 focus_subscription,
843 focus_out_subscription,
844 workspace_subscription,
845 filter_update_subscription,
846 ],
847 };
848 if let Some((item, editor)) = workspace_active_editor(workspace, cx) {
849 outline_panel.replace_active_editor(item, editor, window, cx);
850 }
851 outline_panel
852 });
853
854 outline_panel
855 }
856
857 fn serialization_key(workspace: &Workspace) -> Option<String> {
858 workspace
859 .database_id()
860 .map(|id| i64::from(id).to_string())
861 .or(workspace.session_id())
862 .map(|id| format!("{}-{:?}", OUTLINE_PANEL_KEY, id))
863 }
864
865 fn serialize(&mut self, cx: &mut Context<Self>) {
866 let Some(serialization_key) = self
867 .workspace
868 .read_with(cx, |workspace, _| {
869 OutlinePanel::serialization_key(workspace)
870 })
871 .ok()
872 .flatten()
873 else {
874 return;
875 };
876 let width = self.width;
877 let active = Some(self.active);
878 self.pending_serialization = cx.background_spawn(
879 async move {
880 KEY_VALUE_STORE
881 .write_kvp(
882 serialization_key,
883 serde_json::to_string(&SerializedOutlinePanel { width, active })?,
884 )
885 .await?;
886 anyhow::Ok(())
887 }
888 .log_err(),
889 );
890 }
891
892 fn dispatch_context(&self, window: &mut Window, cx: &mut Context<Self>) -> KeyContext {
893 let mut dispatch_context = KeyContext::new_with_defaults();
894 dispatch_context.add("OutlinePanel");
895 dispatch_context.add("menu");
896 let identifier = if self.filter_editor.focus_handle(cx).is_focused(window) {
897 "editing"
898 } else {
899 "not_editing"
900 };
901 dispatch_context.add(identifier);
902 dispatch_context
903 }
904
905 fn unfold_directory(
906 &mut self,
907 _: &UnfoldDirectory,
908 window: &mut Window,
909 cx: &mut Context<Self>,
910 ) {
911 if let Some(PanelEntry::FoldedDirs(FoldedDirsEntry {
912 worktree_id,
913 entries,
914 ..
915 })) = self.selected_entry().cloned()
916 {
917 self.unfolded_dirs
918 .entry(worktree_id)
919 .or_default()
920 .extend(entries.iter().map(|entry| entry.id));
921 self.update_cached_entries(None, window, cx);
922 }
923 }
924
925 fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
926 let (worktree_id, entry) = match self.selected_entry().cloned() {
927 Some(PanelEntry::Fs(FsEntry::Directory(directory))) => {
928 (directory.worktree_id, Some(directory.entry))
929 }
930 Some(PanelEntry::FoldedDirs(folded_dirs)) => {
931 (folded_dirs.worktree_id, folded_dirs.entries.last().cloned())
932 }
933 _ => return,
934 };
935 let Some(entry) = entry else {
936 return;
937 };
938 let unfolded_dirs = self.unfolded_dirs.get_mut(&worktree_id);
939 let worktree = self
940 .project
941 .read(cx)
942 .worktree_for_id(worktree_id, cx)
943 .map(|w| w.read(cx).snapshot());
944 let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
945 return;
946 };
947
948 unfolded_dirs.remove(&entry.id);
949 self.update_cached_entries(None, window, cx);
950 }
951
952 fn open_selected_entry(
953 &mut self,
954 _: &OpenSelectedEntry,
955 window: &mut Window,
956 cx: &mut Context<Self>,
957 ) {
958 if self.filter_editor.focus_handle(cx).is_focused(window) {
959 cx.propagate()
960 } else if let Some(selected_entry) = self.selected_entry().cloned() {
961 self.toggle_expanded(&selected_entry, window, cx);
962 self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
963 }
964 }
965
966 fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
967 if self.filter_editor.focus_handle(cx).is_focused(window) {
968 self.focus_handle.focus(window);
969 } else {
970 self.filter_editor.focus_handle(cx).focus(window);
971 }
972
973 if self.context_menu.is_some() {
974 self.context_menu.take();
975 cx.notify();
976 }
977 }
978
979 fn open_excerpts(
980 &mut self,
981 action: &editor::OpenExcerpts,
982 window: &mut Window,
983 cx: &mut Context<Self>,
984 ) {
985 if self.filter_editor.focus_handle(cx).is_focused(window) {
986 cx.propagate()
987 } else if let Some((active_editor, selected_entry)) =
988 self.active_editor().zip(self.selected_entry().cloned())
989 {
990 self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
991 active_editor.update(cx, |editor, cx| editor.open_excerpts(action, window, cx));
992 }
993 }
994
995 fn open_excerpts_split(
996 &mut self,
997 action: &editor::OpenExcerptsSplit,
998 window: &mut Window,
999 cx: &mut Context<Self>,
1000 ) {
1001 if self.filter_editor.focus_handle(cx).is_focused(window) {
1002 cx.propagate()
1003 } else if let Some((active_editor, selected_entry)) =
1004 self.active_editor().zip(self.selected_entry().cloned())
1005 {
1006 self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
1007 active_editor.update(cx, |editor, cx| {
1008 editor.open_excerpts_in_split(action, window, cx)
1009 });
1010 }
1011 }
1012
1013 fn scroll_editor_to_entry(
1014 &mut self,
1015 entry: &PanelEntry,
1016 prefer_selection_change: bool,
1017 prefer_focus_change: bool,
1018 window: &mut Window,
1019 cx: &mut Context<OutlinePanel>,
1020 ) {
1021 let Some(active_editor) = self.active_editor() else {
1022 return;
1023 };
1024 let active_multi_buffer = active_editor.read(cx).buffer().clone();
1025 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
1026 let mut change_selection = prefer_selection_change;
1027 let mut change_focus = prefer_focus_change;
1028 let mut scroll_to_buffer = None;
1029 let scroll_target = match entry {
1030 PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => {
1031 change_focus = false;
1032 None
1033 }
1034 PanelEntry::Fs(FsEntry::ExternalFile(file)) => {
1035 change_selection = false;
1036 scroll_to_buffer = Some(file.buffer_id);
1037 multi_buffer_snapshot.excerpts().find_map(
1038 |(excerpt_id, buffer_snapshot, excerpt_range)| {
1039 if buffer_snapshot.remote_id() == file.buffer_id {
1040 multi_buffer_snapshot
1041 .anchor_in_excerpt(excerpt_id, excerpt_range.context.start)
1042 } else {
1043 None
1044 }
1045 },
1046 )
1047 }
1048
1049 PanelEntry::Fs(FsEntry::File(file)) => {
1050 change_selection = false;
1051 scroll_to_buffer = Some(file.buffer_id);
1052 self.project
1053 .update(cx, |project, cx| {
1054 project
1055 .path_for_entry(file.entry.id, cx)
1056 .and_then(|path| project.get_open_buffer(&path, cx))
1057 })
1058 .map(|buffer| {
1059 active_multi_buffer
1060 .read(cx)
1061 .excerpts_for_buffer(buffer.read(cx).remote_id(), cx)
1062 })
1063 .and_then(|excerpts| {
1064 let (excerpt_id, excerpt_range) = excerpts.first()?;
1065 multi_buffer_snapshot
1066 .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
1067 })
1068 }
1069 PanelEntry::Outline(OutlineEntry::Outline(outline)) => multi_buffer_snapshot
1070 .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.start)
1071 .or_else(|| {
1072 multi_buffer_snapshot
1073 .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.end)
1074 }),
1075 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1076 change_selection = false;
1077 change_focus = false;
1078 multi_buffer_snapshot.anchor_in_excerpt(excerpt.id, excerpt.range.context.start)
1079 }
1080 PanelEntry::Search(search_entry) => Some(search_entry.match_range.start),
1081 };
1082
1083 if let Some(anchor) = scroll_target {
1084 let activate = self
1085 .workspace
1086 .update(cx, |workspace, cx| match self.active_item() {
1087 Some(active_item) => workspace.activate_item(
1088 active_item.as_ref(),
1089 true,
1090 change_focus,
1091 window,
1092 cx,
1093 ),
1094 None => workspace.activate_item(&active_editor, true, change_focus, window, cx),
1095 });
1096
1097 if activate.is_ok() {
1098 self.select_entry(entry.clone(), true, window, cx);
1099 if change_selection {
1100 active_editor.update(cx, |editor, cx| {
1101 editor.change_selections(
1102 Some(Autoscroll::Strategy(AutoscrollStrategy::Center, None)),
1103 window,
1104 cx,
1105 |s| s.select_ranges(Some(anchor..anchor)),
1106 );
1107 });
1108 } else {
1109 let mut offset = Point::default();
1110 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 Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| {
4339 div()
4340 .occlude()
4341 .id("project-panel-horizontal-scroll")
4342 .on_mouse_move(cx.listener(|_, _, _, cx| {
4343 cx.notify();
4344 cx.stop_propagation()
4345 }))
4346 .on_hover(|_, _, cx| {
4347 cx.stop_propagation();
4348 })
4349 .on_any_mouse_down(|_, _, cx| {
4350 cx.stop_propagation();
4351 })
4352 .on_mouse_up(
4353 MouseButton::Left,
4354 cx.listener(|outline_panel, _, window, cx| {
4355 if !outline_panel.horizontal_scrollbar_state.is_dragging()
4356 && !outline_panel.focus_handle.contains_focused(window, cx)
4357 {
4358 outline_panel.hide_scrollbar(window, cx);
4359 cx.notify();
4360 }
4361
4362 cx.stop_propagation();
4363 }),
4364 )
4365 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4366 cx.notify();
4367 }))
4368 .w_full()
4369 .absolute()
4370 .right_1()
4371 .left_1()
4372 .bottom_0()
4373 .h(px(12.))
4374 .cursor_default()
4375 .child(scrollbar)
4376 })
4377 }
4378
4379 fn should_show_scrollbar(cx: &App) -> bool {
4380 let show = OutlinePanelSettings::get_global(cx)
4381 .scrollbar
4382 .show
4383 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4384 match show {
4385 ShowScrollbar::Auto => true,
4386 ShowScrollbar::System => true,
4387 ShowScrollbar::Always => true,
4388 ShowScrollbar::Never => false,
4389 }
4390 }
4391
4392 fn should_autohide_scrollbar(cx: &App) -> bool {
4393 let show = OutlinePanelSettings::get_global(cx)
4394 .scrollbar
4395 .show
4396 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4397 match show {
4398 ShowScrollbar::Auto => true,
4399 ShowScrollbar::System => cx
4400 .try_global::<ScrollbarAutoHide>()
4401 .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4402 ShowScrollbar::Always => false,
4403 ShowScrollbar::Never => true,
4404 }
4405 }
4406
4407 fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4408 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4409 if !Self::should_autohide_scrollbar(cx) {
4410 return;
4411 }
4412 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4413 cx.background_executor()
4414 .timer(SCROLLBAR_SHOW_INTERVAL)
4415 .await;
4416 panel
4417 .update(cx, |panel, cx| {
4418 panel.show_scrollbar = false;
4419 cx.notify();
4420 })
4421 .log_err();
4422 }))
4423 }
4424
4425 fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
4426 let item_text_chars = match entry {
4427 PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
4428 .buffer_snapshot_for_id(external.buffer_id, cx)
4429 .and_then(|snapshot| {
4430 Some(snapshot.file()?.path().file_name()?.to_string_lossy().len())
4431 })
4432 .unwrap_or_default(),
4433 PanelEntry::Fs(FsEntry::Directory(directory)) => directory
4434 .entry
4435 .path
4436 .file_name()
4437 .map(|name| name.to_string_lossy().len())
4438 .unwrap_or_default(),
4439 PanelEntry::Fs(FsEntry::File(file)) => file
4440 .entry
4441 .path
4442 .file_name()
4443 .map(|name| name.to_string_lossy().len())
4444 .unwrap_or_default(),
4445 PanelEntry::FoldedDirs(folded_dirs) => {
4446 folded_dirs
4447 .entries
4448 .iter()
4449 .map(|dir| {
4450 dir.path
4451 .file_name()
4452 .map(|name| name.to_string_lossy().len())
4453 .unwrap_or_default()
4454 })
4455 .sum::<usize>()
4456 + folded_dirs.entries.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len()
4457 }
4458 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
4459 .excerpt_label(excerpt.buffer_id, &excerpt.range, cx)
4460 .map(|label| label.len())
4461 .unwrap_or_default(),
4462 PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(),
4463 PanelEntry::Search(search) => search
4464 .render_data
4465 .get()
4466 .map(|data| data.context_text.len())
4467 .unwrap_or_default(),
4468 };
4469
4470 (item_text_chars + depth) as u64
4471 }
4472
4473 fn render_main_contents(
4474 &mut self,
4475 query: Option<String>,
4476 show_indent_guides: bool,
4477 indent_size: f32,
4478 window: &mut Window,
4479 cx: &mut Context<Self>,
4480 ) -> Div {
4481 let contents = if self.cached_entries.is_empty() {
4482 let header = if self.updating_fs_entries || self.updating_cached_entries {
4483 None
4484 } else if query.is_some() {
4485 Some("No matches for query")
4486 } else {
4487 Some("No outlines available")
4488 };
4489
4490 v_flex()
4491 .flex_1()
4492 .justify_center()
4493 .size_full()
4494 .when_some(header, |panel, header| {
4495 panel
4496 .child(h_flex().justify_center().child(Label::new(header)))
4497 .when_some(query.clone(), |panel, query| {
4498 panel.child(h_flex().justify_center().child(Label::new(query)))
4499 })
4500 .child(
4501 h_flex()
4502 .pt(DynamicSpacing::Base04.rems(cx))
4503 .justify_center()
4504 .child({
4505 let keystroke =
4506 match self.position(window, cx) {
4507 DockPosition::Left => window
4508 .keystroke_text_for(&workspace::ToggleLeftDock),
4509 DockPosition::Bottom => window
4510 .keystroke_text_for(&workspace::ToggleBottomDock),
4511 DockPosition::Right => window
4512 .keystroke_text_for(&workspace::ToggleRightDock),
4513 };
4514 Label::new(format!("Toggle this panel with {keystroke}"))
4515 }),
4516 )
4517 })
4518 } else {
4519 let list_contents = {
4520 let items_len = self.cached_entries.len();
4521 let multi_buffer_snapshot = self
4522 .active_editor()
4523 .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
4524 uniform_list(cx.entity().clone(), "entries", items_len, {
4525 move |outline_panel, range, window, cx| {
4526 let entries = outline_panel.cached_entries.get(range);
4527 entries
4528 .map(|entries| entries.to_vec())
4529 .unwrap_or_default()
4530 .into_iter()
4531 .filter_map(|cached_entry| match cached_entry.entry {
4532 PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4533 &entry,
4534 cached_entry.depth,
4535 cached_entry.string_match.as_ref(),
4536 window,
4537 cx,
4538 )),
4539 PanelEntry::FoldedDirs(folded_dirs_entry) => {
4540 Some(outline_panel.render_folded_dirs(
4541 &folded_dirs_entry,
4542 cached_entry.depth,
4543 cached_entry.string_match.as_ref(),
4544 window,
4545 cx,
4546 ))
4547 }
4548 PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4549 outline_panel.render_excerpt(
4550 &excerpt,
4551 cached_entry.depth,
4552 window,
4553 cx,
4554 )
4555 }
4556 PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4557 Some(outline_panel.render_outline(
4558 &entry,
4559 cached_entry.depth,
4560 cached_entry.string_match.as_ref(),
4561 window,
4562 cx,
4563 ))
4564 }
4565 PanelEntry::Search(SearchEntry {
4566 match_range,
4567 render_data,
4568 kind,
4569 ..
4570 }) => outline_panel.render_search_match(
4571 multi_buffer_snapshot.as_ref(),
4572 &match_range,
4573 &render_data,
4574 kind,
4575 cached_entry.depth,
4576 cached_entry.string_match.as_ref(),
4577 window,
4578 cx,
4579 ),
4580 })
4581 .collect()
4582 }
4583 })
4584 .with_sizing_behavior(ListSizingBehavior::Infer)
4585 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4586 .with_width_from_item(self.max_width_item_index)
4587 .track_scroll(self.scroll_handle.clone())
4588 .when(show_indent_guides, |list| {
4589 list.with_decoration(
4590 ui::indent_guides(
4591 cx.entity().clone(),
4592 px(indent_size),
4593 IndentGuideColors::panel(cx),
4594 |outline_panel, range, _, _| {
4595 let entries = outline_panel.cached_entries.get(range);
4596 if let Some(entries) = entries {
4597 entries.into_iter().map(|item| item.depth).collect()
4598 } else {
4599 smallvec::SmallVec::new()
4600 }
4601 },
4602 )
4603 .with_render_fn(
4604 cx.entity().clone(),
4605 move |outline_panel, params, _, _| {
4606 const LEFT_OFFSET: Pixels = px(14.);
4607
4608 let indent_size = params.indent_size;
4609 let item_height = params.item_height;
4610 let active_indent_guide_ix = find_active_indent_guide_ix(
4611 outline_panel,
4612 ¶ms.indent_guides,
4613 );
4614
4615 params
4616 .indent_guides
4617 .into_iter()
4618 .enumerate()
4619 .map(|(ix, layout)| {
4620 let bounds = Bounds::new(
4621 point(
4622 layout.offset.x * indent_size + LEFT_OFFSET,
4623 layout.offset.y * item_height,
4624 ),
4625 size(px(1.), layout.length * item_height),
4626 );
4627 ui::RenderedIndentGuide {
4628 bounds,
4629 layout,
4630 is_active: active_indent_guide_ix == Some(ix),
4631 hitbox: None,
4632 }
4633 })
4634 .collect()
4635 },
4636 ),
4637 )
4638 })
4639 };
4640
4641 v_flex()
4642 .flex_shrink()
4643 .size_full()
4644 .child(list_contents.size_full().flex_shrink())
4645 .children(self.render_vertical_scrollbar(cx))
4646 .when_some(
4647 self.render_horizontal_scrollbar(window, cx),
4648 |this, scrollbar| this.pb_4().child(scrollbar),
4649 )
4650 }
4651 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4652 deferred(
4653 anchored()
4654 .position(*position)
4655 .anchor(gpui::Corner::TopLeft)
4656 .child(menu.clone()),
4657 )
4658 .with_priority(1)
4659 }));
4660
4661 v_flex().w_full().flex_1().overflow_hidden().child(contents)
4662 }
4663
4664 fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4665 v_flex().flex_none().child(horizontal_separator(cx)).child(
4666 h_flex()
4667 .p_2()
4668 .w_full()
4669 .child(self.filter_editor.clone())
4670 .child(
4671 div().child(
4672 IconButton::new(
4673 "outline-panel-menu",
4674 if pinned {
4675 IconName::Unpin
4676 } else {
4677 IconName::Pin
4678 },
4679 )
4680 .tooltip(Tooltip::text(if pinned {
4681 "Unpin Outline"
4682 } else {
4683 "Pin Active Outline"
4684 }))
4685 .shape(IconButtonShape::Square)
4686 .on_click(cx.listener(
4687 |outline_panel, _, window, cx| {
4688 outline_panel.toggle_active_editor_pin(
4689 &ToggleActiveEditorPin,
4690 window,
4691 cx,
4692 );
4693 },
4694 )),
4695 ),
4696 ),
4697 )
4698 }
4699
4700 fn buffers_inside_directory(
4701 &self,
4702 dir_worktree: WorktreeId,
4703 dir_entry: &GitEntry,
4704 ) -> HashSet<BufferId> {
4705 if !dir_entry.is_dir() {
4706 debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4707 return HashSet::default();
4708 }
4709
4710 self.fs_entries
4711 .iter()
4712 .skip_while(|fs_entry| match fs_entry {
4713 FsEntry::Directory(directory) => {
4714 directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4715 }
4716 _ => true,
4717 })
4718 .skip(1)
4719 .take_while(|fs_entry| match fs_entry {
4720 FsEntry::ExternalFile(..) => false,
4721 FsEntry::Directory(directory) => {
4722 directory.worktree_id == dir_worktree
4723 && directory.entry.path.starts_with(&dir_entry.path)
4724 }
4725 FsEntry::File(file) => {
4726 file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4727 }
4728 })
4729 .filter_map(|fs_entry| match fs_entry {
4730 FsEntry::File(file) => Some(file.buffer_id),
4731 _ => None,
4732 })
4733 .collect()
4734 }
4735}
4736
4737fn workspace_active_editor(
4738 workspace: &Workspace,
4739 cx: &App,
4740) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4741 let active_item = workspace.active_item(cx)?;
4742 let active_editor = active_item
4743 .act_as::<Editor>(cx)
4744 .filter(|editor| editor.read(cx).mode().is_full())?;
4745 Some((active_item, active_editor))
4746}
4747
4748fn back_to_common_visited_parent(
4749 visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
4750 worktree_id: &WorktreeId,
4751 new_entry: &Entry,
4752) -> Option<(WorktreeId, ProjectEntryId)> {
4753 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4754 match new_entry.path.parent() {
4755 Some(parent_path) => {
4756 if parent_path == visited_path.as_ref() {
4757 return Some((*worktree_id, *visited_dir_id));
4758 }
4759 }
4760 None => {
4761 break;
4762 }
4763 }
4764 visited_dirs.pop();
4765 }
4766 None
4767}
4768
4769fn file_name(path: &Path) -> String {
4770 let mut current_path = path;
4771 loop {
4772 if let Some(file_name) = current_path.file_name() {
4773 return file_name.to_string_lossy().into_owned();
4774 }
4775 match current_path.parent() {
4776 Some(parent) => current_path = parent,
4777 None => return path.to_string_lossy().into_owned(),
4778 }
4779 }
4780}
4781
4782impl Panel for OutlinePanel {
4783 fn persistent_name() -> &'static str {
4784 "Outline Panel"
4785 }
4786
4787 fn position(&self, _: &Window, cx: &App) -> DockPosition {
4788 match OutlinePanelSettings::get_global(cx).dock {
4789 OutlinePanelDockPosition::Left => DockPosition::Left,
4790 OutlinePanelDockPosition::Right => DockPosition::Right,
4791 }
4792 }
4793
4794 fn position_is_valid(&self, position: DockPosition) -> bool {
4795 matches!(position, DockPosition::Left | DockPosition::Right)
4796 }
4797
4798 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4799 settings::update_settings_file::<OutlinePanelSettings>(
4800 self.fs.clone(),
4801 cx,
4802 move |settings, _| {
4803 let dock = match position {
4804 DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
4805 DockPosition::Right => OutlinePanelDockPosition::Right,
4806 };
4807 settings.dock = Some(dock);
4808 },
4809 );
4810 }
4811
4812 fn size(&self, _: &Window, cx: &App) -> Pixels {
4813 self.width
4814 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
4815 }
4816
4817 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
4818 self.width = size;
4819 cx.notify();
4820 cx.defer_in(window, |this, _, cx| {
4821 this.serialize(cx);
4822 });
4823 }
4824
4825 fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4826 OutlinePanelSettings::get_global(cx)
4827 .button
4828 .then_some(IconName::ListTree)
4829 }
4830
4831 fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
4832 Some("Outline Panel")
4833 }
4834
4835 fn toggle_action(&self) -> Box<dyn Action> {
4836 Box::new(ToggleFocus)
4837 }
4838
4839 fn starts_open(&self, _window: &Window, _: &App) -> bool {
4840 self.active
4841 }
4842
4843 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
4844 cx.spawn_in(window, async move |outline_panel, cx| {
4845 outline_panel
4846 .update_in(cx, |outline_panel, window, cx| {
4847 let old_active = outline_panel.active;
4848 outline_panel.active = active;
4849 if old_active != active {
4850 if active {
4851 if let Some((active_item, active_editor)) =
4852 outline_panel.workspace.upgrade().and_then(|workspace| {
4853 workspace_active_editor(workspace.read(cx), cx)
4854 })
4855 {
4856 if outline_panel.should_replace_active_item(active_item.as_ref()) {
4857 outline_panel.replace_active_editor(
4858 active_item,
4859 active_editor,
4860 window,
4861 cx,
4862 );
4863 } else {
4864 outline_panel.update_fs_entries(active_editor, None, window, cx)
4865 }
4866 return;
4867 }
4868 }
4869
4870 if !outline_panel.pinned {
4871 outline_panel.clear_previous(window, cx);
4872 }
4873 }
4874 outline_panel.serialize(cx);
4875 })
4876 .ok();
4877 })
4878 .detach()
4879 }
4880
4881 fn activation_priority(&self) -> u32 {
4882 5
4883 }
4884}
4885
4886impl Focusable for OutlinePanel {
4887 fn focus_handle(&self, cx: &App) -> FocusHandle {
4888 self.filter_editor.focus_handle(cx).clone()
4889 }
4890}
4891
4892impl EventEmitter<Event> for OutlinePanel {}
4893
4894impl EventEmitter<PanelEvent> for OutlinePanel {}
4895
4896impl Render for OutlinePanel {
4897 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4898 let (is_local, is_via_ssh) = self
4899 .project
4900 .read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
4901 let query = self.query(cx);
4902 let pinned = self.pinned;
4903 let settings = OutlinePanelSettings::get_global(cx);
4904 let indent_size = settings.indent_size;
4905 let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
4906
4907 let search_query = match &self.mode {
4908 ItemsDisplayMode::Search(search_query) => Some(search_query),
4909 _ => None,
4910 };
4911
4912 v_flex()
4913 .id("outline-panel")
4914 .size_full()
4915 .overflow_hidden()
4916 .relative()
4917 .on_hover(cx.listener(|this, hovered, window, cx| {
4918 if *hovered {
4919 this.show_scrollbar = true;
4920 this.hide_scrollbar_task.take();
4921 cx.notify();
4922 } else if !this.focus_handle.contains_focused(window, cx) {
4923 this.hide_scrollbar(window, cx);
4924 }
4925 }))
4926 .key_context(self.dispatch_context(window, cx))
4927 .on_action(cx.listener(Self::open_selected_entry))
4928 .on_action(cx.listener(Self::cancel))
4929 .on_action(cx.listener(Self::select_next))
4930 .on_action(cx.listener(Self::select_previous))
4931 .on_action(cx.listener(Self::select_first))
4932 .on_action(cx.listener(Self::select_last))
4933 .on_action(cx.listener(Self::select_parent))
4934 .on_action(cx.listener(Self::expand_selected_entry))
4935 .on_action(cx.listener(Self::collapse_selected_entry))
4936 .on_action(cx.listener(Self::expand_all_entries))
4937 .on_action(cx.listener(Self::collapse_all_entries))
4938 .on_action(cx.listener(Self::copy_path))
4939 .on_action(cx.listener(Self::copy_relative_path))
4940 .on_action(cx.listener(Self::toggle_active_editor_pin))
4941 .on_action(cx.listener(Self::unfold_directory))
4942 .on_action(cx.listener(Self::fold_directory))
4943 .on_action(cx.listener(Self::open_excerpts))
4944 .on_action(cx.listener(Self::open_excerpts_split))
4945 .when(is_local, |el| {
4946 el.on_action(cx.listener(Self::reveal_in_finder))
4947 })
4948 .when(is_local || is_via_ssh, |el| {
4949 el.on_action(cx.listener(Self::open_in_terminal))
4950 })
4951 .on_mouse_down(
4952 MouseButton::Right,
4953 cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
4954 if let Some(entry) = outline_panel.selected_entry().cloned() {
4955 outline_panel.deploy_context_menu(event.position, entry, window, cx)
4956 } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
4957 outline_panel.deploy_context_menu(
4958 event.position,
4959 PanelEntry::Fs(entry),
4960 window,
4961 cx,
4962 )
4963 }
4964 }),
4965 )
4966 .track_focus(&self.focus_handle)
4967 .when_some(search_query, |outline_panel, search_state| {
4968 outline_panel.child(
4969 h_flex()
4970 .py_1p5()
4971 .px_2()
4972 .h(DynamicSpacing::Base32.px(cx))
4973 .flex_shrink_0()
4974 .border_b_1()
4975 .border_color(cx.theme().colors().border)
4976 .gap_0p5()
4977 .child(Label::new("Searching:").color(Color::Muted))
4978 .child(Label::new(search_state.query.to_string())),
4979 )
4980 })
4981 .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
4982 .child(self.render_filter_footer(pinned, cx))
4983 }
4984}
4985
4986fn find_active_indent_guide_ix(
4987 outline_panel: &OutlinePanel,
4988 candidates: &[IndentGuideLayout],
4989) -> Option<usize> {
4990 let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
4991 return None;
4992 };
4993 let target_depth = outline_panel
4994 .cached_entries
4995 .get(*target_ix)
4996 .map(|cached_entry| cached_entry.depth)?;
4997
4998 let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
4999 .cached_entries
5000 .get(target_ix + 1)
5001 .filter(|cached_entry| cached_entry.depth > target_depth)
5002 .map(|entry| entry.depth)
5003 {
5004 (target_ix + 1, target_depth.saturating_sub(1))
5005 } else {
5006 (*target_ix, target_depth.saturating_sub(1))
5007 };
5008
5009 candidates
5010 .iter()
5011 .enumerate()
5012 .find(|(_, guide)| {
5013 guide.offset.y <= target_ix
5014 && target_ix < guide.offset.y + guide.length
5015 && guide.offset.x == target_depth
5016 })
5017 .map(|(ix, _)| ix)
5018}
5019
5020fn subscribe_for_editor_events(
5021 editor: &Entity<Editor>,
5022 window: &mut Window,
5023 cx: &mut Context<OutlinePanel>,
5024) -> Subscription {
5025 let debounce = Some(UPDATE_DEBOUNCE);
5026 cx.subscribe_in(
5027 editor,
5028 window,
5029 move |outline_panel, editor, e: &EditorEvent, window, cx| {
5030 if !outline_panel.active {
5031 return;
5032 }
5033 match e {
5034 EditorEvent::SelectionsChanged { local: true } => {
5035 outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5036 cx.notify();
5037 }
5038 EditorEvent::ExcerptsAdded { excerpts, .. } => {
5039 outline_panel
5040 .new_entries_for_fs_update
5041 .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
5042 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5043 }
5044 EditorEvent::ExcerptsRemoved { ids, .. } => {
5045 let mut ids = ids.iter().collect::<HashSet<_>>();
5046 for excerpts in outline_panel.excerpts.values_mut() {
5047 excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
5048 if ids.is_empty() {
5049 break;
5050 }
5051 }
5052 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5053 }
5054 EditorEvent::ExcerptsExpanded { ids } => {
5055 outline_panel.invalidate_outlines(ids);
5056 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5057 if update_cached_items {
5058 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5059 }
5060 }
5061 EditorEvent::ExcerptsEdited { ids } => {
5062 outline_panel.invalidate_outlines(ids);
5063 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5064 if update_cached_items {
5065 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5066 }
5067 }
5068 EditorEvent::BufferFoldToggled { ids, .. } => {
5069 outline_panel.invalidate_outlines(ids);
5070 let mut latest_unfolded_buffer_id = None;
5071 let mut latest_folded_buffer_id = None;
5072 let mut ignore_selections_change = false;
5073 outline_panel.new_entries_for_fs_update.extend(
5074 ids.iter()
5075 .filter(|id| {
5076 outline_panel
5077 .excerpts
5078 .iter()
5079 .find_map(|(buffer_id, excerpts)| {
5080 if excerpts.contains_key(id) {
5081 ignore_selections_change |= outline_panel
5082 .preserve_selection_on_buffer_fold_toggles
5083 .remove(buffer_id);
5084 Some(buffer_id)
5085 } else {
5086 None
5087 }
5088 })
5089 .map(|buffer_id| {
5090 if editor.read(cx).is_buffer_folded(*buffer_id, cx) {
5091 latest_folded_buffer_id = Some(*buffer_id);
5092 false
5093 } else {
5094 latest_unfolded_buffer_id = Some(*buffer_id);
5095 true
5096 }
5097 })
5098 .unwrap_or(true)
5099 })
5100 .copied(),
5101 );
5102 if !ignore_selections_change {
5103 if let Some(entry_to_select) = latest_unfolded_buffer_id
5104 .or(latest_folded_buffer_id)
5105 .and_then(|toggled_buffer_id| {
5106 outline_panel.fs_entries.iter().find_map(
5107 |fs_entry| match fs_entry {
5108 FsEntry::ExternalFile(external) => {
5109 if external.buffer_id == toggled_buffer_id {
5110 Some(fs_entry.clone())
5111 } else {
5112 None
5113 }
5114 }
5115 FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5116 if *buffer_id == toggled_buffer_id {
5117 Some(fs_entry.clone())
5118 } else {
5119 None
5120 }
5121 }
5122 FsEntry::Directory(..) => None,
5123 },
5124 )
5125 })
5126 .map(PanelEntry::Fs)
5127 {
5128 outline_panel.select_entry(entry_to_select, true, window, cx);
5129 }
5130 }
5131
5132 outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5133 }
5134 EditorEvent::Reparsed(buffer_id) => {
5135 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
5136 for (_, excerpt) in excerpts {
5137 excerpt.invalidate_outlines();
5138 }
5139 }
5140 let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5141 if update_cached_items {
5142 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5143 }
5144 }
5145 _ => {}
5146 }
5147 },
5148 )
5149}
5150
5151fn empty_icon() -> AnyElement {
5152 h_flex()
5153 .size(IconSize::default().rems())
5154 .invisible()
5155 .flex_none()
5156 .into_any_element()
5157}
5158
5159fn horizontal_separator(cx: &mut App) -> Div {
5160 div().mx_2().border_primary(cx).border_t_1()
5161}
5162
5163#[derive(Debug, Default)]
5164struct GenerationState {
5165 entries: Vec<CachedEntry>,
5166 match_candidates: Vec<StringMatchCandidate>,
5167 max_width_estimate_and_index: Option<(u64, usize)>,
5168}
5169
5170impl GenerationState {
5171 fn clear(&mut self) {
5172 self.entries.clear();
5173 self.match_candidates.clear();
5174 self.max_width_estimate_and_index = None;
5175 }
5176}
5177
5178#[cfg(test)]
5179mod tests {
5180 use db::indoc;
5181 use gpui::{TestAppContext, VisualTestContext, WindowHandle};
5182 use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
5183 use pretty_assertions::assert_eq;
5184 use project::FakeFs;
5185 use search::project_search::{self, perform_project_search};
5186 use serde_json::json;
5187 use util::path;
5188 use workspace::{OpenOptions, OpenVisible};
5189
5190 use super::*;
5191
5192 const SELECTED_MARKER: &str = " <==== selected";
5193
5194 #[gpui::test(iterations = 10)]
5195 async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5196 init_test(cx);
5197
5198 let fs = FakeFs::new(cx.background_executor.clone());
5199 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5200 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5201 project.read_with(cx, |project, _| {
5202 project.languages().add(Arc::new(rust_lang()))
5203 });
5204 let workspace = add_outline_panel(&project, cx).await;
5205 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5206 let outline_panel = outline_panel(&workspace, cx);
5207 outline_panel.update_in(cx, |outline_panel, window, cx| {
5208 outline_panel.set_active(true, window, cx)
5209 });
5210
5211 workspace
5212 .update(cx, |workspace, window, cx| {
5213 ProjectSearchView::deploy_search(
5214 workspace,
5215 &workspace::DeploySearch::default(),
5216 window,
5217 cx,
5218 )
5219 })
5220 .unwrap();
5221 let search_view = workspace
5222 .update(cx, |workspace, _, cx| {
5223 workspace
5224 .active_pane()
5225 .read(cx)
5226 .items()
5227 .find_map(|item| item.downcast::<ProjectSearchView>())
5228 .expect("Project search view expected to appear after new search event trigger")
5229 })
5230 .unwrap();
5231
5232 let query = "param_names_for_lifetime_elision_hints";
5233 perform_project_search(&search_view, query, cx);
5234 search_view.update(cx, |search_view, cx| {
5235 search_view
5236 .results_editor()
5237 .update(cx, |results_editor, cx| {
5238 assert_eq!(
5239 results_editor.display_text(cx).match_indices(query).count(),
5240 9
5241 );
5242 });
5243 });
5244
5245 let all_matches = r#"/rust-analyzer/
5246 crates/
5247 ide/src/
5248 inlay_hints/
5249 fn_lifetime_fn.rs
5250 search: match config.param_names_for_lifetime_elision_hints {
5251 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5252 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5253 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5254 inlay_hints.rs
5255 search: pub param_names_for_lifetime_elision_hints: bool,
5256 search: param_names_for_lifetime_elision_hints: self
5257 static_index.rs
5258 search: param_names_for_lifetime_elision_hints: false,
5259 rust-analyzer/src/
5260 cli/
5261 analysis_stats.rs
5262 search: param_names_for_lifetime_elision_hints: true,
5263 config.rs
5264 search: param_names_for_lifetime_elision_hints: self"#;
5265 let select_first_in_all_matches = |line_to_select: &str| {
5266 assert!(all_matches.contains(line_to_select));
5267 all_matches.replacen(
5268 line_to_select,
5269 &format!("{line_to_select}{SELECTED_MARKER}"),
5270 1,
5271 )
5272 };
5273
5274 cx.executor()
5275 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5276 cx.run_until_parked();
5277 outline_panel.update(cx, |outline_panel, cx| {
5278 assert_eq!(
5279 display_entries(
5280 &project,
5281 &snapshot(&outline_panel, cx),
5282 &outline_panel.cached_entries,
5283 outline_panel.selected_entry(),
5284 cx,
5285 ),
5286 select_first_in_all_matches(
5287 "search: match config.param_names_for_lifetime_elision_hints {"
5288 )
5289 );
5290 });
5291
5292 outline_panel.update_in(cx, |outline_panel, window, cx| {
5293 outline_panel.select_parent(&SelectParent, window, 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("fn_lifetime_fn.rs")
5303 );
5304 });
5305 outline_panel.update_in(cx, |outline_panel, window, cx| {
5306 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5307 });
5308 cx.executor()
5309 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5310 cx.run_until_parked();
5311 outline_panel.update(cx, |outline_panel, cx| {
5312 assert_eq!(
5313 display_entries(
5314 &project,
5315 &snapshot(&outline_panel, cx),
5316 &outline_panel.cached_entries,
5317 outline_panel.selected_entry(),
5318 cx,
5319 ),
5320 format!(
5321 r#"/rust-analyzer/
5322 crates/
5323 ide/src/
5324 inlay_hints/
5325 fn_lifetime_fn.rs{SELECTED_MARKER}
5326 inlay_hints.rs
5327 search: pub param_names_for_lifetime_elision_hints: bool,
5328 search: param_names_for_lifetime_elision_hints: self
5329 static_index.rs
5330 search: param_names_for_lifetime_elision_hints: false,
5331 rust-analyzer/src/
5332 cli/
5333 analysis_stats.rs
5334 search: param_names_for_lifetime_elision_hints: true,
5335 config.rs
5336 search: param_names_for_lifetime_elision_hints: self"#,
5337 )
5338 );
5339 });
5340
5341 outline_panel.update_in(cx, |outline_panel, window, cx| {
5342 outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5343 });
5344 cx.executor()
5345 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5346 cx.run_until_parked();
5347 outline_panel.update_in(cx, |outline_panel, window, cx| {
5348 outline_panel.select_parent(&SelectParent, window, cx);
5349 assert_eq!(
5350 display_entries(
5351 &project,
5352 &snapshot(&outline_panel, cx),
5353 &outline_panel.cached_entries,
5354 outline_panel.selected_entry(),
5355 cx,
5356 ),
5357 select_first_in_all_matches("inlay_hints/")
5358 );
5359 });
5360
5361 outline_panel.update_in(cx, |outline_panel, window, cx| {
5362 outline_panel.select_parent(&SelectParent, window, cx);
5363 assert_eq!(
5364 display_entries(
5365 &project,
5366 &snapshot(&outline_panel, cx),
5367 &outline_panel.cached_entries,
5368 outline_panel.selected_entry(),
5369 cx,
5370 ),
5371 select_first_in_all_matches("ide/src/")
5372 );
5373 });
5374
5375 outline_panel.update_in(cx, |outline_panel, window, cx| {
5376 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5377 });
5378 cx.executor()
5379 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5380 cx.run_until_parked();
5381 outline_panel.update(cx, |outline_panel, cx| {
5382 assert_eq!(
5383 display_entries(
5384 &project,
5385 &snapshot(&outline_panel, cx),
5386 &outline_panel.cached_entries,
5387 outline_panel.selected_entry(),
5388 cx,
5389 ),
5390 format!(
5391 r#"/rust-analyzer/
5392 crates/
5393 ide/src/{SELECTED_MARKER}
5394 rust-analyzer/src/
5395 cli/
5396 analysis_stats.rs
5397 search: param_names_for_lifetime_elision_hints: true,
5398 config.rs
5399 search: param_names_for_lifetime_elision_hints: self"#,
5400 )
5401 );
5402 });
5403 outline_panel.update_in(cx, |outline_panel, window, cx| {
5404 outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5405 });
5406 cx.executor()
5407 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5408 cx.run_until_parked();
5409 outline_panel.update(cx, |outline_panel, cx| {
5410 assert_eq!(
5411 display_entries(
5412 &project,
5413 &snapshot(&outline_panel, cx),
5414 &outline_panel.cached_entries,
5415 outline_panel.selected_entry(),
5416 cx,
5417 ),
5418 select_first_in_all_matches("ide/src/")
5419 );
5420 });
5421 }
5422
5423 #[gpui::test(iterations = 10)]
5424 async fn test_item_filtering(cx: &mut TestAppContext) {
5425 init_test(cx);
5426
5427 let fs = FakeFs::new(cx.background_executor.clone());
5428 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5429 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5430 project.read_with(cx, |project, _| {
5431 project.languages().add(Arc::new(rust_lang()))
5432 });
5433 let workspace = add_outline_panel(&project, cx).await;
5434 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5435 let outline_panel = outline_panel(&workspace, cx);
5436 outline_panel.update_in(cx, |outline_panel, window, cx| {
5437 outline_panel.set_active(true, window, cx)
5438 });
5439
5440 workspace
5441 .update(cx, |workspace, window, cx| {
5442 ProjectSearchView::deploy_search(
5443 workspace,
5444 &workspace::DeploySearch::default(),
5445 window,
5446 cx,
5447 )
5448 })
5449 .unwrap();
5450 let search_view = workspace
5451 .update(cx, |workspace, _, cx| {
5452 workspace
5453 .active_pane()
5454 .read(cx)
5455 .items()
5456 .find_map(|item| item.downcast::<ProjectSearchView>())
5457 .expect("Project search view expected to appear after new search event trigger")
5458 })
5459 .unwrap();
5460
5461 let query = "param_names_for_lifetime_elision_hints";
5462 perform_project_search(&search_view, query, cx);
5463 search_view.update(cx, |search_view, cx| {
5464 search_view
5465 .results_editor()
5466 .update(cx, |results_editor, cx| {
5467 assert_eq!(
5468 results_editor.display_text(cx).match_indices(query).count(),
5469 9
5470 );
5471 });
5472 });
5473 let all_matches = r#"/rust-analyzer/
5474 crates/
5475 ide/src/
5476 inlay_hints/
5477 fn_lifetime_fn.rs
5478 search: match config.param_names_for_lifetime_elision_hints {
5479 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5480 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5481 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5482 inlay_hints.rs
5483 search: pub param_names_for_lifetime_elision_hints: bool,
5484 search: param_names_for_lifetime_elision_hints: self
5485 static_index.rs
5486 search: param_names_for_lifetime_elision_hints: false,
5487 rust-analyzer/src/
5488 cli/
5489 analysis_stats.rs
5490 search: param_names_for_lifetime_elision_hints: true,
5491 config.rs
5492 search: param_names_for_lifetime_elision_hints: self"#;
5493
5494 cx.executor()
5495 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5496 cx.run_until_parked();
5497 outline_panel.update(cx, |outline_panel, cx| {
5498 assert_eq!(
5499 display_entries(
5500 &project,
5501 &snapshot(&outline_panel, cx),
5502 &outline_panel.cached_entries,
5503 None,
5504 cx,
5505 ),
5506 all_matches,
5507 );
5508 });
5509
5510 let filter_text = "a";
5511 outline_panel.update_in(cx, |outline_panel, window, cx| {
5512 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5513 filter_editor.set_text(filter_text, window, cx);
5514 });
5515 });
5516 cx.executor()
5517 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5518 cx.run_until_parked();
5519
5520 outline_panel.update(cx, |outline_panel, cx| {
5521 assert_eq!(
5522 display_entries(
5523 &project,
5524 &snapshot(&outline_panel, cx),
5525 &outline_panel.cached_entries,
5526 None,
5527 cx,
5528 ),
5529 all_matches
5530 .lines()
5531 .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5532 .filter(|item| item.contains(filter_text))
5533 .collect::<Vec<_>>()
5534 .join("\n"),
5535 );
5536 });
5537
5538 outline_panel.update_in(cx, |outline_panel, window, cx| {
5539 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5540 filter_editor.set_text("", window, cx);
5541 });
5542 });
5543 cx.executor()
5544 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5545 cx.run_until_parked();
5546 outline_panel.update(cx, |outline_panel, cx| {
5547 assert_eq!(
5548 display_entries(
5549 &project,
5550 &snapshot(&outline_panel, cx),
5551 &outline_panel.cached_entries,
5552 None,
5553 cx,
5554 ),
5555 all_matches,
5556 );
5557 });
5558 }
5559
5560 #[gpui::test(iterations = 10)]
5561 async fn test_item_opening(cx: &mut TestAppContext) {
5562 init_test(cx);
5563
5564 let fs = FakeFs::new(cx.background_executor.clone());
5565 populate_with_test_ra_project(&fs, path!("/rust-analyzer")).await;
5566 let project = Project::test(fs.clone(), [path!("/rust-analyzer").as_ref()], cx).await;
5567 project.read_with(cx, |project, _| {
5568 project.languages().add(Arc::new(rust_lang()))
5569 });
5570 let workspace = add_outline_panel(&project, cx).await;
5571 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5572 let outline_panel = outline_panel(&workspace, cx);
5573 outline_panel.update_in(cx, |outline_panel, window, cx| {
5574 outline_panel.set_active(true, window, cx)
5575 });
5576
5577 workspace
5578 .update(cx, |workspace, window, cx| {
5579 ProjectSearchView::deploy_search(
5580 workspace,
5581 &workspace::DeploySearch::default(),
5582 window,
5583 cx,
5584 )
5585 })
5586 .unwrap();
5587 let search_view = workspace
5588 .update(cx, |workspace, _, cx| {
5589 workspace
5590 .active_pane()
5591 .read(cx)
5592 .items()
5593 .find_map(|item| item.downcast::<ProjectSearchView>())
5594 .expect("Project search view expected to appear after new search event trigger")
5595 })
5596 .unwrap();
5597
5598 let query = "param_names_for_lifetime_elision_hints";
5599 perform_project_search(&search_view, query, cx);
5600 search_view.update(cx, |search_view, cx| {
5601 search_view
5602 .results_editor()
5603 .update(cx, |results_editor, cx| {
5604 assert_eq!(
5605 results_editor.display_text(cx).match_indices(query).count(),
5606 9
5607 );
5608 });
5609 });
5610 let root_path = format!("{}/", path!("/rust-analyzer"));
5611 let all_matches = format!(
5612 r#"{root_path}
5613 crates/
5614 ide/src/
5615 inlay_hints/
5616 fn_lifetime_fn.rs
5617 search: match config.param_names_for_lifetime_elision_hints {{
5618 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
5619 search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
5620 search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
5621 inlay_hints.rs
5622 search: pub param_names_for_lifetime_elision_hints: bool,
5623 search: param_names_for_lifetime_elision_hints: self
5624 static_index.rs
5625 search: param_names_for_lifetime_elision_hints: false,
5626 rust-analyzer/src/
5627 cli/
5628 analysis_stats.rs
5629 search: param_names_for_lifetime_elision_hints: true,
5630 config.rs
5631 search: param_names_for_lifetime_elision_hints: self"#
5632 );
5633 let select_first_in_all_matches = |line_to_select: &str| {
5634 assert!(all_matches.contains(line_to_select));
5635 all_matches.replacen(
5636 line_to_select,
5637 &format!("{line_to_select}{SELECTED_MARKER}"),
5638 1,
5639 )
5640 };
5641 cx.executor()
5642 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5643 cx.run_until_parked();
5644
5645 let active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5646 outline_panel
5647 .active_editor()
5648 .expect("should have an active editor open")
5649 });
5650 let initial_outline_selection =
5651 "search: match config.param_names_for_lifetime_elision_hints {";
5652 outline_panel.update_in(cx, |outline_panel, window, cx| {
5653 assert_eq!(
5654 display_entries(
5655 &project,
5656 &snapshot(&outline_panel, cx),
5657 &outline_panel.cached_entries,
5658 outline_panel.selected_entry(),
5659 cx,
5660 ),
5661 select_first_in_all_matches(initial_outline_selection)
5662 );
5663 assert_eq!(
5664 selected_row_text(&active_editor, cx),
5665 initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5666 "Should place the initial editor selection on the corresponding search result"
5667 );
5668
5669 outline_panel.select_next(&SelectNext, window, cx);
5670 outline_panel.select_next(&SelectNext, window, cx);
5671 });
5672
5673 let navigated_outline_selection =
5674 "search: Some(it) if config.param_names_for_lifetime_elision_hints => {";
5675 outline_panel.update(cx, |outline_panel, cx| {
5676 assert_eq!(
5677 display_entries(
5678 &project,
5679 &snapshot(&outline_panel, cx),
5680 &outline_panel.cached_entries,
5681 outline_panel.selected_entry(),
5682 cx,
5683 ),
5684 select_first_in_all_matches(navigated_outline_selection)
5685 );
5686 });
5687 cx.executor()
5688 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5689 outline_panel.update(cx, |_, cx| {
5690 assert_eq!(
5691 selected_row_text(&active_editor, cx),
5692 navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5693 "Should still have the initial caret position after SelectNext calls"
5694 );
5695 });
5696
5697 outline_panel.update_in(cx, |outline_panel, window, cx| {
5698 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5699 });
5700 outline_panel.update(cx, |_outline_panel, cx| {
5701 assert_eq!(
5702 selected_row_text(&active_editor, cx),
5703 navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5704 "After opening, should move the caret to the opened outline entry's position"
5705 );
5706 });
5707
5708 outline_panel.update_in(cx, |outline_panel, window, cx| {
5709 outline_panel.select_next(&SelectNext, window, cx);
5710 });
5711 let next_navigated_outline_selection = "search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },";
5712 outline_panel.update(cx, |outline_panel, cx| {
5713 assert_eq!(
5714 display_entries(
5715 &project,
5716 &snapshot(&outline_panel, cx),
5717 &outline_panel.cached_entries,
5718 outline_panel.selected_entry(),
5719 cx,
5720 ),
5721 select_first_in_all_matches(next_navigated_outline_selection)
5722 );
5723 });
5724 cx.executor()
5725 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5726 outline_panel.update(cx, |_outline_panel, cx| {
5727 assert_eq!(
5728 selected_row_text(&active_editor, cx),
5729 next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5730 "Should again preserve the selection after another SelectNext call"
5731 );
5732 });
5733
5734 outline_panel.update_in(cx, |outline_panel, window, cx| {
5735 outline_panel.open_excerpts(&editor::OpenExcerpts, window, cx);
5736 });
5737 cx.executor()
5738 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5739 cx.run_until_parked();
5740 let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5741 outline_panel
5742 .active_editor()
5743 .expect("should have an active editor open")
5744 });
5745 outline_panel.update(cx, |outline_panel, cx| {
5746 assert_ne!(
5747 active_editor, new_active_editor,
5748 "After opening an excerpt, new editor should be open"
5749 );
5750 assert_eq!(
5751 display_entries(
5752 &project,
5753 &snapshot(&outline_panel, cx),
5754 &outline_panel.cached_entries,
5755 outline_panel.selected_entry(),
5756 cx,
5757 ),
5758 "fn_lifetime_fn.rs <==== selected"
5759 );
5760 assert_eq!(
5761 selected_row_text(&new_active_editor, cx),
5762 next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5763 "When opening the excerpt, should navigate to the place corresponding the outline entry"
5764 );
5765 });
5766 }
5767
5768 #[gpui::test]
5769 async fn test_multiple_workrees(cx: &mut TestAppContext) {
5770 init_test(cx);
5771
5772 let fs = FakeFs::new(cx.background_executor.clone());
5773 fs.insert_tree(
5774 "/root",
5775 json!({
5776 "one": {
5777 "a.txt": "aaa aaa"
5778 },
5779 "two": {
5780 "b.txt": "a aaa"
5781 }
5782
5783 }),
5784 )
5785 .await;
5786 let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await;
5787 let workspace = add_outline_panel(&project, cx).await;
5788 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5789 let outline_panel = outline_panel(&workspace, cx);
5790 outline_panel.update_in(cx, |outline_panel, window, cx| {
5791 outline_panel.set_active(true, window, cx)
5792 });
5793
5794 let items = workspace
5795 .update(cx, |workspace, window, cx| {
5796 workspace.open_paths(
5797 vec![PathBuf::from("/root/two")],
5798 OpenOptions {
5799 visible: Some(OpenVisible::OnlyDirectories),
5800 ..Default::default()
5801 },
5802 None,
5803 window,
5804 cx,
5805 )
5806 })
5807 .unwrap()
5808 .await;
5809 assert_eq!(items.len(), 1, "Were opening another worktree directory");
5810 assert!(
5811 items[0].is_none(),
5812 "Directory should be opened successfully"
5813 );
5814
5815 workspace
5816 .update(cx, |workspace, window, cx| {
5817 ProjectSearchView::deploy_search(
5818 workspace,
5819 &workspace::DeploySearch::default(),
5820 window,
5821 cx,
5822 )
5823 })
5824 .unwrap();
5825 let search_view = workspace
5826 .update(cx, |workspace, _, cx| {
5827 workspace
5828 .active_pane()
5829 .read(cx)
5830 .items()
5831 .find_map(|item| item.downcast::<ProjectSearchView>())
5832 .expect("Project search view expected to appear after new search event trigger")
5833 })
5834 .unwrap();
5835
5836 let query = "aaa";
5837 perform_project_search(&search_view, query, cx);
5838 search_view.update(cx, |search_view, cx| {
5839 search_view
5840 .results_editor()
5841 .update(cx, |results_editor, cx| {
5842 assert_eq!(
5843 results_editor.display_text(cx).match_indices(query).count(),
5844 3
5845 );
5846 });
5847 });
5848
5849 cx.executor()
5850 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5851 cx.run_until_parked();
5852 outline_panel.update(cx, |outline_panel, cx| {
5853 assert_eq!(
5854 display_entries(
5855 &project,
5856 &snapshot(&outline_panel, cx),
5857 &outline_panel.cached_entries,
5858 outline_panel.selected_entry(),
5859 cx,
5860 ),
5861 r#"/root/one/
5862 a.txt
5863 search: aaa aaa <==== selected
5864 search: aaa aaa
5865/root/two/
5866 b.txt
5867 search: a aaa"#
5868 );
5869 });
5870
5871 outline_panel.update_in(cx, |outline_panel, window, cx| {
5872 outline_panel.select_previous(&SelectPrevious, window, cx);
5873 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5874 });
5875 cx.executor()
5876 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5877 cx.run_until_parked();
5878 outline_panel.update(cx, |outline_panel, cx| {
5879 assert_eq!(
5880 display_entries(
5881 &project,
5882 &snapshot(&outline_panel, cx),
5883 &outline_panel.cached_entries,
5884 outline_panel.selected_entry(),
5885 cx,
5886 ),
5887 r#"/root/one/
5888 a.txt <==== selected
5889/root/two/
5890 b.txt
5891 search: a aaa"#
5892 );
5893 });
5894
5895 outline_panel.update_in(cx, |outline_panel, window, cx| {
5896 outline_panel.select_next(&SelectNext, window, cx);
5897 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5898 });
5899 cx.executor()
5900 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5901 cx.run_until_parked();
5902 outline_panel.update(cx, |outline_panel, cx| {
5903 assert_eq!(
5904 display_entries(
5905 &project,
5906 &snapshot(&outline_panel, cx),
5907 &outline_panel.cached_entries,
5908 outline_panel.selected_entry(),
5909 cx,
5910 ),
5911 r#"/root/one/
5912 a.txt
5913/root/two/ <==== selected"#
5914 );
5915 });
5916
5917 outline_panel.update_in(cx, |outline_panel, window, cx| {
5918 outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5919 });
5920 cx.executor()
5921 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5922 cx.run_until_parked();
5923 outline_panel.update(cx, |outline_panel, cx| {
5924 assert_eq!(
5925 display_entries(
5926 &project,
5927 &snapshot(&outline_panel, cx),
5928 &outline_panel.cached_entries,
5929 outline_panel.selected_entry(),
5930 cx,
5931 ),
5932 r#"/root/one/
5933 a.txt
5934/root/two/ <==== selected
5935 b.txt
5936 search: a aaa"#
5937 );
5938 });
5939 }
5940
5941 #[gpui::test]
5942 async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
5943 init_test(cx);
5944
5945 let root = path!("/root");
5946 let fs = FakeFs::new(cx.background_executor.clone());
5947 fs.insert_tree(
5948 root,
5949 json!({
5950 "src": {
5951 "lib.rs": indoc!("
5952#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5953struct OutlineEntryExcerpt {
5954 id: ExcerptId,
5955 buffer_id: BufferId,
5956 range: ExcerptRange<language::Anchor>,
5957}"),
5958 }
5959 }),
5960 )
5961 .await;
5962 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
5963 project.read_with(cx, |project, _| {
5964 project.languages().add(Arc::new(
5965 rust_lang()
5966 .with_outline_query(
5967 r#"
5968 (struct_item
5969 (visibility_modifier)? @context
5970 "struct" @context
5971 name: (_) @name) @item
5972
5973 (field_declaration
5974 (visibility_modifier)? @context
5975 name: (_) @name) @item
5976"#,
5977 )
5978 .unwrap(),
5979 ))
5980 });
5981 let workspace = add_outline_panel(&project, cx).await;
5982 let cx = &mut VisualTestContext::from_window(*workspace, cx);
5983 let outline_panel = outline_panel(&workspace, cx);
5984 cx.update(|window, cx| {
5985 outline_panel.update(cx, |outline_panel, cx| {
5986 outline_panel.set_active(true, window, cx)
5987 });
5988 });
5989
5990 let _editor = workspace
5991 .update(cx, |workspace, window, cx| {
5992 workspace.open_abs_path(
5993 PathBuf::from(path!("/root/src/lib.rs")),
5994 OpenOptions {
5995 visible: Some(OpenVisible::All),
5996 ..Default::default()
5997 },
5998 window,
5999 cx,
6000 )
6001 })
6002 .unwrap()
6003 .await
6004 .expect("Failed to open Rust source file")
6005 .downcast::<Editor>()
6006 .expect("Should open an editor for Rust source file");
6007
6008 cx.executor()
6009 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6010 cx.run_until_parked();
6011 outline_panel.update(cx, |outline_panel, cx| {
6012 assert_eq!(
6013 display_entries(
6014 &project,
6015 &snapshot(&outline_panel, cx),
6016 &outline_panel.cached_entries,
6017 outline_panel.selected_entry(),
6018 cx,
6019 ),
6020 indoc!(
6021 "
6022outline: struct OutlineEntryExcerpt
6023 outline: id
6024 outline: buffer_id
6025 outline: range"
6026 )
6027 );
6028 });
6029
6030 cx.update(|window, cx| {
6031 outline_panel.update(cx, |outline_panel, cx| {
6032 outline_panel.select_next(&SelectNext, window, cx);
6033 });
6034 });
6035 cx.executor()
6036 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6037 cx.run_until_parked();
6038 outline_panel.update(cx, |outline_panel, cx| {
6039 assert_eq!(
6040 display_entries(
6041 &project,
6042 &snapshot(&outline_panel, cx),
6043 &outline_panel.cached_entries,
6044 outline_panel.selected_entry(),
6045 cx,
6046 ),
6047 indoc!(
6048 "
6049outline: struct OutlineEntryExcerpt <==== selected
6050 outline: id
6051 outline: buffer_id
6052 outline: range"
6053 )
6054 );
6055 });
6056
6057 cx.update(|window, cx| {
6058 outline_panel.update(cx, |outline_panel, cx| {
6059 outline_panel.select_next(&SelectNext, window, cx);
6060 });
6061 });
6062 cx.executor()
6063 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6064 cx.run_until_parked();
6065 outline_panel.update(cx, |outline_panel, cx| {
6066 assert_eq!(
6067 display_entries(
6068 &project,
6069 &snapshot(&outline_panel, cx),
6070 &outline_panel.cached_entries,
6071 outline_panel.selected_entry(),
6072 cx,
6073 ),
6074 indoc!(
6075 "
6076outline: struct OutlineEntryExcerpt
6077 outline: id <==== selected
6078 outline: buffer_id
6079 outline: range"
6080 )
6081 );
6082 });
6083
6084 cx.update(|window, cx| {
6085 outline_panel.update(cx, |outline_panel, cx| {
6086 outline_panel.select_next(&SelectNext, window, cx);
6087 });
6088 });
6089 cx.executor()
6090 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6091 cx.run_until_parked();
6092 outline_panel.update(cx, |outline_panel, cx| {
6093 assert_eq!(
6094 display_entries(
6095 &project,
6096 &snapshot(&outline_panel, cx),
6097 &outline_panel.cached_entries,
6098 outline_panel.selected_entry(),
6099 cx,
6100 ),
6101 indoc!(
6102 "
6103outline: struct OutlineEntryExcerpt
6104 outline: id
6105 outline: buffer_id <==== selected
6106 outline: range"
6107 )
6108 );
6109 });
6110
6111 cx.update(|window, cx| {
6112 outline_panel.update(cx, |outline_panel, cx| {
6113 outline_panel.select_next(&SelectNext, window, cx);
6114 });
6115 });
6116 cx.executor()
6117 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6118 cx.run_until_parked();
6119 outline_panel.update(cx, |outline_panel, cx| {
6120 assert_eq!(
6121 display_entries(
6122 &project,
6123 &snapshot(&outline_panel, cx),
6124 &outline_panel.cached_entries,
6125 outline_panel.selected_entry(),
6126 cx,
6127 ),
6128 indoc!(
6129 "
6130outline: struct OutlineEntryExcerpt
6131 outline: id
6132 outline: buffer_id
6133 outline: range <==== selected"
6134 )
6135 );
6136 });
6137
6138 cx.update(|window, cx| {
6139 outline_panel.update(cx, |outline_panel, cx| {
6140 outline_panel.select_next(&SelectNext, window, cx);
6141 });
6142 });
6143 cx.executor()
6144 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6145 cx.run_until_parked();
6146 outline_panel.update(cx, |outline_panel, cx| {
6147 assert_eq!(
6148 display_entries(
6149 &project,
6150 &snapshot(&outline_panel, cx),
6151 &outline_panel.cached_entries,
6152 outline_panel.selected_entry(),
6153 cx,
6154 ),
6155 indoc!(
6156 "
6157outline: struct OutlineEntryExcerpt <==== selected
6158 outline: id
6159 outline: buffer_id
6160 outline: range"
6161 )
6162 );
6163 });
6164
6165 cx.update(|window, cx| {
6166 outline_panel.update(cx, |outline_panel, cx| {
6167 outline_panel.select_previous(&SelectPrevious, window, cx);
6168 });
6169 });
6170 cx.executor()
6171 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6172 cx.run_until_parked();
6173 outline_panel.update(cx, |outline_panel, cx| {
6174 assert_eq!(
6175 display_entries(
6176 &project,
6177 &snapshot(&outline_panel, cx),
6178 &outline_panel.cached_entries,
6179 outline_panel.selected_entry(),
6180 cx,
6181 ),
6182 indoc!(
6183 "
6184outline: struct OutlineEntryExcerpt
6185 outline: id
6186 outline: buffer_id
6187 outline: range <==== selected"
6188 )
6189 );
6190 });
6191
6192 cx.update(|window, cx| {
6193 outline_panel.update(cx, |outline_panel, cx| {
6194 outline_panel.select_previous(&SelectPrevious, window, cx);
6195 });
6196 });
6197 cx.executor()
6198 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6199 cx.run_until_parked();
6200 outline_panel.update(cx, |outline_panel, cx| {
6201 assert_eq!(
6202 display_entries(
6203 &project,
6204 &snapshot(&outline_panel, cx),
6205 &outline_panel.cached_entries,
6206 outline_panel.selected_entry(),
6207 cx,
6208 ),
6209 indoc!(
6210 "
6211outline: struct OutlineEntryExcerpt
6212 outline: id
6213 outline: buffer_id <==== selected
6214 outline: range"
6215 )
6216 );
6217 });
6218
6219 cx.update(|window, cx| {
6220 outline_panel.update(cx, |outline_panel, cx| {
6221 outline_panel.select_previous(&SelectPrevious, window, cx);
6222 });
6223 });
6224 cx.executor()
6225 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6226 cx.run_until_parked();
6227 outline_panel.update(cx, |outline_panel, cx| {
6228 assert_eq!(
6229 display_entries(
6230 &project,
6231 &snapshot(&outline_panel, cx),
6232 &outline_panel.cached_entries,
6233 outline_panel.selected_entry(),
6234 cx,
6235 ),
6236 indoc!(
6237 "
6238outline: struct OutlineEntryExcerpt
6239 outline: id <==== selected
6240 outline: buffer_id
6241 outline: range"
6242 )
6243 );
6244 });
6245
6246 cx.update(|window, cx| {
6247 outline_panel.update(cx, |outline_panel, cx| {
6248 outline_panel.select_previous(&SelectPrevious, window, cx);
6249 });
6250 });
6251 cx.executor()
6252 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6253 cx.run_until_parked();
6254 outline_panel.update(cx, |outline_panel, cx| {
6255 assert_eq!(
6256 display_entries(
6257 &project,
6258 &snapshot(&outline_panel, cx),
6259 &outline_panel.cached_entries,
6260 outline_panel.selected_entry(),
6261 cx,
6262 ),
6263 indoc!(
6264 "
6265outline: struct OutlineEntryExcerpt <==== selected
6266 outline: id
6267 outline: buffer_id
6268 outline: range"
6269 )
6270 );
6271 });
6272
6273 cx.update(|window, cx| {
6274 outline_panel.update(cx, |outline_panel, cx| {
6275 outline_panel.select_previous(&SelectPrevious, window, cx);
6276 });
6277 });
6278 cx.executor()
6279 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6280 cx.run_until_parked();
6281 outline_panel.update(cx, |outline_panel, cx| {
6282 assert_eq!(
6283 display_entries(
6284 &project,
6285 &snapshot(&outline_panel, cx),
6286 &outline_panel.cached_entries,
6287 outline_panel.selected_entry(),
6288 cx,
6289 ),
6290 indoc!(
6291 "
6292outline: struct OutlineEntryExcerpt
6293 outline: id
6294 outline: buffer_id
6295 outline: range <==== selected"
6296 )
6297 );
6298 });
6299 }
6300
6301 #[gpui::test(iterations = 10)]
6302 async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6303 init_test(cx);
6304
6305 let root = "/frontend-project";
6306 let fs = FakeFs::new(cx.background_executor.clone());
6307 fs.insert_tree(
6308 root,
6309 json!({
6310 "public": {
6311 "lottie": {
6312 "syntax-tree.json": r#"{ "something": "static" }"#
6313 }
6314 },
6315 "src": {
6316 "app": {
6317 "(site)": {
6318 "(about)": {
6319 "jobs": {
6320 "[slug]": {
6321 "page.tsx": r#"static"#
6322 }
6323 }
6324 },
6325 "(blog)": {
6326 "post": {
6327 "[slug]": {
6328 "page.tsx": r#"static"#
6329 }
6330 }
6331 },
6332 }
6333 },
6334 "components": {
6335 "ErrorBoundary.tsx": r#"static"#,
6336 }
6337 }
6338
6339 }),
6340 )
6341 .await;
6342 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
6343 let workspace = add_outline_panel(&project, cx).await;
6344 let cx = &mut VisualTestContext::from_window(*workspace, cx);
6345 let outline_panel = outline_panel(&workspace, cx);
6346 outline_panel.update_in(cx, |outline_panel, window, cx| {
6347 outline_panel.set_active(true, window, cx)
6348 });
6349
6350 workspace
6351 .update(cx, |workspace, window, cx| {
6352 ProjectSearchView::deploy_search(
6353 workspace,
6354 &workspace::DeploySearch::default(),
6355 window,
6356 cx,
6357 )
6358 })
6359 .unwrap();
6360 let search_view = workspace
6361 .update(cx, |workspace, _, cx| {
6362 workspace
6363 .active_pane()
6364 .read(cx)
6365 .items()
6366 .find_map(|item| item.downcast::<ProjectSearchView>())
6367 .expect("Project search view expected to appear after new search event trigger")
6368 })
6369 .unwrap();
6370
6371 let query = "static";
6372 perform_project_search(&search_view, query, cx);
6373 search_view.update(cx, |search_view, cx| {
6374 search_view
6375 .results_editor()
6376 .update(cx, |results_editor, cx| {
6377 assert_eq!(
6378 results_editor.display_text(cx).match_indices(query).count(),
6379 4
6380 );
6381 });
6382 });
6383
6384 cx.executor()
6385 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6386 cx.run_until_parked();
6387 outline_panel.update(cx, |outline_panel, cx| {
6388 assert_eq!(
6389 display_entries(
6390 &project,
6391 &snapshot(&outline_panel, cx),
6392 &outline_panel.cached_entries,
6393 outline_panel.selected_entry(),
6394 cx,
6395 ),
6396 r#"/frontend-project/
6397 public/lottie/
6398 syntax-tree.json
6399 search: { "something": "static" } <==== selected
6400 src/
6401 app/(site)/
6402 (about)/jobs/[slug]/
6403 page.tsx
6404 search: static
6405 (blog)/post/[slug]/
6406 page.tsx
6407 search: static
6408 components/
6409 ErrorBoundary.tsx
6410 search: static"#
6411 );
6412 });
6413
6414 outline_panel.update_in(cx, |outline_panel, window, cx| {
6415 // Move to 5th element in the list, 3 items down.
6416 for _ in 0..2 {
6417 outline_panel.select_next(&SelectNext, window, cx);
6418 }
6419 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6420 });
6421 cx.executor()
6422 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6423 cx.run_until_parked();
6424 outline_panel.update(cx, |outline_panel, cx| {
6425 assert_eq!(
6426 display_entries(
6427 &project,
6428 &snapshot(&outline_panel, cx),
6429 &outline_panel.cached_entries,
6430 outline_panel.selected_entry(),
6431 cx,
6432 ),
6433 r#"/frontend-project/
6434 public/lottie/
6435 syntax-tree.json
6436 search: { "something": "static" }
6437 src/
6438 app/(site)/ <==== selected
6439 components/
6440 ErrorBoundary.tsx
6441 search: static"#
6442 );
6443 });
6444
6445 outline_panel.update_in(cx, |outline_panel, window, cx| {
6446 // Move to the next visible non-FS entry
6447 for _ in 0..3 {
6448 outline_panel.select_next(&SelectNext, window, cx);
6449 }
6450 });
6451 cx.run_until_parked();
6452 outline_panel.update(cx, |outline_panel, cx| {
6453 assert_eq!(
6454 display_entries(
6455 &project,
6456 &snapshot(&outline_panel, cx),
6457 &outline_panel.cached_entries,
6458 outline_panel.selected_entry(),
6459 cx,
6460 ),
6461 r#"/frontend-project/
6462 public/lottie/
6463 syntax-tree.json
6464 search: { "something": "static" }
6465 src/
6466 app/(site)/
6467 components/
6468 ErrorBoundary.tsx
6469 search: static <==== selected"#
6470 );
6471 });
6472
6473 outline_panel.update_in(cx, |outline_panel, window, cx| {
6474 outline_panel
6475 .active_editor()
6476 .expect("Should have an active editor")
6477 .update(cx, |editor, cx| {
6478 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6479 });
6480 });
6481 cx.executor()
6482 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6483 cx.run_until_parked();
6484 outline_panel.update(cx, |outline_panel, cx| {
6485 assert_eq!(
6486 display_entries(
6487 &project,
6488 &snapshot(&outline_panel, cx),
6489 &outline_panel.cached_entries,
6490 outline_panel.selected_entry(),
6491 cx,
6492 ),
6493 r#"/frontend-project/
6494 public/lottie/
6495 syntax-tree.json
6496 search: { "something": "static" }
6497 src/
6498 app/(site)/
6499 components/
6500 ErrorBoundary.tsx <==== selected"#
6501 );
6502 });
6503
6504 outline_panel.update_in(cx, |outline_panel, window, cx| {
6505 outline_panel
6506 .active_editor()
6507 .expect("Should have an active editor")
6508 .update(cx, |editor, cx| {
6509 editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6510 });
6511 });
6512 cx.executor()
6513 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6514 cx.run_until_parked();
6515 outline_panel.update(cx, |outline_panel, cx| {
6516 assert_eq!(
6517 display_entries(
6518 &project,
6519 &snapshot(&outline_panel, cx),
6520 &outline_panel.cached_entries,
6521 outline_panel.selected_entry(),
6522 cx,
6523 ),
6524 r#"/frontend-project/
6525 public/lottie/
6526 syntax-tree.json
6527 search: { "something": "static" }
6528 src/
6529 app/(site)/
6530 components/
6531 ErrorBoundary.tsx <==== selected
6532 search: static"#
6533 );
6534 });
6535 }
6536
6537 async fn add_outline_panel(
6538 project: &Entity<Project>,
6539 cx: &mut TestAppContext,
6540 ) -> WindowHandle<Workspace> {
6541 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6542
6543 let outline_panel = window
6544 .update(cx, |_, window, cx| {
6545 cx.spawn_in(window, async |this, cx| {
6546 OutlinePanel::load(this, cx.clone()).await
6547 })
6548 })
6549 .unwrap()
6550 .await
6551 .expect("Failed to load outline panel");
6552
6553 window
6554 .update(cx, |workspace, window, cx| {
6555 workspace.add_panel(outline_panel, window, cx);
6556 })
6557 .unwrap();
6558 window
6559 }
6560
6561 fn outline_panel(
6562 workspace: &WindowHandle<Workspace>,
6563 cx: &mut TestAppContext,
6564 ) -> Entity<OutlinePanel> {
6565 workspace
6566 .update(cx, |workspace, _, cx| {
6567 workspace
6568 .panel::<OutlinePanel>(cx)
6569 .expect("no outline panel")
6570 })
6571 .unwrap()
6572 }
6573
6574 fn display_entries(
6575 project: &Entity<Project>,
6576 multi_buffer_snapshot: &MultiBufferSnapshot,
6577 cached_entries: &[CachedEntry],
6578 selected_entry: Option<&PanelEntry>,
6579 cx: &mut App,
6580 ) -> String {
6581 let mut display_string = String::new();
6582 for entry in cached_entries {
6583 if !display_string.is_empty() {
6584 display_string += "\n";
6585 }
6586 for _ in 0..entry.depth {
6587 display_string += " ";
6588 }
6589 display_string += &match &entry.entry {
6590 PanelEntry::Fs(entry) => match entry {
6591 FsEntry::ExternalFile(_) => {
6592 panic!("Did not cover external files with tests")
6593 }
6594 FsEntry::Directory(directory) => {
6595 match project
6596 .read(cx)
6597 .worktree_for_id(directory.worktree_id, cx)
6598 .and_then(|worktree| {
6599 if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
6600 Some(worktree.read(cx).abs_path())
6601 } else {
6602 None
6603 }
6604 }) {
6605 Some(root_path) => format!(
6606 "{}/{}",
6607 root_path.display(),
6608 directory.entry.path.display(),
6609 ),
6610 None => format!(
6611 "{}/",
6612 directory
6613 .entry
6614 .path
6615 .file_name()
6616 .unwrap_or_default()
6617 .to_string_lossy()
6618 ),
6619 }
6620 }
6621 FsEntry::File(file) => file
6622 .entry
6623 .path
6624 .file_name()
6625 .map(|name| name.to_string_lossy().to_string())
6626 .unwrap_or_default(),
6627 },
6628 PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6629 .entries
6630 .iter()
6631 .filter_map(|dir| dir.path.file_name())
6632 .map(|name| name.to_string_lossy().to_string() + "/")
6633 .collect(),
6634 PanelEntry::Outline(outline_entry) => match outline_entry {
6635 OutlineEntry::Excerpt(_) => continue,
6636 OutlineEntry::Outline(outline_entry) => {
6637 format!("outline: {}", outline_entry.outline.text)
6638 }
6639 },
6640 PanelEntry::Search(search_entry) => {
6641 format!(
6642 "search: {}",
6643 search_entry
6644 .render_data
6645 .get_or_init(|| SearchData::new(
6646 &search_entry.match_range,
6647 &multi_buffer_snapshot
6648 ))
6649 .context_text
6650 )
6651 }
6652 };
6653
6654 if Some(&entry.entry) == selected_entry {
6655 display_string += SELECTED_MARKER;
6656 }
6657 }
6658 display_string
6659 }
6660
6661 fn init_test(cx: &mut TestAppContext) {
6662 cx.update(|cx| {
6663 let settings = SettingsStore::test(cx);
6664 cx.set_global(settings);
6665
6666 theme::init(theme::LoadThemes::JustBase, cx);
6667
6668 language::init(cx);
6669 editor::init(cx);
6670 workspace::init_settings(cx);
6671 Project::init_settings(cx);
6672 project_search::init(cx);
6673 super::init(cx);
6674 });
6675 }
6676
6677 // Based on https://github.com/rust-lang/rust-analyzer/
6678 async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6679 fs.insert_tree(
6680 root,
6681 json!({
6682 "crates": {
6683 "ide": {
6684 "src": {
6685 "inlay_hints": {
6686 "fn_lifetime_fn.rs": r##"
6687 pub(super) fn hints(
6688 acc: &mut Vec<InlayHint>,
6689 config: &InlayHintsConfig,
6690 func: ast::Fn,
6691 ) -> Option<()> {
6692 // ... snip
6693
6694 let mut used_names: FxHashMap<SmolStr, usize> =
6695 match config.param_names_for_lifetime_elision_hints {
6696 true => generic_param_list
6697 .iter()
6698 .flat_map(|gpl| gpl.lifetime_params())
6699 .filter_map(|param| param.lifetime())
6700 .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6701 .collect(),
6702 false => Default::default(),
6703 };
6704 {
6705 let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6706 if self_param.is_some() && potential_lt_refs.next().is_some() {
6707 allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6708 // self can't be used as a lifetime, so no need to check for collisions
6709 "'self".into()
6710 } else {
6711 gen_idx_name()
6712 });
6713 }
6714 potential_lt_refs.for_each(|(name, ..)| {
6715 let name = match name {
6716 Some(it) if config.param_names_for_lifetime_elision_hints => {
6717 if let Some(c) = used_names.get_mut(it.text().as_str()) {
6718 *c += 1;
6719 SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6720 } else {
6721 used_names.insert(it.text().as_str().into(), 0);
6722 SmolStr::from_iter(["\'", it.text().as_str()])
6723 }
6724 }
6725 _ => gen_idx_name(),
6726 };
6727 allocated_lifetimes.push(name);
6728 });
6729 }
6730
6731 // ... snip
6732 }
6733
6734 // ... snip
6735
6736 #[test]
6737 fn hints_lifetimes_named() {
6738 check_with_config(
6739 InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6740 r#"
6741 fn nested_in<'named>(named: & &X< &()>) {}
6742 // ^'named1, 'named2, 'named3, $
6743 //^'named1 ^'named2 ^'named3
6744 "#,
6745 );
6746 }
6747
6748 // ... snip
6749 "##,
6750 },
6751 "inlay_hints.rs": r#"
6752 #[derive(Clone, Debug, PartialEq, Eq)]
6753 pub struct InlayHintsConfig {
6754 // ... snip
6755 pub param_names_for_lifetime_elision_hints: bool,
6756 pub max_length: Option<usize>,
6757 // ... snip
6758 }
6759
6760 impl Config {
6761 pub fn inlay_hints(&self) -> InlayHintsConfig {
6762 InlayHintsConfig {
6763 // ... snip
6764 param_names_for_lifetime_elision_hints: self
6765 .inlayHints_lifetimeElisionHints_useParameterNames()
6766 .to_owned(),
6767 max_length: self.inlayHints_maxLength().to_owned(),
6768 // ... snip
6769 }
6770 }
6771 }
6772 "#,
6773 "static_index.rs": r#"
6774// ... snip
6775 fn add_file(&mut self, file_id: FileId) {
6776 let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6777 let folds = self.analysis.folding_ranges(file_id).unwrap();
6778 let inlay_hints = self
6779 .analysis
6780 .inlay_hints(
6781 &InlayHintsConfig {
6782 // ... snip
6783 closure_style: hir::ClosureStyle::ImplFn,
6784 param_names_for_lifetime_elision_hints: false,
6785 binding_mode_hints: false,
6786 max_length: Some(25),
6787 closure_capture_hints: false,
6788 // ... snip
6789 },
6790 file_id,
6791 None,
6792 )
6793 .unwrap();
6794 // ... snip
6795 }
6796// ... snip
6797 "#
6798 }
6799 },
6800 "rust-analyzer": {
6801 "src": {
6802 "cli": {
6803 "analysis_stats.rs": r#"
6804 // ... snip
6805 for &file_id in &file_ids {
6806 _ = analysis.inlay_hints(
6807 &InlayHintsConfig {
6808 // ... snip
6809 implicit_drop_hints: true,
6810 lifetime_elision_hints: ide::LifetimeElisionHints::Always,
6811 param_names_for_lifetime_elision_hints: true,
6812 hide_named_constructor_hints: false,
6813 hide_closure_initialization_hints: false,
6814 closure_style: hir::ClosureStyle::ImplFn,
6815 max_length: Some(25),
6816 closing_brace_hints_min_lines: Some(20),
6817 fields_to_resolve: InlayFieldsToResolve::empty(),
6818 range_exclusive_hints: true,
6819 },
6820 file_id.into(),
6821 None,
6822 );
6823 }
6824 // ... snip
6825 "#,
6826 },
6827 "config.rs": r#"
6828 config_data! {
6829 /// Configs that only make sense when they are set by a client. As such they can only be defined
6830 /// by setting them using client's settings (e.g `settings.json` on VS Code).
6831 client: struct ClientDefaultConfigData <- ClientConfigInput -> {
6832 // ... snip
6833 /// Maximum length for inlay hints. Set to null to have an unlimited length.
6834 inlayHints_maxLength: Option<usize> = Some(25),
6835 // ... snip
6836 /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
6837 inlayHints_lifetimeElisionHints_useParameterNames: bool = false,
6838 // ... snip
6839 }
6840 }
6841
6842 impl Config {
6843 // ... snip
6844 pub fn inlay_hints(&self) -> InlayHintsConfig {
6845 InlayHintsConfig {
6846 // ... snip
6847 param_names_for_lifetime_elision_hints: self
6848 .inlayHints_lifetimeElisionHints_useParameterNames()
6849 .to_owned(),
6850 max_length: self.inlayHints_maxLength().to_owned(),
6851 // ... snip
6852 }
6853 }
6854 // ... snip
6855 }
6856 "#
6857 }
6858 }
6859 }
6860 }),
6861 )
6862 .await;
6863 }
6864
6865 fn rust_lang() -> Language {
6866 Language::new(
6867 LanguageConfig {
6868 name: "Rust".into(),
6869 matcher: LanguageMatcher {
6870 path_suffixes: vec!["rs".to_string()],
6871 ..Default::default()
6872 },
6873 ..Default::default()
6874 },
6875 Some(tree_sitter_rust::LANGUAGE.into()),
6876 )
6877 .with_highlights_query(
6878 r#"
6879 (field_identifier) @field
6880 (struct_expression) @struct
6881 "#,
6882 )
6883 .unwrap()
6884 .with_injection_query(
6885 r#"
6886 (macro_invocation
6887 (token_tree) @injection.content
6888 (#set! injection.language "rust"))
6889 "#,
6890 )
6891 .unwrap()
6892 }
6893
6894 fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
6895 outline_panel
6896 .active_editor()
6897 .unwrap()
6898 .read(cx)
6899 .buffer()
6900 .read(cx)
6901 .snapshot(cx)
6902 }
6903
6904 fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
6905 editor.update(cx, |editor, cx| {
6906 let selections = editor.selections.all::<language::Point>(cx);
6907 assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
6908 let selection = selections.first().unwrap();
6909 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
6910 let line_start = language::Point::new(selection.start.row, 0);
6911 let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
6912 multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
6913 })
6914 }
6915}