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