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