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