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
2414 .read(cx)
2415 .selections
2416 .newest::<language::Point>(cx)
2417 .head();
2418 let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx));
2419 let multi_buffer = editor.read(cx).buffer();
2420 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
2421 let (excerpt_id, buffer, _) = editor
2422 .read(cx)
2423 .buffer()
2424 .read(cx)
2425 .excerpt_containing(selection, cx)?;
2426 let buffer_id = buffer.read(cx).remote_id();
2427 let selection_display_point = selection.to_display_point(&editor_snapshot);
2428
2429 match &self.mode {
2430 ItemsDisplayMode::Search(search_state) => search_state
2431 .matches
2432 .iter()
2433 .rev()
2434 .min_by_key(|&(match_range, _)| {
2435 let match_display_range =
2436 match_range.clone().to_display_points(&editor_snapshot);
2437 let start_distance = if selection_display_point < match_display_range.start {
2438 match_display_range.start - selection_display_point
2439 } else {
2440 selection_display_point - match_display_range.start
2441 };
2442 let end_distance = if selection_display_point < match_display_range.end {
2443 match_display_range.end - selection_display_point
2444 } else {
2445 selection_display_point - match_display_range.end
2446 };
2447 start_distance + end_distance
2448 })
2449 .and_then(|(closest_range, _)| {
2450 self.cached_entries.iter().find_map(|cached_entry| {
2451 if let PanelEntry::Search(SearchEntry { match_range, .. }) =
2452 &cached_entry.entry
2453 {
2454 if match_range == closest_range {
2455 Some(cached_entry.entry.clone())
2456 } else {
2457 None
2458 }
2459 } else {
2460 None
2461 }
2462 })
2463 }),
2464 ItemsDisplayMode::Outline => self.outline_location(
2465 buffer_id,
2466 excerpt_id,
2467 multi_buffer_snapshot,
2468 editor_snapshot,
2469 selection_display_point,
2470 ),
2471 }
2472 }
2473
2474 fn outline_location(
2475 &mut self,
2476 buffer_id: BufferId,
2477 excerpt_id: ExcerptId,
2478 multi_buffer_snapshot: editor::MultiBufferSnapshot,
2479 editor_snapshot: editor::EditorSnapshot,
2480 selection_display_point: DisplayPoint,
2481 ) -> Option<PanelEntry> {
2482 let excerpt_outlines = self
2483 .excerpts
2484 .get(&buffer_id)
2485 .and_then(|excerpts| excerpts.get(&excerpt_id))
2486 .into_iter()
2487 .flat_map(|excerpt| excerpt.iter_outlines())
2488 .flat_map(|outline| {
2489 let start = multi_buffer_snapshot
2490 .anchor_in_excerpt(excerpt_id, outline.range.start)?
2491 .to_display_point(&editor_snapshot);
2492 let end = multi_buffer_snapshot
2493 .anchor_in_excerpt(excerpt_id, outline.range.end)?
2494 .to_display_point(&editor_snapshot);
2495 Some((start..end, outline))
2496 })
2497 .collect::<Vec<_>>();
2498
2499 let mut matching_outline_indices = Vec::new();
2500 let mut children = HashMap::default();
2501 let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
2502
2503 for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
2504 if outline_range
2505 .to_inclusive()
2506 .contains(&selection_display_point)
2507 {
2508 matching_outline_indices.push(i);
2509 } else if (outline_range.start.row()..outline_range.end.row())
2510 .to_inclusive()
2511 .contains(&selection_display_point.row())
2512 {
2513 matching_outline_indices.push(i);
2514 }
2515
2516 while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
2517 if parent_outline.depth >= outline.depth
2518 || !parent_range.contains(&outline_range.start)
2519 {
2520 parents_stack.pop();
2521 } else {
2522 break;
2523 }
2524 }
2525 if let Some((_, _, parent_index)) = parents_stack.last_mut() {
2526 children
2527 .entry(*parent_index)
2528 .or_insert_with(Vec::new)
2529 .push(i);
2530 }
2531 parents_stack.push((outline_range, outline, i));
2532 }
2533
2534 let outline_item = matching_outline_indices
2535 .into_iter()
2536 .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
2537 .filter(|(i, _)| {
2538 children
2539 .get(i)
2540 .map(|children| {
2541 children.iter().all(|child_index| {
2542 excerpt_outlines
2543 .get(*child_index)
2544 .map(|(child_range, _)| child_range.start > selection_display_point)
2545 .unwrap_or(false)
2546 })
2547 })
2548 .unwrap_or(true)
2549 })
2550 .min_by_key(|(_, (outline_range, outline))| {
2551 let distance_from_start = if outline_range.start > selection_display_point {
2552 outline_range.start - selection_display_point
2553 } else {
2554 selection_display_point - outline_range.start
2555 };
2556 let distance_from_end = if outline_range.end > selection_display_point {
2557 outline_range.end - selection_display_point
2558 } else {
2559 selection_display_point - outline_range.end
2560 };
2561
2562 (
2563 cmp::Reverse(outline.depth),
2564 distance_from_start + distance_from_end,
2565 )
2566 })
2567 .map(|(_, (_, outline))| *outline)
2568 .cloned();
2569
2570 let closest_container = match outline_item {
2571 Some(outline) => {
2572 PanelEntry::Outline(OutlineEntry::Outline(buffer_id, excerpt_id, outline))
2573 }
2574 None => {
2575 self.cached_entries.iter().rev().find_map(|cached_entry| {
2576 match &cached_entry.entry {
2577 PanelEntry::Outline(OutlineEntry::Excerpt(
2578 entry_buffer_id,
2579 entry_excerpt_id,
2580 _,
2581 )) => {
2582 if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
2583 Some(cached_entry.entry.clone())
2584 } else {
2585 None
2586 }
2587 }
2588 PanelEntry::Fs(
2589 FsEntry::ExternalFile(file_buffer_id, file_excerpts)
2590 | FsEntry::File(_, _, file_buffer_id, file_excerpts),
2591 ) => {
2592 if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
2593 Some(cached_entry.entry.clone())
2594 } else {
2595 None
2596 }
2597 }
2598 _ => None,
2599 }
2600 })?
2601 }
2602 };
2603 Some(closest_container)
2604 }
2605
2606 fn fetch_outdated_outlines(&mut self, cx: &mut ViewContext<Self>) {
2607 let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
2608 if excerpt_fetch_ranges.is_empty() {
2609 return;
2610 }
2611
2612 let syntax_theme = cx.theme().syntax().clone();
2613 for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
2614 for (excerpt_id, excerpt_range) in excerpt_ranges {
2615 let syntax_theme = syntax_theme.clone();
2616 let buffer_snapshot = buffer_snapshot.clone();
2617 self.outline_fetch_tasks.insert(
2618 (buffer_id, excerpt_id),
2619 cx.spawn(|outline_panel, mut cx| async move {
2620 let fetched_outlines = cx
2621 .background_executor()
2622 .spawn(async move {
2623 buffer_snapshot
2624 .outline_items_containing(
2625 excerpt_range.context,
2626 false,
2627 Some(&syntax_theme),
2628 )
2629 .unwrap_or_default()
2630 })
2631 .await;
2632 outline_panel
2633 .update(&mut cx, |outline_panel, cx| {
2634 if let Some(excerpt) = outline_panel
2635 .excerpts
2636 .entry(buffer_id)
2637 .or_default()
2638 .get_mut(&excerpt_id)
2639 {
2640 excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
2641 }
2642 outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
2643 })
2644 .ok();
2645 }),
2646 );
2647 }
2648 }
2649 }
2650
2651 fn is_singleton_active(&self, cx: &AppContext) -> bool {
2652 self.active_editor().map_or(false, |active_editor| {
2653 active_editor.read(cx).buffer().read(cx).is_singleton()
2654 })
2655 }
2656
2657 fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
2658 self.outline_fetch_tasks.clear();
2659 let mut ids = ids.iter().collect::<HashSet<_>>();
2660 for excerpts in self.excerpts.values_mut() {
2661 ids.retain(|id| {
2662 if let Some(excerpt) = excerpts.get_mut(id) {
2663 excerpt.invalidate_outlines();
2664 false
2665 } else {
2666 true
2667 }
2668 });
2669 if ids.is_empty() {
2670 break;
2671 }
2672 }
2673 }
2674
2675 fn excerpt_fetch_ranges(
2676 &self,
2677 cx: &AppContext,
2678 ) -> HashMap<
2679 BufferId,
2680 (
2681 BufferSnapshot,
2682 HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
2683 ),
2684 > {
2685 self.fs_entries
2686 .iter()
2687 .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
2688 match fs_entry {
2689 FsEntry::File(_, _, buffer_id, file_excerpts)
2690 | FsEntry::ExternalFile(buffer_id, file_excerpts) => {
2691 let excerpts = self.excerpts.get(buffer_id);
2692 for &file_excerpt in file_excerpts {
2693 if let Some(excerpt) = excerpts
2694 .and_then(|excerpts| excerpts.get(&file_excerpt))
2695 .filter(|excerpt| excerpt.should_fetch_outlines())
2696 {
2697 match excerpts_to_fetch.entry(*buffer_id) {
2698 hash_map::Entry::Occupied(mut o) => {
2699 o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
2700 }
2701 hash_map::Entry::Vacant(v) => {
2702 if let Some(buffer_snapshot) =
2703 self.buffer_snapshot_for_id(*buffer_id, cx)
2704 {
2705 v.insert((buffer_snapshot, HashMap::default()))
2706 .1
2707 .insert(file_excerpt, excerpt.range.clone());
2708 }
2709 }
2710 }
2711 }
2712 }
2713 }
2714 FsEntry::Directory(..) => {}
2715 }
2716 excerpts_to_fetch
2717 })
2718 }
2719
2720 fn buffer_snapshot_for_id(
2721 &self,
2722 buffer_id: BufferId,
2723 cx: &AppContext,
2724 ) -> Option<BufferSnapshot> {
2725 let editor = self.active_editor()?;
2726 Some(
2727 editor
2728 .read(cx)
2729 .buffer()
2730 .read(cx)
2731 .buffer(buffer_id)?
2732 .read(cx)
2733 .snapshot(),
2734 )
2735 }
2736
2737 fn abs_path(&self, entry: &PanelEntry, cx: &AppContext) -> Option<PathBuf> {
2738 match entry {
2739 PanelEntry::Fs(
2740 FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
2741 ) => self
2742 .buffer_snapshot_for_id(*buffer_id, cx)
2743 .and_then(|buffer_snapshot| {
2744 let file = File::from_dyn(buffer_snapshot.file())?;
2745 file.worktree.read(cx).absolutize(&file.path).ok()
2746 }),
2747 PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self
2748 .project
2749 .read(cx)
2750 .worktree_for_id(*worktree_id, cx)?
2751 .read(cx)
2752 .absolutize(&entry.path)
2753 .ok(),
2754 PanelEntry::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
2755 self.project
2756 .read(cx)
2757 .worktree_for_id(*worktree_id, cx)
2758 .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
2759 }),
2760 PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
2761 }
2762 }
2763
2764 fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option<Arc<Path>> {
2765 match entry {
2766 FsEntry::ExternalFile(buffer_id, _) => {
2767 let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
2768 Some(buffer_snapshot.file()?.path().clone())
2769 }
2770 FsEntry::Directory(_, entry) => Some(entry.path.clone()),
2771 FsEntry::File(_, entry, ..) => Some(entry.path.clone()),
2772 }
2773 }
2774
2775 fn update_cached_entries(
2776 &mut self,
2777 debounce: Option<Duration>,
2778 cx: &mut ViewContext<OutlinePanel>,
2779 ) {
2780 if !self.active {
2781 return;
2782 }
2783
2784 let is_singleton = self.is_singleton_active(cx);
2785 let query = self.query(cx);
2786 self.cached_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
2787 if let Some(debounce) = debounce {
2788 cx.background_executor().timer(debounce).await;
2789 }
2790 let Some(new_cached_entries) = outline_panel
2791 .update(&mut cx, |outline_panel, cx| {
2792 outline_panel.generate_cached_entries(is_singleton, query, cx)
2793 })
2794 .ok()
2795 else {
2796 return;
2797 };
2798 let new_cached_entries = new_cached_entries.await;
2799 outline_panel
2800 .update(&mut cx, |outline_panel, cx| {
2801 outline_panel.cached_entries = new_cached_entries;
2802 if outline_panel.selected_entry.is_invalidated() {
2803 if let Some(new_selected_entry) =
2804 outline_panel.active_editor().and_then(|active_editor| {
2805 outline_panel.location_for_editor_selection(&active_editor, cx)
2806 })
2807 {
2808 outline_panel.select_entry(new_selected_entry, false, cx);
2809 }
2810 }
2811
2812 outline_panel.autoscroll(cx);
2813 cx.notify();
2814 })
2815 .ok();
2816 });
2817 }
2818
2819 fn generate_cached_entries(
2820 &self,
2821 is_singleton: bool,
2822 query: Option<String>,
2823 cx: &mut ViewContext<'_, Self>,
2824 ) -> Task<Vec<CachedEntry>> {
2825 let project = self.project.clone();
2826 cx.spawn(|outline_panel, mut cx| async move {
2827 let mut entries = Vec::new();
2828 let mut match_candidates = Vec::new();
2829
2830 let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
2831 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2832 let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
2833 let track_matches = query.is_some();
2834
2835 #[derive(Debug)]
2836 struct ParentStats {
2837 path: Arc<Path>,
2838 folded: bool,
2839 expanded: bool,
2840 depth: usize,
2841 }
2842 let mut parent_dirs = Vec::<ParentStats>::new();
2843 for entry in outline_panel.fs_entries.clone() {
2844 let is_expanded = outline_panel.is_expanded(&entry);
2845 let (depth, should_add) = match &entry {
2846 FsEntry::Directory(worktree_id, dir_entry) => {
2847 let mut should_add = true;
2848 let is_root = project
2849 .read(cx)
2850 .worktree_for_id(*worktree_id, cx)
2851 .map_or(false, |worktree| {
2852 worktree.read(cx).root_entry() == Some(dir_entry)
2853 });
2854 let folded = auto_fold_dirs
2855 && !is_root
2856 && outline_panel
2857 .unfolded_dirs
2858 .get(worktree_id)
2859 .map_or(true, |unfolded_dirs| {
2860 !unfolded_dirs.contains(&dir_entry.id)
2861 });
2862 let fs_depth = outline_panel
2863 .fs_entries_depth
2864 .get(&(*worktree_id, dir_entry.id))
2865 .copied()
2866 .unwrap_or(0);
2867 while let Some(parent) = parent_dirs.last() {
2868 if dir_entry.path.starts_with(&parent.path) {
2869 break;
2870 }
2871 parent_dirs.pop();
2872 }
2873 let auto_fold = match parent_dirs.last() {
2874 Some(parent) => {
2875 parent.folded
2876 && Some(parent.path.as_ref()) == dir_entry.path.parent()
2877 && outline_panel
2878 .fs_children_count
2879 .get(worktree_id)
2880 .and_then(|entries| entries.get(&dir_entry.path))
2881 .copied()
2882 .unwrap_or_default()
2883 .may_be_fold_part()
2884 }
2885 None => false,
2886 };
2887 let folded = folded || auto_fold;
2888 let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
2889 Some(parent) => {
2890 let parent_folded = parent.folded;
2891 let parent_expanded = parent.expanded;
2892 let new_depth = if parent_folded {
2893 parent.depth
2894 } else {
2895 parent.depth + 1
2896 };
2897 parent_dirs.push(ParentStats {
2898 path: dir_entry.path.clone(),
2899 folded,
2900 expanded: parent_expanded && is_expanded,
2901 depth: new_depth,
2902 });
2903 (new_depth, parent_expanded, parent_folded)
2904 }
2905 None => {
2906 parent_dirs.push(ParentStats {
2907 path: dir_entry.path.clone(),
2908 folded,
2909 expanded: is_expanded,
2910 depth: fs_depth,
2911 });
2912 (fs_depth, true, false)
2913 }
2914 };
2915
2916 if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) =
2917 folded_dirs_entry.take()
2918 {
2919 if folded
2920 && worktree_id == &folded_worktree_id
2921 && dir_entry.path.parent()
2922 == folded_dirs.last().map(|entry| entry.path.as_ref())
2923 {
2924 folded_dirs.push(dir_entry.clone());
2925 folded_dirs_entry =
2926 Some((folded_depth, folded_worktree_id, folded_dirs))
2927 } else {
2928 if !is_singleton {
2929 let start_of_collapsed_dir_sequence = !parent_expanded
2930 && parent_dirs
2931 .iter()
2932 .rev()
2933 .nth(folded_dirs.len() + 1)
2934 .map_or(true, |parent| parent.expanded);
2935 if start_of_collapsed_dir_sequence
2936 || parent_expanded
2937 || query.is_some()
2938 {
2939 if parent_folded {
2940 folded_dirs.push(dir_entry.clone());
2941 should_add = false;
2942 }
2943 let new_folded_dirs = PanelEntry::FoldedDirs(
2944 folded_worktree_id,
2945 folded_dirs,
2946 );
2947 outline_panel.push_entry(
2948 &mut entries,
2949 &mut match_candidates,
2950 track_matches,
2951 new_folded_dirs,
2952 folded_depth,
2953 cx,
2954 );
2955 }
2956 }
2957
2958 folded_dirs_entry = if parent_folded {
2959 None
2960 } else {
2961 Some((depth, *worktree_id, vec![dir_entry.clone()]))
2962 };
2963 }
2964 } else if folded {
2965 folded_dirs_entry =
2966 Some((depth, *worktree_id, vec![dir_entry.clone()]));
2967 }
2968
2969 let should_add =
2970 should_add && parent_expanded && folded_dirs_entry.is_none();
2971 (depth, should_add)
2972 }
2973 FsEntry::ExternalFile(..) => {
2974 if let Some((folded_depth, worktree_id, folded_dirs)) =
2975 folded_dirs_entry.take()
2976 {
2977 let parent_expanded = parent_dirs
2978 .iter()
2979 .rev()
2980 .find(|parent| {
2981 folded_dirs.iter().all(|entry| entry.path != parent.path)
2982 })
2983 .map_or(true, |parent| parent.expanded);
2984 if !is_singleton && (parent_expanded || query.is_some()) {
2985 outline_panel.push_entry(
2986 &mut entries,
2987 &mut match_candidates,
2988 track_matches,
2989 PanelEntry::FoldedDirs(worktree_id, folded_dirs),
2990 folded_depth,
2991 cx,
2992 );
2993 }
2994 }
2995 parent_dirs.clear();
2996 (0, true)
2997 }
2998 FsEntry::File(worktree_id, file_entry, ..) => {
2999 if let Some((folded_depth, worktree_id, folded_dirs)) =
3000 folded_dirs_entry.take()
3001 {
3002 let parent_expanded = parent_dirs
3003 .iter()
3004 .rev()
3005 .find(|parent| {
3006 folded_dirs.iter().all(|entry| entry.path != parent.path)
3007 })
3008 .map_or(true, |parent| parent.expanded);
3009 if !is_singleton && (parent_expanded || query.is_some()) {
3010 outline_panel.push_entry(
3011 &mut entries,
3012 &mut match_candidates,
3013 track_matches,
3014 PanelEntry::FoldedDirs(worktree_id, folded_dirs),
3015 folded_depth,
3016 cx,
3017 );
3018 }
3019 }
3020
3021 let fs_depth = outline_panel
3022 .fs_entries_depth
3023 .get(&(*worktree_id, file_entry.id))
3024 .copied()
3025 .unwrap_or(0);
3026 while let Some(parent) = parent_dirs.last() {
3027 if file_entry.path.starts_with(&parent.path) {
3028 break;
3029 }
3030 parent_dirs.pop();
3031 }
3032 let (depth, should_add) = match parent_dirs.last() {
3033 Some(parent) => {
3034 let new_depth = parent.depth + 1;
3035 (new_depth, parent.expanded)
3036 }
3037 None => (fs_depth, true),
3038 };
3039 (depth, should_add)
3040 }
3041 };
3042
3043 if !is_singleton
3044 && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3045 {
3046 outline_panel.push_entry(
3047 &mut entries,
3048 &mut match_candidates,
3049 track_matches,
3050 PanelEntry::Fs(entry.clone()),
3051 depth,
3052 cx,
3053 );
3054 }
3055
3056 match outline_panel.mode {
3057 ItemsDisplayMode::Search(_) => {
3058 if is_singleton || query.is_some() || (should_add && is_expanded) {
3059 outline_panel.add_search_entries(
3060 &mut entries,
3061 &mut match_candidates,
3062 entry.clone(),
3063 depth,
3064 query.clone(),
3065 is_singleton,
3066 cx,
3067 );
3068 }
3069 }
3070 ItemsDisplayMode::Outline => {
3071 let excerpts_to_consider =
3072 if is_singleton || query.is_some() || (should_add && is_expanded) {
3073 match &entry {
3074 FsEntry::File(_, _, buffer_id, entry_excerpts) => {
3075 Some((*buffer_id, entry_excerpts))
3076 }
3077 FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
3078 Some((*buffer_id, entry_excerpts))
3079 }
3080 _ => None,
3081 }
3082 } else {
3083 None
3084 };
3085 if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
3086 outline_panel.add_excerpt_entries(
3087 buffer_id,
3088 entry_excerpts,
3089 depth,
3090 track_matches,
3091 is_singleton,
3092 query.as_deref(),
3093 &mut entries,
3094 &mut match_candidates,
3095 cx,
3096 );
3097 }
3098 }
3099 }
3100
3101 if is_singleton
3102 && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
3103 && !entries.iter().any(|item| {
3104 matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
3105 })
3106 {
3107 outline_panel.push_entry(
3108 &mut entries,
3109 &mut match_candidates,
3110 track_matches,
3111 PanelEntry::Fs(entry.clone()),
3112 0,
3113 cx,
3114 );
3115 }
3116 }
3117
3118 if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
3119 let parent_expanded = parent_dirs
3120 .iter()
3121 .rev()
3122 .find(|parent| folded_dirs.iter().all(|entry| entry.path != parent.path))
3123 .map_or(true, |parent| parent.expanded);
3124 if parent_expanded || query.is_some() {
3125 outline_panel.push_entry(
3126 &mut entries,
3127 &mut match_candidates,
3128 track_matches,
3129 PanelEntry::FoldedDirs(worktree_id, folded_dirs),
3130 folded_depth,
3131 cx,
3132 );
3133 }
3134 }
3135 }) else {
3136 return Vec::new();
3137 };
3138
3139 let Some(query) = query else {
3140 return entries;
3141 };
3142
3143 let mut matched_ids = match_strings(
3144 &match_candidates,
3145 &query,
3146 true,
3147 usize::MAX,
3148 &AtomicBool::default(),
3149 cx.background_executor().clone(),
3150 )
3151 .await
3152 .into_iter()
3153 .map(|string_match| (string_match.candidate_id, string_match))
3154 .collect::<HashMap<_, _>>();
3155
3156 let mut id = 0;
3157 entries.retain_mut(|cached_entry| {
3158 let retain = match matched_ids.remove(&id) {
3159 Some(string_match) => {
3160 cached_entry.string_match = Some(string_match);
3161 true
3162 }
3163 None => false,
3164 };
3165 id += 1;
3166 retain
3167 });
3168
3169 entries
3170 })
3171 }
3172
3173 #[allow(clippy::too_many_arguments)]
3174 fn push_entry(
3175 &self,
3176 entries: &mut Vec<CachedEntry>,
3177 match_candidates: &mut Vec<StringMatchCandidate>,
3178 track_matches: bool,
3179 entry: PanelEntry,
3180 depth: usize,
3181 cx: &mut WindowContext,
3182 ) {
3183 let entry = if let PanelEntry::FoldedDirs(worktree_id, entries) = &entry {
3184 match entries.len() {
3185 0 => {
3186 debug_panic!("Empty folded dirs receiver");
3187 return;
3188 }
3189 1 => PanelEntry::Fs(FsEntry::Directory(*worktree_id, entries[0].clone())),
3190 _ => entry,
3191 }
3192 } else {
3193 entry
3194 };
3195
3196 if track_matches {
3197 let id = entries.len();
3198 match &entry {
3199 PanelEntry::Fs(fs_entry) => {
3200 if let Some(file_name) =
3201 self.relative_path(fs_entry, cx).as_deref().map(file_name)
3202 {
3203 match_candidates.push(StringMatchCandidate {
3204 id,
3205 string: file_name.to_string(),
3206 char_bag: file_name.chars().collect(),
3207 });
3208 }
3209 }
3210 PanelEntry::FoldedDirs(worktree_id, entries) => {
3211 let dir_names = self.dir_names_string(entries, *worktree_id, cx);
3212 {
3213 match_candidates.push(StringMatchCandidate {
3214 id,
3215 string: dir_names.clone(),
3216 char_bag: dir_names.chars().collect(),
3217 });
3218 }
3219 }
3220 PanelEntry::Outline(outline_entry) => match outline_entry {
3221 OutlineEntry::Outline(_, _, outline) => {
3222 match_candidates.push(StringMatchCandidate {
3223 id,
3224 string: outline.text.clone(),
3225 char_bag: outline.text.chars().collect(),
3226 });
3227 }
3228 OutlineEntry::Excerpt(..) => {}
3229 },
3230 PanelEntry::Search(new_search_entry) => {
3231 match_candidates.push(StringMatchCandidate {
3232 id,
3233 char_bag: new_search_entry.render_data.context_text.chars().collect(),
3234 string: new_search_entry.render_data.context_text.clone(),
3235 });
3236 }
3237 }
3238 }
3239 entries.push(CachedEntry {
3240 depth,
3241 entry,
3242 string_match: None,
3243 });
3244 }
3245
3246 fn dir_names_string(
3247 &self,
3248 entries: &[Entry],
3249 worktree_id: WorktreeId,
3250 cx: &AppContext,
3251 ) -> String {
3252 let dir_names_segment = entries
3253 .iter()
3254 .map(|entry| self.entry_name(&worktree_id, entry, cx))
3255 .collect::<PathBuf>();
3256 dir_names_segment.to_string_lossy().to_string()
3257 }
3258
3259 fn query(&self, cx: &AppContext) -> Option<String> {
3260 let query = self.filter_editor.read(cx).text(cx);
3261 if query.trim().is_empty() {
3262 None
3263 } else {
3264 Some(query)
3265 }
3266 }
3267
3268 fn is_expanded(&self, entry: &FsEntry) -> bool {
3269 let entry_to_check = match entry {
3270 FsEntry::ExternalFile(buffer_id, _) => CollapsedEntry::ExternalFile(*buffer_id),
3271 FsEntry::File(worktree_id, _, buffer_id, _) => {
3272 CollapsedEntry::File(*worktree_id, *buffer_id)
3273 }
3274 FsEntry::Directory(worktree_id, entry) => CollapsedEntry::Dir(*worktree_id, entry.id),
3275 };
3276 !self.collapsed_entries.contains(&entry_to_check)
3277 }
3278
3279 fn update_non_fs_items(&mut self, cx: &mut ViewContext<OutlinePanel>) {
3280 if !self.active {
3281 return;
3282 }
3283
3284 self.update_search_matches(cx);
3285 self.fetch_outdated_outlines(cx);
3286 self.autoscroll(cx);
3287 }
3288
3289 fn update_search_matches(&mut self, cx: &mut ViewContext<OutlinePanel>) {
3290 if !self.active {
3291 return;
3292 }
3293
3294 let project_search = self
3295 .active_item()
3296 .and_then(|item| item.downcast::<ProjectSearchView>());
3297 let project_search_matches = project_search
3298 .as_ref()
3299 .map(|project_search| project_search.read(cx).get_matches(cx))
3300 .unwrap_or_default();
3301
3302 let buffer_search = self
3303 .active_item()
3304 .as_deref()
3305 .and_then(|active_item| {
3306 self.workspace
3307 .upgrade()
3308 .and_then(|workspace| workspace.read(cx).pane_for(active_item))
3309 })
3310 .and_then(|pane| {
3311 pane.read(cx)
3312 .toolbar()
3313 .read(cx)
3314 .item_of_type::<BufferSearchBar>()
3315 });
3316 let buffer_search_matches = self
3317 .active_editor()
3318 .map(|active_editor| active_editor.update(cx, |editor, cx| editor.get_matches(cx)))
3319 .unwrap_or_default();
3320
3321 let mut update_cached_entries = false;
3322 if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
3323 if matches!(self.mode, ItemsDisplayMode::Search(_)) {
3324 self.mode = ItemsDisplayMode::Outline;
3325 update_cached_entries = true;
3326 }
3327 } else {
3328 let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
3329 (
3330 SearchKind::Project,
3331 project_search_matches,
3332 project_search
3333 .map(|project_search| project_search.read(cx).search_query_text(cx))
3334 .unwrap_or_default(),
3335 )
3336 } else {
3337 (
3338 SearchKind::Buffer,
3339 buffer_search_matches,
3340 buffer_search
3341 .map(|buffer_search| buffer_search.read(cx).query(cx))
3342 .unwrap_or_default(),
3343 )
3344 };
3345
3346 update_cached_entries = match &self.mode {
3347 ItemsDisplayMode::Search(current_search_state) => {
3348 current_search_state.query != new_search_query
3349 || current_search_state.kind != kind
3350 || current_search_state.matches.is_empty()
3351 || current_search_state.matches.iter().enumerate().any(
3352 |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
3353 )
3354 }
3355 ItemsDisplayMode::Outline => true,
3356 };
3357 self.mode = ItemsDisplayMode::Search(SearchState::new(
3358 kind,
3359 new_search_query,
3360 new_search_matches,
3361 cx.theme().syntax().clone(),
3362 cx,
3363 ));
3364 }
3365 if update_cached_entries {
3366 self.selected_entry.invalidate();
3367 self.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
3368 }
3369 }
3370
3371 #[allow(clippy::too_many_arguments)]
3372 fn add_excerpt_entries(
3373 &self,
3374 buffer_id: BufferId,
3375 entries_to_add: &[ExcerptId],
3376 parent_depth: usize,
3377 track_matches: bool,
3378 is_singleton: bool,
3379 query: Option<&str>,
3380 entries: &mut Vec<CachedEntry>,
3381 match_candidates: &mut Vec<StringMatchCandidate>,
3382 cx: &mut ViewContext<Self>,
3383 ) {
3384 if let Some(excerpts) = self.excerpts.get(&buffer_id) {
3385 for &excerpt_id in entries_to_add {
3386 let Some(excerpt) = excerpts.get(&excerpt_id) else {
3387 continue;
3388 };
3389 let excerpt_depth = parent_depth + 1;
3390 self.push_entry(
3391 entries,
3392 match_candidates,
3393 track_matches,
3394 PanelEntry::Outline(OutlineEntry::Excerpt(
3395 buffer_id,
3396 excerpt_id,
3397 excerpt.range.clone(),
3398 )),
3399 excerpt_depth,
3400 cx,
3401 );
3402
3403 let mut outline_base_depth = excerpt_depth + 1;
3404 if is_singleton {
3405 outline_base_depth = 0;
3406 entries.clear();
3407 match_candidates.clear();
3408 } else if query.is_none()
3409 && self
3410 .collapsed_entries
3411 .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
3412 {
3413 continue;
3414 }
3415
3416 for outline in excerpt.iter_outlines() {
3417 self.push_entry(
3418 entries,
3419 match_candidates,
3420 track_matches,
3421 PanelEntry::Outline(OutlineEntry::Outline(
3422 buffer_id,
3423 excerpt_id,
3424 outline.clone(),
3425 )),
3426 outline_base_depth + outline.depth,
3427 cx,
3428 );
3429 }
3430 }
3431 }
3432 }
3433
3434 #[allow(clippy::too_many_arguments)]
3435 fn add_search_entries(
3436 &mut self,
3437 entries: &mut Vec<CachedEntry>,
3438 match_candidates: &mut Vec<StringMatchCandidate>,
3439 parent_entry: FsEntry,
3440 parent_depth: usize,
3441 filter_query: Option<String>,
3442 is_singleton: bool,
3443 cx: &mut ViewContext<Self>,
3444 ) {
3445 let Some(active_editor) = self.active_editor() else {
3446 return;
3447 };
3448 let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
3449 return;
3450 };
3451
3452 let kind = search_state.kind;
3453 let related_excerpts = match &parent_entry {
3454 FsEntry::Directory(_, _) => return,
3455 FsEntry::ExternalFile(_, excerpts) => excerpts,
3456 FsEntry::File(_, _, _, excerpts) => excerpts,
3457 }
3458 .iter()
3459 .copied()
3460 .collect::<HashSet<_>>();
3461
3462 let depth = if is_singleton { 0 } else { parent_depth + 1 };
3463 let multi_buffer_snapshot = active_editor.read(cx).buffer().read(cx).snapshot(cx);
3464 let new_search_matches = search_state.matches.iter().filter(|(match_range, _)| {
3465 related_excerpts.contains(&match_range.start.excerpt_id)
3466 || related_excerpts.contains(&match_range.end.excerpt_id)
3467 });
3468
3469 let previous_search_matches = entries
3470 .iter()
3471 .skip_while(|entry| {
3472 if let PanelEntry::Fs(entry) = &entry.entry {
3473 entry == &parent_entry
3474 } else {
3475 true
3476 }
3477 })
3478 .take_while(|entry| matches!(entry.entry, PanelEntry::Search(_)))
3479 .fold(
3480 HashMap::default(),
3481 |mut previous_matches, previous_entry| match &previous_entry.entry {
3482 PanelEntry::Search(search_entry) => {
3483 previous_matches.insert(
3484 (search_entry.kind, &search_entry.match_range),
3485 &search_entry.render_data,
3486 );
3487 previous_matches
3488 }
3489 _ => previous_matches,
3490 },
3491 );
3492
3493 let new_search_entries = new_search_matches
3494 .map(|(match_range, search_data)| {
3495 let previous_search_data =
3496 previous_search_matches.get(&(kind, match_range)).copied();
3497 let render_data = search_data
3498 .get()
3499 .or(previous_search_data)
3500 .unwrap_or_else(|| {
3501 search_data.get_or_init(|| {
3502 Arc::new(SearchData::new(match_range, &multi_buffer_snapshot))
3503 })
3504 });
3505 if let (Some(previous_highlights), None) = (
3506 previous_search_data.and_then(|data| data.highlights_data.get()),
3507 render_data.highlights_data.get(),
3508 ) {
3509 render_data
3510 .highlights_data
3511 .set(previous_highlights.clone())
3512 .ok();
3513 }
3514
3515 SearchEntry {
3516 match_range: match_range.clone(),
3517 kind,
3518 render_data: Arc::clone(render_data),
3519 }
3520 })
3521 .collect::<Vec<_>>();
3522 for new_search_entry in new_search_entries {
3523 self.push_entry(
3524 entries,
3525 match_candidates,
3526 filter_query.is_some(),
3527 PanelEntry::Search(new_search_entry),
3528 depth,
3529 cx,
3530 );
3531 }
3532 }
3533
3534 fn active_editor(&self) -> Option<View<Editor>> {
3535 self.active_item.as_ref()?.active_editor.upgrade()
3536 }
3537
3538 fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
3539 self.active_item.as_ref()?.item_handle.upgrade()
3540 }
3541
3542 fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
3543 self.active_item().map_or(true, |active_item| {
3544 !self.pinned && active_item.item_id() != new_active_item.item_id()
3545 })
3546 }
3547
3548 pub fn toggle_active_editor_pin(
3549 &mut self,
3550 _: &ToggleActiveEditorPin,
3551 cx: &mut ViewContext<Self>,
3552 ) {
3553 self.pinned = !self.pinned;
3554 if !self.pinned {
3555 if let Some((active_item, active_editor)) = self
3556 .workspace
3557 .upgrade()
3558 .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
3559 {
3560 if self.should_replace_active_item(active_item.as_ref()) {
3561 self.replace_active_editor(active_item, active_editor, cx);
3562 }
3563 }
3564 }
3565
3566 cx.notify();
3567 }
3568
3569 fn selected_entry(&self) -> Option<&PanelEntry> {
3570 match &self.selected_entry {
3571 SelectedEntry::Invalidated(entry) => entry.as_ref(),
3572 SelectedEntry::Valid(entry, _) => Some(entry),
3573 SelectedEntry::None => None,
3574 }
3575 }
3576
3577 fn select_entry(&mut self, entry: PanelEntry, focus: bool, cx: &mut ViewContext<Self>) {
3578 if focus {
3579 self.focus_handle.focus(cx);
3580 }
3581 let ix = self
3582 .cached_entries
3583 .iter()
3584 .enumerate()
3585 .find(|(_, cached_entry)| &cached_entry.entry == &entry)
3586 .map(|(i, _)| i)
3587 .unwrap_or_default();
3588
3589 self.selected_entry = SelectedEntry::Valid(entry, ix);
3590
3591 self.autoscroll(cx);
3592 cx.notify();
3593 }
3594}
3595
3596fn workspace_active_editor(
3597 workspace: &Workspace,
3598 cx: &AppContext,
3599) -> Option<(Box<dyn ItemHandle>, View<Editor>)> {
3600 let active_item = workspace.active_item(cx)?;
3601 let active_editor = active_item
3602 .act_as::<Editor>(cx)
3603 .filter(|editor| editor.read(cx).mode() == EditorMode::Full)?;
3604 Some((active_item, active_editor))
3605}
3606
3607fn back_to_common_visited_parent(
3608 visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
3609 worktree_id: &WorktreeId,
3610 new_entry: &Entry,
3611) -> Option<(WorktreeId, ProjectEntryId)> {
3612 while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
3613 match new_entry.path.parent() {
3614 Some(parent_path) => {
3615 if parent_path == visited_path.as_ref() {
3616 return Some((*worktree_id, *visited_dir_id));
3617 }
3618 }
3619 None => {
3620 break;
3621 }
3622 }
3623 visited_dirs.pop();
3624 }
3625 None
3626}
3627
3628fn file_name(path: &Path) -> String {
3629 let mut current_path = path;
3630 loop {
3631 if let Some(file_name) = current_path.file_name() {
3632 return file_name.to_string_lossy().into_owned();
3633 }
3634 match current_path.parent() {
3635 Some(parent) => current_path = parent,
3636 None => return path.to_string_lossy().into_owned(),
3637 }
3638 }
3639}
3640
3641impl Panel for OutlinePanel {
3642 fn persistent_name() -> &'static str {
3643 "Outline Panel"
3644 }
3645
3646 fn position(&self, cx: &WindowContext) -> DockPosition {
3647 match OutlinePanelSettings::get_global(cx).dock {
3648 OutlinePanelDockPosition::Left => DockPosition::Left,
3649 OutlinePanelDockPosition::Right => DockPosition::Right,
3650 }
3651 }
3652
3653 fn position_is_valid(&self, position: DockPosition) -> bool {
3654 matches!(position, DockPosition::Left | DockPosition::Right)
3655 }
3656
3657 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3658 settings::update_settings_file::<OutlinePanelSettings>(
3659 self.fs.clone(),
3660 cx,
3661 move |settings, _| {
3662 let dock = match position {
3663 DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
3664 DockPosition::Right => OutlinePanelDockPosition::Right,
3665 };
3666 settings.dock = Some(dock);
3667 },
3668 );
3669 }
3670
3671 fn size(&self, cx: &WindowContext) -> Pixels {
3672 self.width
3673 .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
3674 }
3675
3676 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
3677 self.width = size;
3678 self.serialize(cx);
3679 cx.notify();
3680 }
3681
3682 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
3683 OutlinePanelSettings::get_global(cx)
3684 .button
3685 .then_some(IconName::ListTree)
3686 }
3687
3688 fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
3689 Some("Outline Panel")
3690 }
3691
3692 fn toggle_action(&self) -> Box<dyn Action> {
3693 Box::new(ToggleFocus)
3694 }
3695
3696 fn starts_open(&self, _: &WindowContext) -> bool {
3697 self.active
3698 }
3699
3700 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3701 cx.spawn(|outline_panel, mut cx| async move {
3702 outline_panel
3703 .update(&mut cx, |outline_panel, cx| {
3704 let old_active = outline_panel.active;
3705 outline_panel.active = active;
3706 if active && old_active != active {
3707 if let Some((active_item, active_editor)) = outline_panel
3708 .workspace
3709 .upgrade()
3710 .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
3711 {
3712 if outline_panel.should_replace_active_item(active_item.as_ref()) {
3713 outline_panel.replace_active_editor(active_item, active_editor, cx);
3714 } else {
3715 outline_panel.update_fs_entries(
3716 &active_editor,
3717 HashSet::default(),
3718 None,
3719 cx,
3720 )
3721 }
3722 } else if !outline_panel.pinned {
3723 outline_panel.clear_previous(cx);
3724 }
3725 }
3726 outline_panel.serialize(cx);
3727 })
3728 .ok();
3729 })
3730 .detach()
3731 }
3732}
3733
3734impl FocusableView for OutlinePanel {
3735 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3736 self.filter_editor.focus_handle(cx).clone()
3737 }
3738}
3739
3740impl EventEmitter<Event> for OutlinePanel {}
3741
3742impl EventEmitter<PanelEvent> for OutlinePanel {}
3743
3744impl Render for OutlinePanel {
3745 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3746 let project = self.project.read(cx);
3747 let query = self.query(cx);
3748 let pinned = self.pinned;
3749 let settings = OutlinePanelSettings::get_global(cx);
3750 let indent_size = settings.indent_size;
3751 let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
3752
3753 let outline_panel = v_flex()
3754 .id("outline-panel")
3755 .size_full()
3756 .relative()
3757 .key_context(self.dispatch_context(cx))
3758 .on_action(cx.listener(Self::open))
3759 .on_action(cx.listener(Self::cancel))
3760 .on_action(cx.listener(Self::select_next))
3761 .on_action(cx.listener(Self::select_prev))
3762 .on_action(cx.listener(Self::select_first))
3763 .on_action(cx.listener(Self::select_last))
3764 .on_action(cx.listener(Self::select_parent))
3765 .on_action(cx.listener(Self::expand_selected_entry))
3766 .on_action(cx.listener(Self::collapse_selected_entry))
3767 .on_action(cx.listener(Self::expand_all_entries))
3768 .on_action(cx.listener(Self::collapse_all_entries))
3769 .on_action(cx.listener(Self::copy_path))
3770 .on_action(cx.listener(Self::copy_relative_path))
3771 .on_action(cx.listener(Self::toggle_active_editor_pin))
3772 .on_action(cx.listener(Self::unfold_directory))
3773 .on_action(cx.listener(Self::fold_directory))
3774 .when(project.is_local(), |el| {
3775 el.on_action(cx.listener(Self::reveal_in_finder))
3776 })
3777 .when(project.is_local() || project.is_via_ssh(), |el| {
3778 el.on_action(cx.listener(Self::open_in_terminal))
3779 })
3780 .on_mouse_down(
3781 MouseButton::Right,
3782 cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
3783 if let Some(entry) = outline_panel.selected_entry().cloned() {
3784 outline_panel.deploy_context_menu(event.position, entry, cx)
3785 } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
3786 outline_panel.deploy_context_menu(event.position, PanelEntry::Fs(entry), cx)
3787 }
3788 }),
3789 )
3790 .track_focus(&self.focus_handle(cx));
3791
3792 if self.cached_entries.is_empty() {
3793 let header = if self.updating_fs_entries {
3794 "Loading outlines"
3795 } else if query.is_some() {
3796 "No matches for query"
3797 } else {
3798 "No outlines available"
3799 };
3800
3801 outline_panel.child(
3802 v_flex()
3803 .justify_center()
3804 .size_full()
3805 .child(h_flex().justify_center().child(Label::new(header)))
3806 .when_some(query.clone(), |panel, query| {
3807 panel.child(h_flex().justify_center().child(Label::new(query)))
3808 })
3809 .child(
3810 h_flex()
3811 .pt(Spacing::Small.rems(cx))
3812 .justify_center()
3813 .child({
3814 let keystroke = match self.position(cx) {
3815 DockPosition::Left => {
3816 cx.keystroke_text_for(&workspace::ToggleLeftDock)
3817 }
3818 DockPosition::Bottom => {
3819 cx.keystroke_text_for(&workspace::ToggleBottomDock)
3820 }
3821 DockPosition::Right => {
3822 cx.keystroke_text_for(&workspace::ToggleRightDock)
3823 }
3824 };
3825 Label::new(format!("Toggle this panel with {keystroke}"))
3826 }),
3827 ),
3828 )
3829 } else {
3830 let search_query = match &self.mode {
3831 ItemsDisplayMode::Search(search_query) => Some(search_query),
3832 _ => None,
3833 };
3834 outline_panel
3835 .when_some(search_query, |outline_panel, search_state| {
3836 outline_panel.child(
3837 div()
3838 .mx_2()
3839 .child(
3840 Label::new(format!("Searching: '{}'", search_state.query))
3841 .color(Color::Muted),
3842 )
3843 .child(horizontal_separator(cx)),
3844 )
3845 })
3846 .child({
3847 let items_len = self.cached_entries.len();
3848 let multi_buffer_snapshot = self
3849 .active_editor()
3850 .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
3851 uniform_list(cx.view().clone(), "entries", items_len, {
3852 move |outline_panel, range, cx| {
3853 let entries = outline_panel.cached_entries.get(range);
3854 entries
3855 .map(|entries| entries.to_vec())
3856 .unwrap_or_default()
3857 .into_iter()
3858 .filter_map(|cached_entry| match cached_entry.entry {
3859 PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
3860 &entry,
3861 cached_entry.depth,
3862 cached_entry.string_match.as_ref(),
3863 cx,
3864 )),
3865 PanelEntry::FoldedDirs(worktree_id, entries) => {
3866 Some(outline_panel.render_folded_dirs(
3867 worktree_id,
3868 &entries,
3869 cached_entry.depth,
3870 cached_entry.string_match.as_ref(),
3871 cx,
3872 ))
3873 }
3874 PanelEntry::Outline(OutlineEntry::Excerpt(
3875 buffer_id,
3876 excerpt_id,
3877 excerpt,
3878 )) => outline_panel.render_excerpt(
3879 buffer_id,
3880 excerpt_id,
3881 &excerpt,
3882 cached_entry.depth,
3883 cx,
3884 ),
3885 PanelEntry::Outline(OutlineEntry::Outline(
3886 buffer_id,
3887 excerpt_id,
3888 outline,
3889 )) => Some(outline_panel.render_outline(
3890 buffer_id,
3891 excerpt_id,
3892 &outline,
3893 cached_entry.depth,
3894 cached_entry.string_match.as_ref(),
3895 cx,
3896 )),
3897 PanelEntry::Search(SearchEntry {
3898 match_range,
3899 render_data,
3900 kind,
3901 ..
3902 }) => Some(outline_panel.render_search_match(
3903 multi_buffer_snapshot.as_ref(),
3904 &match_range,
3905 &render_data,
3906 kind,
3907 cached_entry.depth,
3908 cached_entry.string_match.as_ref(),
3909 cx,
3910 )),
3911 })
3912 .collect()
3913 }
3914 })
3915 .size_full()
3916 .track_scroll(self.scroll_handle.clone())
3917 .when(show_indent_guides, |list| {
3918 list.with_decoration(
3919 ui::indent_guides(
3920 cx.view().clone(),
3921 px(indent_size),
3922 IndentGuideColors::panel(cx),
3923 |outline_panel, range, _| {
3924 let entries = outline_panel.cached_entries.get(range);
3925 if let Some(entries) = entries {
3926 entries.into_iter().map(|item| item.depth).collect()
3927 } else {
3928 smallvec::SmallVec::new()
3929 }
3930 },
3931 )
3932 .with_render_fn(
3933 cx.view().clone(),
3934 move |outline_panel, params, _| {
3935 const LEFT_OFFSET: f32 = 14.;
3936
3937 let indent_size = params.indent_size;
3938 let item_height = params.item_height;
3939 let active_indent_guide_ix = find_active_indent_guide_ix(
3940 outline_panel,
3941 ¶ms.indent_guides,
3942 );
3943
3944 params
3945 .indent_guides
3946 .into_iter()
3947 .enumerate()
3948 .map(|(ix, layout)| {
3949 let bounds = Bounds::new(
3950 point(
3951 px(layout.offset.x as f32) * indent_size
3952 + px(LEFT_OFFSET),
3953 px(layout.offset.y as f32) * item_height,
3954 ),
3955 size(
3956 px(1.),
3957 px(layout.length as f32) * item_height,
3958 ),
3959 );
3960 ui::RenderedIndentGuide {
3961 bounds,
3962 layout,
3963 is_active: active_indent_guide_ix == Some(ix),
3964 hitbox: None,
3965 }
3966 })
3967 .collect()
3968 },
3969 ),
3970 )
3971 })
3972 })
3973 }
3974 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3975 deferred(
3976 anchored()
3977 .position(*position)
3978 .anchor(gpui::AnchorCorner::TopLeft)
3979 .child(menu.clone()),
3980 )
3981 .with_priority(1)
3982 }))
3983 .child(
3984 v_flex().child(horizontal_separator(cx)).child(
3985 h_flex().p_2().child(self.filter_editor.clone()).child(
3986 div().child(
3987 IconButton::new(
3988 "outline-panel-menu",
3989 if pinned {
3990 IconName::Unpin
3991 } else {
3992 IconName::Pin
3993 },
3994 )
3995 .tooltip(move |cx| {
3996 Tooltip::text(
3997 if pinned {
3998 "Unpin Outline"
3999 } else {
4000 "Pin Active Outline"
4001 },
4002 cx,
4003 )
4004 })
4005 .shape(IconButtonShape::Square)
4006 .on_click(cx.listener(|outline_panel, _, cx| {
4007 outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, cx);
4008 })),
4009 ),
4010 ),
4011 ),
4012 )
4013 }
4014}
4015
4016fn find_active_indent_guide_ix(
4017 outline_panel: &OutlinePanel,
4018 candidates: &[IndentGuideLayout],
4019) -> Option<usize> {
4020 let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
4021 return None;
4022 };
4023 let target_depth = outline_panel
4024 .cached_entries
4025 .get(*target_ix)
4026 .map(|cached_entry| cached_entry.depth)?;
4027
4028 let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
4029 .cached_entries
4030 .get(target_ix + 1)
4031 .filter(|cached_entry| cached_entry.depth > target_depth)
4032 .map(|entry| entry.depth)
4033 {
4034 (target_ix + 1, target_depth.saturating_sub(1))
4035 } else {
4036 (*target_ix, target_depth.saturating_sub(1))
4037 };
4038
4039 candidates
4040 .iter()
4041 .enumerate()
4042 .find(|(_, guide)| {
4043 guide.offset.y <= target_ix
4044 && target_ix < guide.offset.y + guide.length
4045 && guide.offset.x == target_depth
4046 })
4047 .map(|(ix, _)| ix)
4048}
4049
4050fn subscribe_for_editor_events(
4051 editor: &View<Editor>,
4052 cx: &mut ViewContext<OutlinePanel>,
4053) -> Subscription {
4054 let debounce = Some(UPDATE_DEBOUNCE);
4055 cx.subscribe(
4056 editor,
4057 move |outline_panel, editor, e: &EditorEvent, cx| match e {
4058 EditorEvent::SelectionsChanged { local: true } => {
4059 outline_panel.reveal_entry_for_selection(&editor, cx);
4060 cx.notify();
4061 }
4062 EditorEvent::ExcerptsAdded { excerpts, .. } => {
4063 outline_panel.update_fs_entries(
4064 &editor,
4065 excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
4066 debounce,
4067 cx,
4068 );
4069 }
4070 EditorEvent::ExcerptsRemoved { ids } => {
4071 let mut ids = ids.iter().collect::<HashSet<_>>();
4072 for excerpts in outline_panel.excerpts.values_mut() {
4073 excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
4074 if ids.is_empty() {
4075 break;
4076 }
4077 }
4078 outline_panel.update_fs_entries(&editor, HashSet::default(), debounce, cx);
4079 }
4080 EditorEvent::ExcerptsExpanded { ids } => {
4081 outline_panel.invalidate_outlines(ids);
4082 outline_panel.update_non_fs_items(cx);
4083 }
4084 EditorEvent::ExcerptsEdited { ids } => {
4085 outline_panel.invalidate_outlines(ids);
4086 outline_panel.update_non_fs_items(cx);
4087 }
4088 EditorEvent::Reparsed(buffer_id) => {
4089 if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
4090 for (_, excerpt) in excerpts {
4091 excerpt.invalidate_outlines();
4092 }
4093 }
4094 outline_panel.update_non_fs_items(cx);
4095 }
4096 _ => {}
4097 },
4098 )
4099}
4100
4101fn empty_icon() -> AnyElement {
4102 h_flex()
4103 .size(IconSize::default().rems())
4104 .invisible()
4105 .flex_none()
4106 .into_any_element()
4107}
4108
4109fn horizontal_separator(cx: &mut WindowContext) -> Div {
4110 div().mx_2().border_primary(cx).border_t_1()
4111}
4112
4113#[cfg(test)]
4114mod tests {
4115 use gpui::{TestAppContext, VisualTestContext, WindowHandle};
4116 use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
4117 use pretty_assertions::assert_eq;
4118 use project::FakeFs;
4119 use search::project_search::{self, perform_project_search};
4120 use serde_json::json;
4121
4122 use super::*;
4123
4124 const SELECTED_MARKER: &str = " <==== selected";
4125
4126 #[gpui::test]
4127 async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
4128 init_test(cx);
4129
4130 let fs = FakeFs::new(cx.background_executor.clone());
4131 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
4132 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
4133 project.read_with(cx, |project, _| {
4134 project.languages().add(Arc::new(rust_lang()))
4135 });
4136 let workspace = add_outline_panel(&project, cx).await;
4137 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4138 let outline_panel = outline_panel(&workspace, cx);
4139 outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
4140
4141 workspace
4142 .update(cx, |workspace, cx| {
4143 ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
4144 })
4145 .unwrap();
4146 let search_view = workspace
4147 .update(cx, |workspace, cx| {
4148 workspace
4149 .active_pane()
4150 .read(cx)
4151 .items()
4152 .find_map(|item| item.downcast::<ProjectSearchView>())
4153 .expect("Project search view expected to appear after new search event trigger")
4154 })
4155 .unwrap();
4156
4157 let query = "param_names_for_lifetime_elision_hints";
4158 perform_project_search(&search_view, query, cx);
4159 search_view.update(cx, |search_view, cx| {
4160 search_view
4161 .results_editor()
4162 .update(cx, |results_editor, cx| {
4163 assert_eq!(
4164 results_editor.display_text(cx).match_indices(query).count(),
4165 9
4166 );
4167 });
4168 });
4169
4170 let all_matches = r#"/
4171 crates/
4172 ide/src/
4173 inlay_hints/
4174 fn_lifetime_fn.rs
4175 search: match config.param_names_for_lifetime_elision_hints {
4176 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
4177 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
4178 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
4179 inlay_hints.rs
4180 search: pub param_names_for_lifetime_elision_hints: bool,
4181 search: param_names_for_lifetime_elision_hints: self
4182 static_index.rs
4183 search: param_names_for_lifetime_elision_hints: false,
4184 rust-analyzer/src/
4185 cli/
4186 analysis_stats.rs
4187 search: param_names_for_lifetime_elision_hints: true,
4188 config.rs
4189 search: param_names_for_lifetime_elision_hints: self"#;
4190 let select_first_in_all_matches = |line_to_select: &str| {
4191 assert!(all_matches.contains(line_to_select));
4192 all_matches.replacen(
4193 line_to_select,
4194 &format!("{line_to_select}{SELECTED_MARKER}"),
4195 1,
4196 )
4197 };
4198
4199 cx.executor()
4200 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4201 cx.run_until_parked();
4202 outline_panel.update(cx, |outline_panel, _| {
4203 assert_eq!(
4204 display_entries(
4205 &outline_panel.cached_entries,
4206 outline_panel.selected_entry()
4207 ),
4208 select_first_in_all_matches(
4209 "search: match config.param_names_for_lifetime_elision_hints {"
4210 )
4211 );
4212 });
4213
4214 outline_panel.update(cx, |outline_panel, cx| {
4215 outline_panel.select_parent(&SelectParent, cx);
4216 assert_eq!(
4217 display_entries(
4218 &outline_panel.cached_entries,
4219 outline_panel.selected_entry()
4220 ),
4221 select_first_in_all_matches("fn_lifetime_fn.rs")
4222 );
4223 });
4224 outline_panel.update(cx, |outline_panel, cx| {
4225 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
4226 });
4227 cx.run_until_parked();
4228 outline_panel.update(cx, |outline_panel, _| {
4229 assert_eq!(
4230 display_entries(
4231 &outline_panel.cached_entries,
4232 outline_panel.selected_entry()
4233 ),
4234 format!(
4235 r#"/
4236 crates/
4237 ide/src/
4238 inlay_hints/
4239 fn_lifetime_fn.rs{SELECTED_MARKER}
4240 inlay_hints.rs
4241 search: pub param_names_for_lifetime_elision_hints: bool,
4242 search: param_names_for_lifetime_elision_hints: self
4243 static_index.rs
4244 search: param_names_for_lifetime_elision_hints: false,
4245 rust-analyzer/src/
4246 cli/
4247 analysis_stats.rs
4248 search: param_names_for_lifetime_elision_hints: true,
4249 config.rs
4250 search: param_names_for_lifetime_elision_hints: self"#,
4251 )
4252 );
4253 });
4254
4255 outline_panel.update(cx, |outline_panel, cx| {
4256 outline_panel.expand_all_entries(&ExpandAllEntries, cx);
4257 });
4258 cx.run_until_parked();
4259 outline_panel.update(cx, |outline_panel, cx| {
4260 outline_panel.select_parent(&SelectParent, cx);
4261 assert_eq!(
4262 display_entries(
4263 &outline_panel.cached_entries,
4264 outline_panel.selected_entry()
4265 ),
4266 select_first_in_all_matches("inlay_hints/")
4267 );
4268 });
4269
4270 outline_panel.update(cx, |outline_panel, cx| {
4271 outline_panel.select_parent(&SelectParent, cx);
4272 assert_eq!(
4273 display_entries(
4274 &outline_panel.cached_entries,
4275 outline_panel.selected_entry()
4276 ),
4277 select_first_in_all_matches("ide/src/")
4278 );
4279 });
4280
4281 outline_panel.update(cx, |outline_panel, cx| {
4282 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
4283 });
4284 cx.run_until_parked();
4285 outline_panel.update(cx, |outline_panel, _| {
4286 assert_eq!(
4287 display_entries(
4288 &outline_panel.cached_entries,
4289 outline_panel.selected_entry()
4290 ),
4291 format!(
4292 r#"/
4293 crates/
4294 ide/src/{SELECTED_MARKER}
4295 rust-analyzer/src/
4296 cli/
4297 analysis_stats.rs
4298 search: param_names_for_lifetime_elision_hints: true,
4299 config.rs
4300 search: param_names_for_lifetime_elision_hints: self"#,
4301 )
4302 );
4303 });
4304 outline_panel.update(cx, |outline_panel, cx| {
4305 outline_panel.expand_selected_entry(&ExpandSelectedEntry, cx);
4306 });
4307 cx.run_until_parked();
4308 outline_panel.update(cx, |outline_panel, _| {
4309 assert_eq!(
4310 display_entries(
4311 &outline_panel.cached_entries,
4312 outline_panel.selected_entry()
4313 ),
4314 select_first_in_all_matches("ide/src/")
4315 );
4316 });
4317 }
4318
4319 #[gpui::test]
4320 async fn test_item_filtering(cx: &mut TestAppContext) {
4321 init_test(cx);
4322
4323 let fs = FakeFs::new(cx.background_executor.clone());
4324 populate_with_test_ra_project(&fs, "/rust-analyzer").await;
4325 let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
4326 project.read_with(cx, |project, _| {
4327 project.languages().add(Arc::new(rust_lang()))
4328 });
4329 let workspace = add_outline_panel(&project, cx).await;
4330 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4331 let outline_panel = outline_panel(&workspace, cx);
4332 outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
4333
4334 workspace
4335 .update(cx, |workspace, cx| {
4336 ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
4337 })
4338 .unwrap();
4339 let search_view = workspace
4340 .update(cx, |workspace, cx| {
4341 workspace
4342 .active_pane()
4343 .read(cx)
4344 .items()
4345 .find_map(|item| item.downcast::<ProjectSearchView>())
4346 .expect("Project search view expected to appear after new search event trigger")
4347 })
4348 .unwrap();
4349
4350 let query = "param_names_for_lifetime_elision_hints";
4351 perform_project_search(&search_view, query, cx);
4352 search_view.update(cx, |search_view, cx| {
4353 search_view
4354 .results_editor()
4355 .update(cx, |results_editor, cx| {
4356 assert_eq!(
4357 results_editor.display_text(cx).match_indices(query).count(),
4358 9
4359 );
4360 });
4361 });
4362 let all_matches = r#"/
4363 crates/
4364 ide/src/
4365 inlay_hints/
4366 fn_lifetime_fn.rs
4367 search: match config.param_names_for_lifetime_elision_hints {
4368 search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
4369 search: Some(it) if config.param_names_for_lifetime_elision_hints => {
4370 search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
4371 inlay_hints.rs
4372 search: pub param_names_for_lifetime_elision_hints: bool,
4373 search: param_names_for_lifetime_elision_hints: self
4374 static_index.rs
4375 search: param_names_for_lifetime_elision_hints: false,
4376 rust-analyzer/src/
4377 cli/
4378 analysis_stats.rs
4379 search: param_names_for_lifetime_elision_hints: true,
4380 config.rs
4381 search: param_names_for_lifetime_elision_hints: self"#;
4382
4383 cx.executor()
4384 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4385 cx.run_until_parked();
4386 outline_panel.update(cx, |outline_panel, _| {
4387 assert_eq!(
4388 display_entries(&outline_panel.cached_entries, None,),
4389 all_matches,
4390 );
4391 });
4392
4393 let filter_text = "a";
4394 outline_panel.update(cx, |outline_panel, cx| {
4395 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
4396 filter_editor.set_text(filter_text, cx);
4397 });
4398 });
4399 cx.executor()
4400 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4401 cx.run_until_parked();
4402
4403 outline_panel.update(cx, |outline_panel, _| {
4404 assert_eq!(
4405 display_entries(&outline_panel.cached_entries, None),
4406 all_matches
4407 .lines()
4408 .filter(|item| item.contains(filter_text))
4409 .collect::<Vec<_>>()
4410 .join("\n"),
4411 );
4412 });
4413
4414 outline_panel.update(cx, |outline_panel, cx| {
4415 outline_panel.filter_editor.update(cx, |filter_editor, cx| {
4416 filter_editor.set_text("", cx);
4417 });
4418 });
4419 cx.executor()
4420 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4421 cx.run_until_parked();
4422 outline_panel.update(cx, |outline_panel, _| {
4423 assert_eq!(
4424 display_entries(&outline_panel.cached_entries, None,),
4425 all_matches,
4426 );
4427 });
4428 }
4429
4430 #[gpui::test]
4431 async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
4432 init_test(cx);
4433
4434 let root = "/frontend-project";
4435 let fs = FakeFs::new(cx.background_executor.clone());
4436 fs.insert_tree(
4437 root,
4438 json!({
4439 "public": {
4440 "lottie": {
4441 "syntax-tree.json": r#"{ "something": "static" }"#
4442 }
4443 },
4444 "src": {
4445 "app": {
4446 "(site)": {
4447 "(about)": {
4448 "jobs": {
4449 "[slug]": {
4450 "page.tsx": r#"static"#
4451 }
4452 }
4453 },
4454 "(blog)": {
4455 "post": {
4456 "[slug]": {
4457 "page.tsx": r#"static"#
4458 }
4459 }
4460 },
4461 }
4462 },
4463 "components": {
4464 "ErrorBoundary.tsx": r#"static"#,
4465 }
4466 }
4467
4468 }),
4469 )
4470 .await;
4471 let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
4472 let workspace = add_outline_panel(&project, cx).await;
4473 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4474 let outline_panel = outline_panel(&workspace, cx);
4475 outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
4476
4477 workspace
4478 .update(cx, |workspace, cx| {
4479 ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
4480 })
4481 .unwrap();
4482 let search_view = workspace
4483 .update(cx, |workspace, cx| {
4484 workspace
4485 .active_pane()
4486 .read(cx)
4487 .items()
4488 .find_map(|item| item.downcast::<ProjectSearchView>())
4489 .expect("Project search view expected to appear after new search event trigger")
4490 })
4491 .unwrap();
4492
4493 let query = "static";
4494 perform_project_search(&search_view, query, cx);
4495 search_view.update(cx, |search_view, cx| {
4496 search_view
4497 .results_editor()
4498 .update(cx, |results_editor, cx| {
4499 assert_eq!(
4500 results_editor.display_text(cx).match_indices(query).count(),
4501 4
4502 );
4503 });
4504 });
4505
4506 cx.executor()
4507 .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4508 cx.run_until_parked();
4509 outline_panel.update(cx, |outline_panel, _| {
4510 assert_eq!(
4511 display_entries(
4512 &outline_panel.cached_entries,
4513 outline_panel.selected_entry()
4514 ),
4515 r#"/
4516 public/lottie/
4517 syntax-tree.json
4518 search: { "something": "static" } <==== selected
4519 src/
4520 app/(site)/
4521 (about)/jobs/[slug]/
4522 page.tsx
4523 search: static
4524 (blog)/post/[slug]/
4525 page.tsx
4526 search: static
4527 components/
4528 ErrorBoundary.tsx
4529 search: static"#
4530 );
4531 });
4532
4533 outline_panel.update(cx, |outline_panel, cx| {
4534 outline_panel.select_next(&SelectNext, cx);
4535 outline_panel.select_next(&SelectNext, cx);
4536 outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
4537 });
4538 cx.run_until_parked();
4539 outline_panel.update(cx, |outline_panel, _| {
4540 assert_eq!(
4541 display_entries(
4542 &outline_panel.cached_entries,
4543 outline_panel.selected_entry()
4544 ),
4545 r#"/
4546 public/lottie/
4547 syntax-tree.json
4548 search: { "something": "static" }
4549 src/
4550 app/(site)/ <==== selected
4551 components/
4552 ErrorBoundary.tsx
4553 search: static"#
4554 );
4555 });
4556 }
4557
4558 async fn add_outline_panel(
4559 project: &Model<Project>,
4560 cx: &mut TestAppContext,
4561 ) -> WindowHandle<Workspace> {
4562 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4563
4564 let outline_panel = window
4565 .update(cx, |_, cx| cx.spawn(OutlinePanel::load))
4566 .unwrap()
4567 .await
4568 .expect("Failed to load outline panel");
4569
4570 window
4571 .update(cx, |workspace, cx| {
4572 workspace.add_panel(outline_panel, cx);
4573 })
4574 .unwrap();
4575 window
4576 }
4577
4578 fn outline_panel(
4579 workspace: &WindowHandle<Workspace>,
4580 cx: &mut TestAppContext,
4581 ) -> View<OutlinePanel> {
4582 workspace
4583 .update(cx, |workspace, cx| {
4584 workspace
4585 .panel::<OutlinePanel>(cx)
4586 .expect("no outline panel")
4587 })
4588 .unwrap()
4589 }
4590
4591 fn display_entries(
4592 cached_entries: &[CachedEntry],
4593 selected_entry: Option<&PanelEntry>,
4594 ) -> String {
4595 let mut display_string = String::new();
4596 for entry in cached_entries {
4597 if !display_string.is_empty() {
4598 display_string += "\n";
4599 }
4600 for _ in 0..entry.depth {
4601 display_string += " ";
4602 }
4603 display_string += &match &entry.entry {
4604 PanelEntry::Fs(entry) => match entry {
4605 FsEntry::ExternalFile(_, _) => {
4606 panic!("Did not cover external files with tests")
4607 }
4608 FsEntry::Directory(_, dir_entry) => format!(
4609 "{}/",
4610 dir_entry
4611 .path
4612 .file_name()
4613 .map(|name| name.to_string_lossy().to_string())
4614 .unwrap_or_default()
4615 ),
4616 FsEntry::File(_, file_entry, ..) => file_entry
4617 .path
4618 .file_name()
4619 .map(|name| name.to_string_lossy().to_string())
4620 .unwrap_or_default(),
4621 },
4622 PanelEntry::FoldedDirs(_, dirs) => dirs
4623 .iter()
4624 .filter_map(|dir| dir.path.file_name())
4625 .map(|name| name.to_string_lossy().to_string() + "/")
4626 .collect(),
4627 PanelEntry::Outline(outline_entry) => match outline_entry {
4628 OutlineEntry::Excerpt(_, _, _) => continue,
4629 OutlineEntry::Outline(_, _, outline) => format!("outline: {}", outline.text),
4630 },
4631 PanelEntry::Search(SearchEntry { render_data, .. }) => {
4632 format!("search: {}", render_data.context_text)
4633 }
4634 };
4635
4636 if Some(&entry.entry) == selected_entry {
4637 display_string += SELECTED_MARKER;
4638 }
4639 }
4640 display_string
4641 }
4642
4643 fn init_test(cx: &mut TestAppContext) {
4644 cx.update(|cx| {
4645 let settings = SettingsStore::test(cx);
4646 cx.set_global(settings);
4647
4648 theme::init(theme::LoadThemes::JustBase, cx);
4649
4650 language::init(cx);
4651 editor::init(cx);
4652 workspace::init_settings(cx);
4653 Project::init_settings(cx);
4654 project_search::init(cx);
4655 super::init((), cx);
4656 });
4657 }
4658
4659 // Based on https://github.com/rust-lang/rust-analyzer/
4660 async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
4661 fs.insert_tree(
4662 root,
4663 json!({
4664 "crates": {
4665 "ide": {
4666 "src": {
4667 "inlay_hints": {
4668 "fn_lifetime_fn.rs": r##"
4669 pub(super) fn hints(
4670 acc: &mut Vec<InlayHint>,
4671 config: &InlayHintsConfig,
4672 func: ast::Fn,
4673 ) -> Option<()> {
4674 // ... snip
4675
4676 let mut used_names: FxHashMap<SmolStr, usize> =
4677 match config.param_names_for_lifetime_elision_hints {
4678 true => generic_param_list
4679 .iter()
4680 .flat_map(|gpl| gpl.lifetime_params())
4681 .filter_map(|param| param.lifetime())
4682 .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
4683 .collect(),
4684 false => Default::default(),
4685 };
4686 {
4687 let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
4688 if self_param.is_some() && potential_lt_refs.next().is_some() {
4689 allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
4690 // self can't be used as a lifetime, so no need to check for collisions
4691 "'self".into()
4692 } else {
4693 gen_idx_name()
4694 });
4695 }
4696 potential_lt_refs.for_each(|(name, ..)| {
4697 let name = match name {
4698 Some(it) if config.param_names_for_lifetime_elision_hints => {
4699 if let Some(c) = used_names.get_mut(it.text().as_str()) {
4700 *c += 1;
4701 SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
4702 } else {
4703 used_names.insert(it.text().as_str().into(), 0);
4704 SmolStr::from_iter(["\'", it.text().as_str()])
4705 }
4706 }
4707 _ => gen_idx_name(),
4708 };
4709 allocated_lifetimes.push(name);
4710 });
4711 }
4712
4713 // ... snip
4714 }
4715
4716 // ... snip
4717
4718 #[test]
4719 fn hints_lifetimes_named() {
4720 check_with_config(
4721 InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
4722 r#"
4723 fn nested_in<'named>(named: & &X< &()>) {}
4724 // ^'named1, 'named2, 'named3, $
4725 //^'named1 ^'named2 ^'named3
4726 "#,
4727 );
4728 }
4729
4730 // ... snip
4731 "##,
4732 },
4733 "inlay_hints.rs": r#"
4734 #[derive(Clone, Debug, PartialEq, Eq)]
4735 pub struct InlayHintsConfig {
4736 // ... snip
4737 pub param_names_for_lifetime_elision_hints: bool,
4738 pub max_length: Option<usize>,
4739 // ... snip
4740 }
4741
4742 impl Config {
4743 pub fn inlay_hints(&self) -> InlayHintsConfig {
4744 InlayHintsConfig {
4745 // ... snip
4746 param_names_for_lifetime_elision_hints: self
4747 .inlayHints_lifetimeElisionHints_useParameterNames()
4748 .to_owned(),
4749 max_length: self.inlayHints_maxLength().to_owned(),
4750 // ... snip
4751 }
4752 }
4753 }
4754 "#,
4755 "static_index.rs": r#"
4756// ... snip
4757 fn add_file(&mut self, file_id: FileId) {
4758 let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
4759 let folds = self.analysis.folding_ranges(file_id).unwrap();
4760 let inlay_hints = self
4761 .analysis
4762 .inlay_hints(
4763 &InlayHintsConfig {
4764 // ... snip
4765 closure_style: hir::ClosureStyle::ImplFn,
4766 param_names_for_lifetime_elision_hints: false,
4767 binding_mode_hints: false,
4768 max_length: Some(25),
4769 closure_capture_hints: false,
4770 // ... snip
4771 },
4772 file_id,
4773 None,
4774 )
4775 .unwrap();
4776 // ... snip
4777 }
4778// ... snip
4779 "#
4780 }
4781 },
4782 "rust-analyzer": {
4783 "src": {
4784 "cli": {
4785 "analysis_stats.rs": r#"
4786 // ... snip
4787 for &file_id in &file_ids {
4788 _ = analysis.inlay_hints(
4789 &InlayHintsConfig {
4790 // ... snip
4791 implicit_drop_hints: true,
4792 lifetime_elision_hints: ide::LifetimeElisionHints::Always,
4793 param_names_for_lifetime_elision_hints: true,
4794 hide_named_constructor_hints: false,
4795 hide_closure_initialization_hints: false,
4796 closure_style: hir::ClosureStyle::ImplFn,
4797 max_length: Some(25),
4798 closing_brace_hints_min_lines: Some(20),
4799 fields_to_resolve: InlayFieldsToResolve::empty(),
4800 range_exclusive_hints: true,
4801 },
4802 file_id.into(),
4803 None,
4804 );
4805 }
4806 // ... snip
4807 "#,
4808 },
4809 "config.rs": r#"
4810 config_data! {
4811 /// Configs that only make sense when they are set by a client. As such they can only be defined
4812 /// by setting them using client's settings (e.g `settings.json` on VS Code).
4813 client: struct ClientDefaultConfigData <- ClientConfigInput -> {
4814 // ... snip
4815 /// Maximum length for inlay hints. Set to null to have an unlimited length.
4816 inlayHints_maxLength: Option<usize> = Some(25),
4817 // ... snip
4818 /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
4819 inlayHints_lifetimeElisionHints_useParameterNames: bool = false,
4820 // ... snip
4821 }
4822 }
4823
4824 impl Config {
4825 // ... snip
4826 pub fn inlay_hints(&self) -> InlayHintsConfig {
4827 InlayHintsConfig {
4828 // ... snip
4829 param_names_for_lifetime_elision_hints: self
4830 .inlayHints_lifetimeElisionHints_useParameterNames()
4831 .to_owned(),
4832 max_length: self.inlayHints_maxLength().to_owned(),
4833 // ... snip
4834 }
4835 }
4836 // ... snip
4837 }
4838 "#
4839 }
4840 }
4841 }
4842 }),
4843 )
4844 .await;
4845 }
4846
4847 fn rust_lang() -> Language {
4848 Language::new(
4849 LanguageConfig {
4850 name: "Rust".into(),
4851 matcher: LanguageMatcher {
4852 path_suffixes: vec!["rs".to_string()],
4853 ..Default::default()
4854 },
4855 ..Default::default()
4856 },
4857 Some(tree_sitter_rust::LANGUAGE.into()),
4858 )
4859 .with_highlights_query(
4860 r#"
4861 (field_identifier) @field
4862 (struct_expression) @struct
4863 "#,
4864 )
4865 .unwrap()
4866 .with_injection_query(
4867 r#"
4868 (macro_invocation
4869 (token_tree) @content
4870 (#set! "language" "rust"))
4871 "#,
4872 )
4873 .unwrap()
4874 }
4875}