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