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