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