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