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