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