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