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