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