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