Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:37:26.357Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

outline_panel.rs

8171 lines · 322.2 KB · rust
1mod outline_panel_settings;
2
3use anyhow::Context as _;
4use collections::{BTreeSet, HashMap, HashSet};
5use db::kvp::KeyValueStore;
6use editor::{
7    AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, ExcerptRange, MultiBufferSnapshot,
8    RangeToAnchorExt, SelectionEffects,
9    display_map::ToDisplayPoint,
10    items::{entry_git_aware_label_color, entry_label_color},
11    scroll::{Autoscroll, ScrollAnchor},
12};
13use file_icons::FileIcons;
14
15use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
16use gpui::{
17    Action, AnyElement, App, AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context,
18    DismissEvent, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
19    InteractiveElement, IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
20    MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy,
21    SharedString, Stateful, StatefulInteractiveElement as _, Styled, Subscription, Task, TaskExt,
22    UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, point, px, size,
23    uniform_list,
24};
25use itertools::Itertools;
26use language::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
27use language::{LanguageAwareStyling, language_settings::LanguageSettings};
28
29use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};
30use std::{
31    cmp,
32    collections::BTreeMap,
33    hash::Hash,
34    ops::Range,
35    path::{Path, PathBuf},
36    sync::{
37        Arc, OnceLock,
38        atomic::{self, AtomicBool},
39    },
40    time::Duration,
41    u32,
42};
43
44use outline_panel_settings::{DockSide, OutlinePanelSettings, ShowIndentGuides};
45use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem};
46use search::{BufferSearchBar, ProjectSearchView};
47use serde::{Deserialize, Serialize};
48use settings::{Settings, SettingsStore};
49use theme::SyntaxTheme;
50use theme_settings::ThemeSettings;
51use ui::{
52    ContextMenu, FluentBuilder, HighlightedLabel, IconButton, IconButtonShape, IndentGuideColors,
53    IndentGuideLayout, KeyBinding, ListItem, ScrollAxes, Scrollbars, Tab, Tooltip, WithScrollbar,
54    prelude::*,
55};
56use util::{RangeExt, ResultExt, TryFutureExt, debug_panic, rel_path::RelPath};
57use workspace::{
58    OpenInTerminal, WeakItemHandle, Workspace,
59    dock::{DockPosition, Panel, PanelEvent},
60    item::ItemHandle,
61    searchable::{SearchEvent, SearchableItem},
62};
63use worktree::{Entry, ProjectEntryId, WorktreeId};
64
65use crate::outline_panel_settings::OutlinePanelSettingsScrollbarProxy;
66
67actions!(
68    outline_panel,
69    [
70        /// Collapses all entries in the outline tree.
71        CollapseAllEntries,
72        /// Collapses the currently selected entry.
73        CollapseSelectedEntry,
74        /// Expands all entries in the outline tree.
75        ExpandAllEntries,
76        /// Expands the currently selected entry.
77        ExpandSelectedEntry,
78        /// Folds the selected directory.
79        FoldDirectory,
80        /// Opens the selected entry in the editor.
81        OpenSelectedEntry,
82        /// Reveals the selected item in the system file manager.
83        RevealInFileManager,
84        /// Scroll half a page upwards
85        ScrollUp,
86        /// Scroll half a page downwards
87        ScrollDown,
88        /// Scroll until the cursor displays at the center
89        ScrollCursorCenter,
90        /// Scroll until the cursor displays at the top
91        ScrollCursorTop,
92        /// Scroll until the cursor displays at the bottom
93        ScrollCursorBottom,
94        /// Selects the parent of the current entry.
95        SelectParent,
96        /// Toggles the pin status of the active editor.
97        ToggleActiveEditorPin,
98        /// Unfolds the selected directory.
99        UnfoldDirectory,
100        /// Toggles the outline panel.
101        Toggle,
102        /// Toggles focus on the outline panel.
103        ToggleFocus,
104    ]
105);
106
107const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
108const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
109
110type Outline = OutlineItem<language::Anchor>;
111type HighlightStyleData = Arc<OnceLock<Vec<(Range<usize>, HighlightStyle)>>>;
112
113pub struct OutlinePanel {
114    fs: Arc<dyn Fs>,
115    project: Entity<Project>,
116    workspace: WeakEntity<Workspace>,
117    active: bool,
118    pinned: bool,
119    scroll_handle: UniformListScrollHandle,
120    rendered_entries_len: usize,
121    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
122    focus_handle: FocusHandle,
123    pending_serialization: Task<Option<()>>,
124    fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
125    fs_entries: Vec<FsEntry>,
126    fs_children_count: HashMap<WorktreeId, HashMap<Arc<RelPath>, FsChildren>>,
127    collapsed_entries: HashSet<CollapsedEntry>,
128    unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
129    selected_entry: SelectedEntry,
130    active_item: Option<ActiveItem>,
131    _subscriptions: Vec<Subscription>,
132    new_entries_for_fs_update: HashSet<BufferId>,
133    fs_entries_update_task: Task<()>,
134    fs_entries_update_pending: bool,
135    cached_entries_update_task: Task<()>,
136    cached_entries_update_pending: bool,
137    reveal_selection_task: Task<anyhow::Result<()>>,
138    outline_fetch_tasks: HashMap<BufferId, Task<()>>,
139    buffers: HashMap<BufferId, BufferOutlines>,
140    cached_entries: Vec<CachedEntry>,
141    filter_editor: Entity<Editor>,
142    mode: ItemsDisplayMode,
143    max_width_item_index: Option<usize>,
144    preserve_selection_on_buffer_fold_toggles: HashSet<BufferId>,
145    pending_default_expansion_depth: Option<usize>,
146    outline_children_cache: HashMap<BufferId, HashMap<(Range<Anchor>, usize), bool>>,
147}
148
149#[derive(Debug)]
150enum ItemsDisplayMode {
151    Search(SearchState),
152    Outline,
153}
154
155#[derive(Debug)]
156struct SearchState {
157    kind: SearchKind,
158    query: String,
159    matches: Vec<(Range<editor::Anchor>, Arc<OnceLock<SearchData>>)>,
160    highlight_search_match_tx: async_channel::Sender<HighlightArguments>,
161    _search_match_highlighter: Task<()>,
162    _search_match_notify: Task<()>,
163}
164
165struct HighlightArguments {
166    multi_buffer_snapshot: MultiBufferSnapshot,
167    match_range: Range<editor::Anchor>,
168    search_data: Arc<OnceLock<SearchData>>,
169}
170
171impl SearchState {
172    fn new(
173        kind: SearchKind,
174        query: String,
175        previous_matches: HashMap<Range<editor::Anchor>, Arc<OnceLock<SearchData>>>,
176        new_matches: Vec<Range<editor::Anchor>>,
177        theme: Arc<SyntaxTheme>,
178        window: &mut Window,
179        cx: &mut Context<OutlinePanel>,
180    ) -> Self {
181        let (highlight_search_match_tx, highlight_search_match_rx) = async_channel::unbounded();
182        let (notify_tx, notify_rx) = async_channel::unbounded::<()>();
183        Self {
184            kind,
185            query,
186            matches: new_matches
187                .into_iter()
188                .map(|range| {
189                    let search_data = previous_matches
190                        .get(&range)
191                        .map(Arc::clone)
192                        .unwrap_or_default();
193                    (range, search_data)
194                })
195                .collect(),
196            highlight_search_match_tx,
197            _search_match_highlighter: cx.background_spawn(async move {
198                while let Ok(highlight_arguments) = highlight_search_match_rx.recv().await {
199                    let needs_init = highlight_arguments.search_data.get().is_none();
200                    let search_data = highlight_arguments.search_data.get_or_init(|| {
201                        SearchData::new(
202                            &highlight_arguments.match_range,
203                            &highlight_arguments.multi_buffer_snapshot,
204                        )
205                    });
206                    if needs_init {
207                        notify_tx.try_send(()).ok();
208                    }
209
210                    let highlight_data = &search_data.highlights_data;
211                    if highlight_data.get().is_some() {
212                        continue;
213                    }
214                    let mut left_whitespaces_count = 0;
215                    let mut non_whitespace_symbol_occurred = false;
216                    let context_offset_range = search_data
217                        .context_range
218                        .to_offset(&highlight_arguments.multi_buffer_snapshot);
219                    let mut offset = context_offset_range.start;
220                    let mut context_text = String::new();
221                    let mut highlight_ranges = Vec::new();
222                    for mut chunk in highlight_arguments.multi_buffer_snapshot.chunks(
223                        context_offset_range.start..context_offset_range.end,
224                        LanguageAwareStyling {
225                            tree_sitter: true,
226                            diagnostics: true,
227                        },
228                    ) {
229                        if !non_whitespace_symbol_occurred {
230                            for c in chunk.text.chars() {
231                                if c.is_whitespace() {
232                                    left_whitespaces_count += c.len_utf8();
233                                } else {
234                                    non_whitespace_symbol_occurred = true;
235                                    break;
236                                }
237                            }
238                        }
239
240                        if chunk.text.len() > context_offset_range.end - offset {
241                            chunk.text = &chunk.text[0..(context_offset_range.end - offset)];
242                            offset = context_offset_range.end;
243                        } else {
244                            offset += chunk.text.len();
245                        }
246                        let style = chunk
247                            .syntax_highlight_id
248                            .and_then(|highlight| theme.get(highlight).cloned());
249
250                        if let Some(style) = style {
251                            let start = context_text.len();
252                            let end = start + chunk.text.len();
253                            highlight_ranges.push((start..end, style));
254                        }
255                        context_text.push_str(chunk.text);
256                        if offset >= context_offset_range.end {
257                            break;
258                        }
259                    }
260
261                    highlight_ranges.iter_mut().for_each(|(range, _)| {
262                        range.start = range.start.saturating_sub(left_whitespaces_count);
263                        range.end = range.end.saturating_sub(left_whitespaces_count);
264                    });
265                    if highlight_data.set(highlight_ranges).ok().is_some() {
266                        notify_tx.try_send(()).ok();
267                    }
268
269                    let trimmed_text = context_text[left_whitespaces_count..].to_owned();
270                    debug_assert_eq!(
271                        trimmed_text, search_data.context_text,
272                        "Highlighted text that does not match the buffer text"
273                    );
274                }
275            }),
276            _search_match_notify: cx.spawn_in(window, async move |outline_panel, cx| {
277                loop {
278                    match notify_rx.recv().await {
279                        Ok(()) => {}
280                        Err(_) => break,
281                    };
282                    while let Ok(()) = notify_rx.try_recv() {
283                        //
284                    }
285                    let update_result = outline_panel.update(cx, |_, cx| {
286                        cx.notify();
287                    });
288                    if update_result.is_err() {
289                        break;
290                    }
291                }
292            }),
293        }
294    }
295}
296
297#[derive(Debug)]
298enum SelectedEntry {
299    Invalidated(Option<PanelEntry>),
300    Valid(PanelEntry, usize),
301    None,
302}
303
304impl SelectedEntry {
305    fn invalidate(&mut self) {
306        match std::mem::replace(self, SelectedEntry::None) {
307            Self::Valid(entry, _) => *self = Self::Invalidated(Some(entry)),
308            Self::None => *self = Self::Invalidated(None),
309            other => *self = other,
310        }
311    }
312
313    fn is_invalidated(&self) -> bool {
314        matches!(self, Self::Invalidated(_))
315    }
316}
317
318#[derive(Debug, Clone, Copy, Default)]
319struct FsChildren {
320    files: usize,
321    dirs: usize,
322}
323
324impl FsChildren {
325    fn may_be_fold_part(&self) -> bool {
326        self.dirs == 0 || (self.dirs == 1 && self.files == 0)
327    }
328}
329
330#[derive(Clone, Debug)]
331struct CachedEntry {
332    depth: usize,
333    string_match: Option<StringMatch>,
334    entry: PanelEntry,
335}
336
337#[derive(Clone, Debug, PartialEq, Eq, Hash)]
338enum CollapsedEntry {
339    Dir(WorktreeId, ProjectEntryId),
340    File(WorktreeId, BufferId),
341    ExternalFile(BufferId),
342    Excerpt(ExcerptRange<Anchor>),
343    Outline(Range<Anchor>),
344}
345
346struct BufferOutlines {
347    excerpts: Vec<ExcerptRange<Anchor>>,
348    outlines: OutlineState,
349}
350
351impl BufferOutlines {
352    fn invalidate_outlines(&mut self) {
353        if let OutlineState::Outlines(valid_outlines) = &mut self.outlines {
354            self.outlines = OutlineState::Invalidated(std::mem::take(valid_outlines));
355        }
356    }
357
358    fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {
359        match &self.outlines {
360            OutlineState::Outlines(outlines) => outlines.iter(),
361            OutlineState::Invalidated(outlines) => outlines.iter(),
362            OutlineState::NotFetched => [].iter(),
363        }
364    }
365
366    fn should_fetch_outlines(&self) -> bool {
367        match &self.outlines {
368            OutlineState::Outlines(_) => false,
369            OutlineState::Invalidated(_) => true,
370            OutlineState::NotFetched => true,
371        }
372    }
373}
374
375#[derive(Debug)]
376enum OutlineState {
377    Outlines(Vec<Outline>),
378    Invalidated(Vec<Outline>),
379    NotFetched,
380}
381
382#[derive(Clone, Debug, PartialEq, Eq)]
383struct FoldedDirsEntry {
384    worktree_id: WorktreeId,
385    entries: Vec<GitEntry>,
386}
387
388// TODO: collapse the inner enums into panel entry
389#[derive(Clone, Debug)]
390enum PanelEntry {
391    Fs(FsEntry),
392    FoldedDirs(FoldedDirsEntry),
393    Outline(OutlineEntry),
394    Search(SearchEntry),
395}
396
397#[derive(Clone, Debug)]
398struct SearchEntry {
399    match_range: Range<editor::Anchor>,
400    kind: SearchKind,
401    render_data: Arc<OnceLock<SearchData>>,
402}
403
404#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
405enum SearchKind {
406    Project,
407    Buffer,
408}
409
410#[derive(Clone, Debug)]
411struct SearchData {
412    context_range: Range<editor::Anchor>,
413    context_text: String,
414    truncated_left: bool,
415    truncated_right: bool,
416    search_match_indices: Vec<Range<usize>>,
417    highlights_data: HighlightStyleData,
418}
419
420struct SearchPrecomputed {
421    multi_buffer_snapshot: MultiBufferSnapshot,
422    matches_by_buffer: HashMap<BufferId, Vec<(Range<editor::Anchor>, Arc<OnceLock<SearchData>>)>>,
423    folded_buffers: HashSet<BufferId>,
424}
425
426impl PartialEq for PanelEntry {
427    fn eq(&self, other: &Self) -> bool {
428        match (self, other) {
429            (Self::Fs(a), Self::Fs(b)) => a == b,
430            (
431                Self::FoldedDirs(FoldedDirsEntry {
432                    worktree_id: worktree_id_a,
433                    entries: entries_a,
434                }),
435                Self::FoldedDirs(FoldedDirsEntry {
436                    worktree_id: worktree_id_b,
437                    entries: entries_b,
438                }),
439            ) => worktree_id_a == worktree_id_b && entries_a == entries_b,
440            (Self::Outline(a), Self::Outline(b)) => a == b,
441            (
442                Self::Search(SearchEntry {
443                    match_range: match_range_a,
444                    kind: kind_a,
445                    ..
446                }),
447                Self::Search(SearchEntry {
448                    match_range: match_range_b,
449                    kind: kind_b,
450                    ..
451                }),
452            ) => match_range_a == match_range_b && kind_a == kind_b,
453            _ => false,
454        }
455    }
456}
457
458impl Eq for PanelEntry {}
459
460const SEARCH_MATCH_CONTEXT_SIZE: u32 = 40;
461const TRUNCATED_CONTEXT_MARK: &str = "…";
462
463impl SearchData {
464    fn new(
465        match_range: &Range<editor::Anchor>,
466        multi_buffer_snapshot: &MultiBufferSnapshot,
467    ) -> Self {
468        let match_point_range = match_range.to_point(multi_buffer_snapshot);
469        let context_left_border = multi_buffer_snapshot.clip_point(
470            language::Point::new(
471                match_point_range.start.row,
472                match_point_range
473                    .start
474                    .column
475                    .saturating_sub(SEARCH_MATCH_CONTEXT_SIZE),
476            ),
477            Bias::Left,
478        );
479        let context_right_border = multi_buffer_snapshot.clip_point(
480            language::Point::new(
481                match_point_range.end.row,
482                match_point_range.end.column + SEARCH_MATCH_CONTEXT_SIZE,
483            ),
484            Bias::Right,
485        );
486
487        let context_anchor_range =
488            (context_left_border..context_right_border).to_anchors(multi_buffer_snapshot);
489        let context_offset_range = context_anchor_range.to_offset(multi_buffer_snapshot);
490        let match_offset_range = match_range.to_offset(multi_buffer_snapshot);
491
492        let mut search_match_indices = vec![
493            match_offset_range.start - context_offset_range.start
494                ..match_offset_range.end - context_offset_range.start,
495        ];
496
497        let entire_context_text = multi_buffer_snapshot
498            .text_for_range(context_offset_range.clone())
499            .collect::<String>();
500        let left_whitespaces_offset = entire_context_text
501            .chars()
502            .take_while(|c| c.is_whitespace())
503            .map(|c| c.len_utf8())
504            .sum::<usize>();
505
506        let mut extended_context_left_border = context_left_border;
507        extended_context_left_border.column = extended_context_left_border.column.saturating_sub(1);
508        let extended_context_left_border =
509            multi_buffer_snapshot.clip_point(extended_context_left_border, Bias::Left);
510        let mut extended_context_right_border = context_right_border;
511        extended_context_right_border.column += 1;
512        let extended_context_right_border =
513            multi_buffer_snapshot.clip_point(extended_context_right_border, Bias::Right);
514
515        let truncated_left = left_whitespaces_offset == 0
516            && extended_context_left_border < context_left_border
517            && multi_buffer_snapshot
518                .chars_at(extended_context_left_border)
519                .last()
520                .is_some_and(|c| !c.is_whitespace());
521        let truncated_right = entire_context_text
522            .chars()
523            .last()
524            .is_none_or(|c| !c.is_whitespace())
525            && extended_context_right_border > context_right_border
526            && multi_buffer_snapshot
527                .chars_at(extended_context_right_border)
528                .next()
529                .is_some_and(|c| !c.is_whitespace());
530        search_match_indices.iter_mut().for_each(|range| {
531            range.start = range.start.saturating_sub(left_whitespaces_offset);
532            range.end = range.end.saturating_sub(left_whitespaces_offset);
533        });
534
535        let trimmed_row_offset_range =
536            context_offset_range.start + left_whitespaces_offset..context_offset_range.end;
537        let trimmed_text = entire_context_text[left_whitespaces_offset..].to_owned();
538        Self {
539            highlights_data: Arc::default(),
540            search_match_indices,
541            context_range: trimmed_row_offset_range.to_anchors(multi_buffer_snapshot),
542            context_text: trimmed_text,
543            truncated_left,
544            truncated_right,
545        }
546    }
547}
548
549#[derive(Clone, Debug, PartialEq, Eq)]
550enum OutlineEntry {
551    Excerpt(ExcerptRange<Anchor>),
552    Outline(Outline),
553}
554
555impl OutlineEntry {
556    fn buffer_id(&self) -> BufferId {
557        match self {
558            OutlineEntry::Excerpt(excerpt) => excerpt.context.start.buffer_id,
559            OutlineEntry::Outline(outline) => outline.range.start.buffer_id,
560        }
561    }
562
563    fn range(&self) -> Range<Anchor> {
564        match self {
565            OutlineEntry::Excerpt(excerpt) => excerpt.context.clone(),
566            OutlineEntry::Outline(outline) => outline.range.clone(),
567        }
568    }
569}
570
571#[derive(Debug, Clone, Eq)]
572struct FsEntryFile {
573    worktree_id: WorktreeId,
574    entry: GitEntry,
575    buffer_id: BufferId,
576    excerpts: Vec<ExcerptRange<language::Anchor>>,
577}
578
579impl PartialEq for FsEntryFile {
580    fn eq(&self, other: &Self) -> bool {
581        self.worktree_id == other.worktree_id
582            && self.entry.id == other.entry.id
583            && self.buffer_id == other.buffer_id
584    }
585}
586
587impl Hash for FsEntryFile {
588    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
589        (self.buffer_id, self.entry.id, self.worktree_id).hash(state);
590    }
591}
592
593#[derive(Debug, Clone, Eq)]
594struct FsEntryDirectory {
595    worktree_id: WorktreeId,
596    entry: GitEntry,
597}
598
599impl PartialEq for FsEntryDirectory {
600    fn eq(&self, other: &Self) -> bool {
601        self.worktree_id == other.worktree_id && self.entry.id == other.entry.id
602    }
603}
604
605impl Hash for FsEntryDirectory {
606    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
607        (self.worktree_id, self.entry.id).hash(state);
608    }
609}
610
611#[derive(Debug, Clone, Eq)]
612struct FsEntryExternalFile {
613    buffer_id: BufferId,
614    excerpts: Vec<ExcerptRange<language::Anchor>>,
615}
616
617impl PartialEq for FsEntryExternalFile {
618    fn eq(&self, other: &Self) -> bool {
619        self.buffer_id == other.buffer_id
620    }
621}
622
623impl Hash for FsEntryExternalFile {
624    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
625        self.buffer_id.hash(state);
626    }
627}
628
629#[derive(Clone, Debug, Eq, PartialEq)]
630enum FsEntry {
631    ExternalFile(FsEntryExternalFile),
632    Directory(FsEntryDirectory),
633    File(FsEntryFile),
634}
635
636struct ActiveItem {
637    item_handle: Box<dyn WeakItemHandle>,
638    active_editor: WeakEntity<Editor>,
639    _buffer_search_subscription: Subscription,
640    _editor_subscription: Subscription,
641}
642
643#[derive(Debug)]
644pub enum Event {
645    Focus,
646}
647
648#[derive(Serialize, Deserialize)]
649struct SerializedOutlinePanel {
650    active: Option<bool>,
651}
652
653pub fn init(cx: &mut App) {
654    cx.observe_new(|workspace: &mut Workspace, _, _| {
655        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
656            workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
657        });
658        workspace.register_action(|workspace, _: &Toggle, window, cx| {
659            if !workspace.toggle_panel_focus::<OutlinePanel>(window, cx) {
660                workspace.close_panel::<OutlinePanel>(window, cx);
661            }
662        });
663    })
664    .detach();
665}
666
667impl OutlinePanel {
668    pub async fn load(
669        workspace: WeakEntity<Workspace>,
670        mut cx: AsyncWindowContext,
671    ) -> anyhow::Result<Entity<Self>> {
672        let serialized_panel = match workspace
673            .read_with(&cx, |workspace, _| {
674                OutlinePanel::serialization_key(workspace)
675            })
676            .ok()
677            .flatten()
678        {
679            Some(serialization_key) => {
680                let kvp = cx.update(|_, cx| KeyValueStore::global(cx))?;
681                cx.background_spawn(async move { kvp.read_kvp(&serialization_key) })
682                    .await
683                    .context("loading outline panel")
684                    .log_err()
685                    .flatten()
686                    .map(|panel| serde_json::from_str::<SerializedOutlinePanel>(&panel))
687                    .transpose()
688                    .log_err()
689                    .flatten()
690            }
691            None => None,
692        };
693
694        workspace.update_in(&mut cx, |workspace, window, cx| {
695            let panel = Self::new(workspace, serialized_panel.as_ref(), window, cx);
696            panel.update(cx, |_, cx| cx.notify());
697            panel
698        })
699    }
700
701    fn new(
702        workspace: &mut Workspace,
703        serialized: Option<&SerializedOutlinePanel>,
704        window: &mut Window,
705        cx: &mut Context<Workspace>,
706    ) -> Entity<Self> {
707        let project = workspace.project().clone();
708        let workspace_handle = cx.entity().downgrade();
709
710        cx.new(|cx| {
711            let filter_editor = cx.new(|cx| {
712                let mut editor = Editor::single_line(window, cx);
713                editor.set_placeholder_text("Search buffer symbols…", window, cx);
714                editor
715            });
716            let filter_update_subscription = cx.subscribe_in(
717                &filter_editor,
718                window,
719                |outline_panel: &mut Self, _, event, window, cx| {
720                    if let editor::EditorEvent::BufferEdited = event {
721                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
722                    }
723                },
724            );
725
726            let focus_handle = cx.focus_handle();
727            let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in);
728            let workspace_subscription = cx.subscribe_in(
729                &workspace
730                    .weak_handle()
731                    .upgrade()
732                    .expect("have a &mut Workspace"),
733                window,
734                move |outline_panel, workspace, event, window, cx| {
735                    if let workspace::Event::ActiveItemChanged = event {
736                        if let Some((new_active_item, new_active_editor)) =
737                            workspace_active_editor(workspace.read(cx), cx)
738                        {
739                            if outline_panel.should_replace_active_item(new_active_item.as_ref()) {
740                                outline_panel.replace_active_editor(
741                                    new_active_item,
742                                    new_active_editor,
743                                    window,
744                                    cx,
745                                );
746                            }
747                        } else {
748                            outline_panel.clear_previous(window, cx);
749                            cx.notify();
750                        }
751                    }
752                },
753            );
754
755            let icons_subscription = cx.observe_global::<FileIcons>(|_, cx| {
756                cx.notify();
757            });
758
759            let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx);
760            let mut current_theme = ThemeSettings::get_global(cx).clone();
761            let mut document_symbols_by_buffer = HashMap::default();
762            let settings_subscription =
763                cx.observe_global_in::<SettingsStore>(window, move |outline_panel, window, cx| {
764                    let new_settings = OutlinePanelSettings::get_global(cx);
765                    let new_theme = ThemeSettings::get_global(cx);
766                    let mut outlines_invalidated = false;
767                    if &current_theme != new_theme {
768                        outline_panel_settings = *new_settings;
769                        current_theme = new_theme.clone();
770                        for buffer in outline_panel.buffers.values_mut() {
771                            buffer.invalidate_outlines();
772                        }
773                        outlines_invalidated = true;
774                        let update_cached_items = outline_panel.update_non_fs_items(window, cx);
775                        if update_cached_items {
776                            outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
777                        }
778                    } else if &outline_panel_settings != new_settings {
779                        let old_expansion_depth = outline_panel_settings.expand_outlines_with_depth;
780                        outline_panel_settings = *new_settings;
781
782                        if old_expansion_depth != new_settings.expand_outlines_with_depth {
783                            let old_collapsed_entries = outline_panel.collapsed_entries.clone();
784                            outline_panel
785                                .collapsed_entries
786                                .retain(|entry| !matches!(entry, CollapsedEntry::Outline(..)));
787
788                            let new_depth = new_settings.expand_outlines_with_depth;
789
790                            for (buffer_id, buffer) in &outline_panel.buffers {
791                                if let OutlineState::Outlines(outlines) = &buffer.outlines {
792                                    for outline in outlines {
793                                        if outline_panel
794                                            .outline_children_cache
795                                            .get(buffer_id)
796                                            .and_then(|children_map| {
797                                                let key = (outline.range.clone(), outline.depth);
798                                                children_map.get(&key)
799                                            })
800                                            .copied()
801                                            .unwrap_or(false)
802                                            && (new_depth == 0 || outline.depth >= new_depth)
803                                        {
804                                            outline_panel.collapsed_entries.insert(
805                                                CollapsedEntry::Outline(outline.range.clone()),
806                                            );
807                                        }
808                                    }
809                                }
810                            }
811
812                            if old_collapsed_entries != outline_panel.collapsed_entries {
813                                outline_panel.update_cached_entries(
814                                    Some(UPDATE_DEBOUNCE),
815                                    window,
816                                    cx,
817                                );
818                            }
819                        } else {
820                            cx.notify();
821                        }
822                    }
823
824                    if !outlines_invalidated {
825                        let new_document_symbols = outline_panel
826                            .buffers
827                            .keys()
828                            .filter_map(|buffer_id| {
829                                let buffer = outline_panel
830                                    .project
831                                    .read(cx)
832                                    .buffer_for_id(*buffer_id, cx)?;
833                                let buffer = buffer.read(cx);
834                                let doc_symbols =
835                                    LanguageSettings::for_buffer(buffer, cx).document_symbols;
836                                Some((*buffer_id, doc_symbols))
837                            })
838                            .collect();
839                        if new_document_symbols != document_symbols_by_buffer {
840                            document_symbols_by_buffer = new_document_symbols;
841                            for buffer in outline_panel.buffers.values_mut() {
842                                buffer.invalidate_outlines();
843                            }
844                            let update_cached_items = outline_panel.update_non_fs_items(window, cx);
845                            if update_cached_items {
846                                outline_panel.update_cached_entries(
847                                    Some(UPDATE_DEBOUNCE),
848                                    window,
849                                    cx,
850                                );
851                            }
852                        }
853                    }
854                });
855
856            let scroll_handle = UniformListScrollHandle::new();
857
858            let mut outline_panel = Self {
859                mode: ItemsDisplayMode::Outline,
860                active: serialized.and_then(|s| s.active).unwrap_or(false),
861                pinned: false,
862                workspace: workspace_handle,
863                project,
864                fs: workspace.app_state().fs.clone(),
865                max_width_item_index: None,
866                scroll_handle,
867                rendered_entries_len: 0,
868                focus_handle,
869                filter_editor,
870                fs_entries: Vec::new(),
871                fs_entries_depth: HashMap::default(),
872                fs_children_count: HashMap::default(),
873                collapsed_entries: HashSet::default(),
874                unfolded_dirs: HashMap::default(),
875                selected_entry: SelectedEntry::None,
876                context_menu: None,
877                active_item: None,
878                pending_serialization: Task::ready(None),
879                new_entries_for_fs_update: HashSet::default(),
880                preserve_selection_on_buffer_fold_toggles: HashSet::default(),
881                pending_default_expansion_depth: None,
882                fs_entries_update_task: Task::ready(()),
883                fs_entries_update_pending: false,
884                cached_entries_update_task: Task::ready(()),
885                cached_entries_update_pending: false,
886                reveal_selection_task: Task::ready(Ok(())),
887                outline_fetch_tasks: HashMap::default(),
888                buffers: HashMap::default(),
889                cached_entries: Vec::new(),
890                _subscriptions: vec![
891                    settings_subscription,
892                    icons_subscription,
893                    focus_subscription,
894                    workspace_subscription,
895                    filter_update_subscription,
896                ],
897                outline_children_cache: HashMap::default(),
898            };
899            if let Some((item, editor)) = workspace_active_editor(workspace, cx) {
900                outline_panel.replace_active_editor(item, editor, window, cx);
901            }
902            outline_panel
903        })
904    }
905
906    fn serialization_key(workspace: &Workspace) -> Option<String> {
907        workspace
908            .database_id()
909            .map(|id| i64::from(id).to_string())
910            .or(workspace.session_id())
911            .map(|id| format!("{}-{:?}", OUTLINE_PANEL_KEY, id))
912    }
913
914    fn serialize(&mut self, cx: &mut Context<Self>) {
915        let Some(serialization_key) = self
916            .workspace
917            .read_with(cx, |workspace, _| {
918                OutlinePanel::serialization_key(workspace)
919            })
920            .ok()
921            .flatten()
922        else {
923            return;
924        };
925        let active = self.active.then_some(true);
926        let kvp = KeyValueStore::global(cx);
927        self.pending_serialization = cx.background_spawn(
928            async move {
929                kvp.write_kvp(
930                    serialization_key,
931                    serde_json::to_string(&SerializedOutlinePanel { active })?,
932                )
933                .await?;
934                anyhow::Ok(())
935            }
936            .log_err(),
937        );
938    }
939
940    fn dispatch_context(&self, window: &mut Window, cx: &mut Context<Self>) -> KeyContext {
941        let mut dispatch_context = KeyContext::new_with_defaults();
942        dispatch_context.add("OutlinePanel");
943        dispatch_context.add("menu");
944        let identifier = if self.filter_editor.focus_handle(cx).is_focused(window) {
945            "editing"
946        } else {
947            "not_editing"
948        };
949        dispatch_context.add(identifier);
950        dispatch_context
951    }
952
953    fn unfold_directory(
954        &mut self,
955        _: &UnfoldDirectory,
956        window: &mut Window,
957        cx: &mut Context<Self>,
958    ) {
959        if let Some(PanelEntry::FoldedDirs(FoldedDirsEntry {
960            worktree_id,
961            entries,
962            ..
963        })) = self.selected_entry().cloned()
964        {
965            self.unfolded_dirs
966                .entry(worktree_id)
967                .or_default()
968                .extend(entries.iter().map(|entry| entry.id));
969            self.update_cached_entries(None, window, cx);
970        }
971    }
972
973    fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
974        let (worktree_id, entry) = match self.selected_entry().cloned() {
975            Some(PanelEntry::Fs(FsEntry::Directory(directory))) => {
976                (directory.worktree_id, Some(directory.entry))
977            }
978            Some(PanelEntry::FoldedDirs(folded_dirs)) => {
979                (folded_dirs.worktree_id, folded_dirs.entries.last().cloned())
980            }
981            _ => return,
982        };
983        let Some(entry) = entry else {
984            return;
985        };
986        let unfolded_dirs = self.unfolded_dirs.get_mut(&worktree_id);
987        let worktree = self
988            .project
989            .read(cx)
990            .worktree_for_id(worktree_id, cx)
991            .map(|w| w.read(cx).snapshot());
992        let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
993            return;
994        };
995
996        unfolded_dirs.remove(&entry.id);
997        self.update_cached_entries(None, window, cx);
998    }
999
1000    fn open_selected_entry(
1001        &mut self,
1002        _: &OpenSelectedEntry,
1003        window: &mut Window,
1004        cx: &mut Context<Self>,
1005    ) {
1006        if self.filter_editor.focus_handle(cx).is_focused(window) {
1007            cx.propagate()
1008        } else if let Some(selected_entry) = self.selected_entry().cloned() {
1009            self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
1010        }
1011    }
1012
1013    fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
1014        if self.filter_editor.focus_handle(cx).is_focused(window) {
1015            self.focus_handle.focus(window, cx);
1016        } else {
1017            self.filter_editor.focus_handle(cx).focus(window, cx);
1018        }
1019
1020        if self.context_menu.is_some() {
1021            self.context_menu.take();
1022            cx.notify();
1023        }
1024    }
1025
1026    fn open_excerpts(
1027        &mut self,
1028        action: &editor::actions::OpenExcerpts,
1029        window: &mut Window,
1030        cx: &mut Context<Self>,
1031    ) {
1032        if self.filter_editor.focus_handle(cx).is_focused(window) {
1033            cx.propagate()
1034        } else if let Some((active_editor, selected_entry)) =
1035            self.active_editor().zip(self.selected_entry().cloned())
1036        {
1037            self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
1038            active_editor.update(cx, |editor, cx| editor.open_excerpts(action, window, cx));
1039        }
1040    }
1041
1042    fn open_excerpts_split(
1043        &mut self,
1044        action: &editor::actions::OpenExcerptsSplit,
1045        window: &mut Window,
1046        cx: &mut Context<Self>,
1047    ) {
1048        if self.filter_editor.focus_handle(cx).is_focused(window) {
1049            cx.propagate()
1050        } else if let Some((active_editor, selected_entry)) =
1051            self.active_editor().zip(self.selected_entry().cloned())
1052        {
1053            self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
1054            active_editor.update(cx, |editor, cx| {
1055                editor.open_excerpts_in_split(action, window, cx)
1056            });
1057        }
1058    }
1059
1060    fn scroll_editor_to_entry(
1061        &mut self,
1062        entry: &PanelEntry,
1063        prefer_selection_change: bool,
1064        prefer_focus_change: bool,
1065        window: &mut Window,
1066        cx: &mut Context<OutlinePanel>,
1067    ) {
1068        let Some(active_editor) = self.active_editor() else {
1069            return;
1070        };
1071        let active_multi_buffer = active_editor.read(cx).buffer().clone();
1072        let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
1073        let mut change_selection = prefer_selection_change;
1074        let mut change_focus = prefer_focus_change;
1075        let mut scroll_to_buffer = None;
1076        let scroll_target = match entry {
1077            PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => {
1078                change_focus = false;
1079                None
1080            }
1081            PanelEntry::Fs(FsEntry::ExternalFile(file)) => {
1082                change_selection = false;
1083                scroll_to_buffer = Some(file.buffer_id);
1084                multi_buffer_snapshot.excerpts().find_map(|excerpt_range| {
1085                    if excerpt_range.context.start.buffer_id == file.buffer_id {
1086                        multi_buffer_snapshot.anchor_in_excerpt(excerpt_range.context.start)
1087                    } else {
1088                        None
1089                    }
1090                })
1091            }
1092
1093            PanelEntry::Fs(FsEntry::File(file)) => {
1094                change_selection = false;
1095                scroll_to_buffer = Some(file.buffer_id);
1096                self.project
1097                    .update(cx, |project, cx| {
1098                        project
1099                            .path_for_entry(file.entry.id, cx)
1100                            .and_then(|path| project.get_open_buffer(&path, cx))
1101                    })
1102                    .map(|buffer| {
1103                        multi_buffer_snapshot.excerpts_for_buffer(buffer.read(cx).remote_id())
1104                    })
1105                    .and_then(|mut excerpts| {
1106                        let excerpt_range = excerpts.next()?;
1107                        multi_buffer_snapshot.anchor_in_excerpt(excerpt_range.context.start)
1108                    })
1109            }
1110            PanelEntry::Outline(OutlineEntry::Outline(outline)) => multi_buffer_snapshot
1111                .anchor_in_excerpt(outline.range.start)
1112                .or_else(|| multi_buffer_snapshot.anchor_in_excerpt(outline.range.end)),
1113            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1114                change_selection = false;
1115                change_focus = false;
1116                multi_buffer_snapshot.anchor_in_excerpt(excerpt.context.start)
1117            }
1118            PanelEntry::Search(search_entry) => Some(search_entry.match_range.start),
1119        };
1120
1121        if let Some(anchor) = scroll_target {
1122            let activate = self
1123                .workspace
1124                .update(cx, |workspace, cx| match self.active_item() {
1125                    Some(active_item) => workspace.activate_item(
1126                        active_item.as_ref(),
1127                        true,
1128                        change_focus,
1129                        window,
1130                        cx,
1131                    ),
1132                    None => workspace.activate_item(&active_editor, true, change_focus, window, cx),
1133                });
1134
1135            if activate.is_ok() {
1136                self.select_entry(entry.clone(), true, window, cx);
1137                if change_selection {
1138                    active_editor.update(cx, |editor, cx| {
1139                        editor.change_selections(
1140                            SelectionEffects::scroll(Autoscroll::center()),
1141                            window,
1142                            cx,
1143                            |s| s.select_ranges(Some(anchor..anchor)),
1144                        );
1145                    });
1146                } else {
1147                    let mut offset = Point::default();
1148                    if let Some(buffer_id) = scroll_to_buffer
1149                        && multi_buffer_snapshot.as_singleton().is_none()
1150                        && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
1151                    {
1152                        offset.y = -(active_editor.read(cx).file_header_size() as f64);
1153                    }
1154
1155                    active_editor.update(cx, |editor, cx| {
1156                        editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, window, cx);
1157                    });
1158                }
1159
1160                if change_focus {
1161                    active_editor.focus_handle(cx).focus(window, cx);
1162                } else {
1163                    self.focus_handle.focus(window, cx);
1164                }
1165            }
1166        }
1167    }
1168
1169    fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context<Self>) {
1170        for _ in 0..self.rendered_entries_len / 2 {
1171            window.dispatch_action(SelectPrevious.boxed_clone(), cx);
1172        }
1173    }
1174
1175    fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context<Self>) {
1176        for _ in 0..self.rendered_entries_len / 2 {
1177            window.dispatch_action(SelectNext.boxed_clone(), cx);
1178        }
1179    }
1180
1181    fn scroll_cursor_center(
1182        &mut self,
1183        _: &ScrollCursorCenter,
1184        _: &mut Window,
1185        cx: &mut Context<Self>,
1186    ) {
1187        if let Some(selected_entry) = self.selected_entry() {
1188            let index = self
1189                .cached_entries
1190                .iter()
1191                .position(|cached_entry| &cached_entry.entry == selected_entry);
1192            if let Some(index) = index {
1193                self.scroll_handle
1194                    .scroll_to_item_strict(index, ScrollStrategy::Center);
1195                cx.notify();
1196            }
1197        }
1198    }
1199
1200    fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, _: &mut Window, cx: &mut Context<Self>) {
1201        if let Some(selected_entry) = self.selected_entry() {
1202            let index = self
1203                .cached_entries
1204                .iter()
1205                .position(|cached_entry| &cached_entry.entry == selected_entry);
1206            if let Some(index) = index {
1207                self.scroll_handle
1208                    .scroll_to_item_strict(index, ScrollStrategy::Top);
1209                cx.notify();
1210            }
1211        }
1212    }
1213
1214    fn scroll_cursor_bottom(
1215        &mut self,
1216        _: &ScrollCursorBottom,
1217        _: &mut Window,
1218        cx: &mut Context<Self>,
1219    ) {
1220        if let Some(selected_entry) = self.selected_entry() {
1221            let index = self
1222                .cached_entries
1223                .iter()
1224                .position(|cached_entry| &cached_entry.entry == selected_entry);
1225            if let Some(index) = index {
1226                self.scroll_handle
1227                    .scroll_to_item_strict(index, ScrollStrategy::Bottom);
1228                cx.notify();
1229            }
1230        }
1231    }
1232
1233    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1234        if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1235            self.cached_entries
1236                .iter()
1237                .map(|cached_entry| &cached_entry.entry)
1238                .skip_while(|entry| entry != &selected_entry)
1239                .nth(1)
1240                .cloned()
1241        }) {
1242            self.select_entry(entry_to_select, true, window, cx);
1243        } else {
1244            self.select_first(&SelectFirst {}, window, cx)
1245        }
1246        if let Some(selected_entry) = self.selected_entry().cloned() {
1247            self.scroll_editor_to_entry(&selected_entry, true, false, window, cx);
1248        }
1249    }
1250
1251    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
1252        if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1253            self.cached_entries
1254                .iter()
1255                .rev()
1256                .map(|cached_entry| &cached_entry.entry)
1257                .skip_while(|entry| entry != &selected_entry)
1258                .nth(1)
1259                .cloned()
1260        }) {
1261            self.select_entry(entry_to_select, true, window, cx);
1262        } else {
1263            self.select_last(&SelectLast, window, cx)
1264        }
1265        if let Some(selected_entry) = self.selected_entry().cloned() {
1266            self.scroll_editor_to_entry(&selected_entry, true, false, window, cx);
1267        }
1268    }
1269
1270    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
1271        if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1272            let mut previous_entries = self
1273                .cached_entries
1274                .iter()
1275                .rev()
1276                .map(|cached_entry| &cached_entry.entry)
1277                .skip_while(|entry| entry != &selected_entry)
1278                .skip(1);
1279            match &selected_entry {
1280                PanelEntry::Fs(fs_entry) => match fs_entry {
1281                    FsEntry::ExternalFile(..) => None,
1282                    FsEntry::File(FsEntryFile {
1283                        worktree_id, entry, ..
1284                    })
1285                    | FsEntry::Directory(FsEntryDirectory {
1286                        worktree_id, entry, ..
1287                    }) => entry.path.parent().and_then(|parent_path| {
1288                        previous_entries.find(|entry| match entry {
1289                            PanelEntry::Fs(FsEntry::Directory(directory)) => {
1290                                directory.worktree_id == *worktree_id
1291                                    && directory.entry.path.as_ref() == parent_path
1292                            }
1293                            PanelEntry::FoldedDirs(FoldedDirsEntry {
1294                                worktree_id: dirs_worktree_id,
1295                                entries: dirs,
1296                                ..
1297                            }) => {
1298                                dirs_worktree_id == worktree_id
1299                                    && dirs
1300                                        .last()
1301                                        .is_some_and(|dir| dir.path.as_ref() == parent_path)
1302                            }
1303                            _ => false,
1304                        })
1305                    }),
1306                },
1307                PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
1308                    .entries
1309                    .first()
1310                    .and_then(|entry| entry.path.parent())
1311                    .and_then(|parent_path| {
1312                        previous_entries.find(|entry| {
1313                            if let PanelEntry::Fs(FsEntry::Directory(directory)) = entry {
1314                                directory.worktree_id == folded_dirs.worktree_id
1315                                    && directory.entry.path.as_ref() == parent_path
1316                            } else {
1317                                false
1318                            }
1319                        })
1320                    }),
1321                PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1322                    previous_entries.find(|entry| match entry {
1323                        PanelEntry::Fs(FsEntry::File(file)) => {
1324                            file.buffer_id == excerpt.context.start.buffer_id
1325                                && file.excerpts.contains(&excerpt)
1326                        }
1327                        PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1328                            external_file.buffer_id == excerpt.context.start.buffer_id
1329                                && external_file.excerpts.contains(&excerpt)
1330                        }
1331                        _ => false,
1332                    })
1333                }
1334                PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1335                    previous_entries.find(|entry| {
1336                        if let PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) = entry {
1337                            if outline.range.start.buffer_id != excerpt.context.start.buffer_id {
1338                                return false;
1339                            }
1340                            let Some(buffer_snapshot) =
1341                                self.buffer_snapshot_for_id(outline.range.start.buffer_id, cx)
1342                            else {
1343                                return false;
1344                            };
1345                            excerpt.contains(&outline.range.start, &buffer_snapshot)
1346                                || excerpt.contains(&outline.range.end, &buffer_snapshot)
1347                        } else {
1348                            false
1349                        }
1350                    })
1351                }
1352                PanelEntry::Search(_) => {
1353                    previous_entries.find(|entry| !matches!(entry, PanelEntry::Search(_)))
1354                }
1355            }
1356        }) {
1357            self.select_entry(entry_to_select.clone(), true, window, cx);
1358        } else {
1359            self.select_first(&SelectFirst {}, window, cx);
1360        }
1361    }
1362
1363    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1364        if let Some(first_entry) = self.cached_entries.first() {
1365            self.select_entry(first_entry.entry.clone(), true, window, cx);
1366        }
1367    }
1368
1369    fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
1370        if let Some(new_selection) = self
1371            .cached_entries
1372            .iter()
1373            .rev()
1374            .map(|cached_entry| &cached_entry.entry)
1375            .next()
1376        {
1377            self.select_entry(new_selection.clone(), true, window, cx);
1378        }
1379    }
1380
1381    fn autoscroll(&mut self, cx: &mut Context<Self>) {
1382        if let Some(selected_entry) = self.selected_entry() {
1383            let index = self
1384                .cached_entries
1385                .iter()
1386                .position(|cached_entry| &cached_entry.entry == selected_entry);
1387            if let Some(index) = index {
1388                self.scroll_handle
1389                    .scroll_to_item(index, ScrollStrategy::Center);
1390                cx.notify();
1391            }
1392        }
1393    }
1394
1395    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1396        if !self.focus_handle.contains_focused(window, cx) {
1397            cx.emit(Event::Focus);
1398        }
1399    }
1400
1401    fn deploy_context_menu(
1402        &mut self,
1403        position: Point<Pixels>,
1404        entry: PanelEntry,
1405        window: &mut Window,
1406        cx: &mut Context<Self>,
1407    ) {
1408        self.select_entry(entry.clone(), true, window, cx);
1409        let is_root = match &entry {
1410            PanelEntry::Fs(FsEntry::File(FsEntryFile {
1411                worktree_id, entry, ..
1412            }))
1413            | PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1414                worktree_id, entry, ..
1415            })) => self
1416                .project
1417                .read(cx)
1418                .worktree_for_id(*worktree_id, cx)
1419                .map(|worktree| {
1420                    worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1421                })
1422                .unwrap_or(false),
1423            PanelEntry::FoldedDirs(FoldedDirsEntry {
1424                worktree_id,
1425                entries,
1426                ..
1427            }) => entries
1428                .first()
1429                .and_then(|entry| {
1430                    self.project
1431                        .read(cx)
1432                        .worktree_for_id(*worktree_id, cx)
1433                        .map(|worktree| {
1434                            worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1435                        })
1436                })
1437                .unwrap_or(false),
1438            PanelEntry::Fs(FsEntry::ExternalFile(..)) => false,
1439            PanelEntry::Outline(..) => {
1440                cx.notify();
1441                return;
1442            }
1443            PanelEntry::Search(_) => {
1444                cx.notify();
1445                return;
1446            }
1447        };
1448        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
1449        let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(&entry);
1450        let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(&entry);
1451
1452        let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
1453            menu.context(self.focus_handle.clone())
1454                .action(
1455                    ui::utils::reveal_in_file_manager_label(false),
1456                    Box::new(RevealInFileManager),
1457                )
1458                .action("Open in Terminal", Box::new(OpenInTerminal))
1459                .when(is_unfoldable, |menu| {
1460                    menu.action("Unfold Directory", Box::new(UnfoldDirectory))
1461                })
1462                .when(is_foldable, |menu| {
1463                    menu.action("Fold Directory", Box::new(FoldDirectory))
1464                })
1465                .separator()
1466                .action("Copy Path", Box::new(zed_actions::workspace::CopyPath))
1467                .action(
1468                    "Copy Relative Path",
1469                    Box::new(zed_actions::workspace::CopyRelativePath),
1470                )
1471        });
1472        window.focus(&context_menu.focus_handle(cx), cx);
1473        let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
1474            outline_panel.context_menu.take();
1475            cx.notify();
1476        });
1477        self.context_menu = Some((context_menu, position, subscription));
1478        cx.notify();
1479    }
1480
1481    fn is_unfoldable(&self, entry: &PanelEntry) -> bool {
1482        matches!(entry, PanelEntry::FoldedDirs(..))
1483    }
1484
1485    fn is_foldable(&self, entry: &PanelEntry) -> bool {
1486        let (directory_worktree, directory_entry) = match entry {
1487            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1488                worktree_id,
1489                entry: directory_entry,
1490                ..
1491            })) => (*worktree_id, Some(directory_entry)),
1492            _ => return false,
1493        };
1494        let Some(directory_entry) = directory_entry else {
1495            return false;
1496        };
1497
1498        if self
1499            .unfolded_dirs
1500            .get(&directory_worktree)
1501            .is_none_or(|unfolded_dirs| !unfolded_dirs.contains(&directory_entry.id))
1502        {
1503            return false;
1504        }
1505
1506        let children = self
1507            .fs_children_count
1508            .get(&directory_worktree)
1509            .and_then(|entries| entries.get(&directory_entry.path))
1510            .copied()
1511            .unwrap_or_default();
1512
1513        children.may_be_fold_part() && children.dirs > 0
1514    }
1515
1516    fn expand_selected_entry(
1517        &mut self,
1518        _: &ExpandSelectedEntry,
1519        window: &mut Window,
1520        cx: &mut Context<Self>,
1521    ) {
1522        let Some(active_editor) = self.active_editor() else {
1523            return;
1524        };
1525        let Some(selected_entry) = self.selected_entry().cloned() else {
1526            return;
1527        };
1528        let mut buffers_to_unfold = HashSet::default();
1529        let entry_to_expand = match &selected_entry {
1530            PanelEntry::FoldedDirs(FoldedDirsEntry {
1531                entries: dir_entries,
1532                worktree_id,
1533                ..
1534            }) => dir_entries.last().map(|entry| {
1535                buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry));
1536                CollapsedEntry::Dir(*worktree_id, entry.id)
1537            }),
1538            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1539                worktree_id, entry, ..
1540            })) => {
1541                buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry));
1542                Some(CollapsedEntry::Dir(*worktree_id, entry.id))
1543            }
1544            PanelEntry::Fs(FsEntry::File(FsEntryFile {
1545                worktree_id,
1546                buffer_id,
1547                ..
1548            })) => {
1549                buffers_to_unfold.insert(*buffer_id);
1550                Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1551            }
1552            PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1553                buffers_to_unfold.insert(external_file.buffer_id);
1554                Some(CollapsedEntry::ExternalFile(external_file.buffer_id))
1555            }
1556            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1557                Some(CollapsedEntry::Excerpt(excerpt.clone()))
1558            }
1559            PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1560                Some(CollapsedEntry::Outline(outline.range.clone()))
1561            }
1562            PanelEntry::Search(_) => return,
1563        };
1564        let Some(collapsed_entry) = entry_to_expand else {
1565            return;
1566        };
1567        let expanded = self.collapsed_entries.remove(&collapsed_entry);
1568        if expanded {
1569            if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
1570                let task = self.project.update(cx, |project, cx| {
1571                    project.expand_entry(worktree_id, dir_entry_id, cx)
1572                });
1573                if let Some(task) = task {
1574                    task.detach_and_log_err(cx);
1575                }
1576            };
1577
1578            active_editor.update(cx, |editor, cx| {
1579                buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
1580            });
1581            self.select_entry(selected_entry, true, window, cx);
1582            if buffers_to_unfold.is_empty() {
1583                self.update_cached_entries(None, window, cx);
1584            } else {
1585                self.toggle_buffers_fold(buffers_to_unfold, false, window, cx)
1586                    .detach();
1587            }
1588        } else {
1589            self.select_next(&SelectNext, window, cx)
1590        }
1591    }
1592
1593    fn collapse_selected_entry(
1594        &mut self,
1595        _: &CollapseSelectedEntry,
1596        window: &mut Window,
1597        cx: &mut Context<Self>,
1598    ) {
1599        let Some(active_editor) = self.active_editor() else {
1600            return;
1601        };
1602        let Some(selected_entry) = self.selected_entry().cloned() else {
1603            return;
1604        };
1605
1606        let mut buffers_to_fold = HashSet::default();
1607        let collapsed = match &selected_entry {
1608            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1609                worktree_id, entry, ..
1610            })) => {
1611                if self
1612                    .collapsed_entries
1613                    .insert(CollapsedEntry::Dir(*worktree_id, entry.id))
1614                {
1615                    buffers_to_fold.extend(self.buffers_inside_directory(*worktree_id, entry));
1616                    true
1617                } else {
1618                    false
1619                }
1620            }
1621            PanelEntry::Fs(FsEntry::File(FsEntryFile {
1622                worktree_id,
1623                buffer_id,
1624                ..
1625            })) => {
1626                if self
1627                    .collapsed_entries
1628                    .insert(CollapsedEntry::File(*worktree_id, *buffer_id))
1629                {
1630                    buffers_to_fold.insert(*buffer_id);
1631                    true
1632                } else {
1633                    false
1634                }
1635            }
1636            PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1637                if self
1638                    .collapsed_entries
1639                    .insert(CollapsedEntry::ExternalFile(external_file.buffer_id))
1640                {
1641                    buffers_to_fold.insert(external_file.buffer_id);
1642                    true
1643                } else {
1644                    false
1645                }
1646            }
1647            PanelEntry::FoldedDirs(folded_dirs) => {
1648                let mut folded = false;
1649                if let Some(dir_entry) = folded_dirs.entries.last()
1650                    && self
1651                        .collapsed_entries
1652                        .insert(CollapsedEntry::Dir(folded_dirs.worktree_id, dir_entry.id))
1653                {
1654                    folded = true;
1655                    buffers_to_fold
1656                        .extend(self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry));
1657                }
1658                folded
1659            }
1660            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
1661                .collapsed_entries
1662                .insert(CollapsedEntry::Excerpt(excerpt.clone())),
1663            PanelEntry::Outline(OutlineEntry::Outline(outline)) => self
1664                .collapsed_entries
1665                .insert(CollapsedEntry::Outline(outline.range.clone())),
1666            PanelEntry::Search(_) => false,
1667        };
1668
1669        if collapsed {
1670            active_editor.update(cx, |editor, cx| {
1671                buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
1672            });
1673            self.select_entry(selected_entry, true, window, cx);
1674            if buffers_to_fold.is_empty() {
1675                self.update_cached_entries(None, window, cx);
1676            } else {
1677                self.toggle_buffers_fold(buffers_to_fold, true, window, cx)
1678                    .detach();
1679            }
1680        } else {
1681            self.select_parent(&SelectParent, window, cx);
1682        }
1683    }
1684
1685    pub fn expand_all_entries(
1686        &mut self,
1687        _: &ExpandAllEntries,
1688        window: &mut Window,
1689        cx: &mut Context<Self>,
1690    ) {
1691        let Some(active_editor) = self.active_editor() else {
1692            return;
1693        };
1694
1695        let mut to_uncollapse: HashSet<CollapsedEntry> = HashSet::default();
1696        let mut buffers_to_unfold: HashSet<BufferId> = HashSet::default();
1697
1698        for fs_entry in &self.fs_entries {
1699            match fs_entry {
1700                FsEntry::File(FsEntryFile {
1701                    worktree_id,
1702                    buffer_id,
1703                    ..
1704                }) => {
1705                    to_uncollapse.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
1706                    buffers_to_unfold.insert(*buffer_id);
1707                }
1708                FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
1709                    to_uncollapse.insert(CollapsedEntry::ExternalFile(*buffer_id));
1710                    buffers_to_unfold.insert(*buffer_id);
1711                }
1712                FsEntry::Directory(FsEntryDirectory {
1713                    worktree_id, entry, ..
1714                }) => {
1715                    to_uncollapse.insert(CollapsedEntry::Dir(*worktree_id, entry.id));
1716                }
1717            }
1718        }
1719
1720        for (_buffer_id, buffer) in &self.buffers {
1721            match &buffer.outlines {
1722                OutlineState::Outlines(outlines) => {
1723                    for outline in outlines {
1724                        to_uncollapse.insert(CollapsedEntry::Outline(outline.range.clone()));
1725                    }
1726                }
1727                OutlineState::Invalidated(outlines) => {
1728                    for outline in outlines {
1729                        to_uncollapse.insert(CollapsedEntry::Outline(outline.range.clone()));
1730                    }
1731                }
1732                OutlineState::NotFetched => {}
1733            }
1734            to_uncollapse.extend(
1735                buffer
1736                    .excerpts
1737                    .iter()
1738                    .map(|excerpt| CollapsedEntry::Excerpt(excerpt.clone())),
1739            );
1740        }
1741
1742        for cached in &self.cached_entries {
1743            if let PanelEntry::FoldedDirs(FoldedDirsEntry {
1744                worktree_id,
1745                entries,
1746                ..
1747            }) = &cached.entry
1748            {
1749                if let Some(last) = entries.last() {
1750                    to_uncollapse.insert(CollapsedEntry::Dir(*worktree_id, last.id));
1751                }
1752            }
1753        }
1754
1755        self.collapsed_entries
1756            .retain(|entry| !to_uncollapse.contains(entry));
1757
1758        active_editor.update(cx, |editor, cx| {
1759            buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
1760        });
1761
1762        if buffers_to_unfold.is_empty() {
1763            self.update_cached_entries(None, window, cx);
1764        } else {
1765            self.toggle_buffers_fold(buffers_to_unfold, false, window, cx)
1766                .detach();
1767        }
1768    }
1769
1770    pub fn collapse_all_entries(
1771        &mut self,
1772        _: &CollapseAllEntries,
1773        window: &mut Window,
1774        cx: &mut Context<Self>,
1775    ) {
1776        let Some(active_editor) = self.active_editor() else {
1777            return;
1778        };
1779        let mut buffers_to_fold = HashSet::default();
1780        self.collapsed_entries
1781            .extend(self.cached_entries.iter().filter_map(
1782                |cached_entry| match &cached_entry.entry {
1783                    PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1784                        worktree_id,
1785                        entry,
1786                        ..
1787                    })) => Some(CollapsedEntry::Dir(*worktree_id, entry.id)),
1788                    PanelEntry::Fs(FsEntry::File(FsEntryFile {
1789                        worktree_id,
1790                        buffer_id,
1791                        ..
1792                    })) => {
1793                        buffers_to_fold.insert(*buffer_id);
1794                        Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1795                    }
1796                    PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1797                        buffers_to_fold.insert(external_file.buffer_id);
1798                        Some(CollapsedEntry::ExternalFile(external_file.buffer_id))
1799                    }
1800                    PanelEntry::FoldedDirs(FoldedDirsEntry {
1801                        worktree_id,
1802                        entries,
1803                        ..
1804                    }) => Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)),
1805                    PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1806                        Some(CollapsedEntry::Excerpt(excerpt.clone()))
1807                    }
1808                    PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1809                        Some(CollapsedEntry::Outline(outline.range.clone()))
1810                    }
1811                    PanelEntry::Search(_) => None,
1812                },
1813            ));
1814
1815        active_editor.update(cx, |editor, cx| {
1816            buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
1817        });
1818        if buffers_to_fold.is_empty() {
1819            self.update_cached_entries(None, window, cx);
1820        } else {
1821            self.toggle_buffers_fold(buffers_to_fold, true, window, cx)
1822                .detach();
1823        }
1824    }
1825
1826    fn toggle_expanded(&mut self, entry: &PanelEntry, window: &mut Window, cx: &mut Context<Self>) {
1827        let Some(active_editor) = self.active_editor() else {
1828            return;
1829        };
1830        let mut fold = false;
1831        let mut buffers_to_toggle = HashSet::default();
1832        match entry {
1833            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1834                worktree_id,
1835                entry: dir_entry,
1836                ..
1837            })) => {
1838                let entry_id = dir_entry.id;
1839                let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1840                buffers_to_toggle.extend(self.buffers_inside_directory(*worktree_id, dir_entry));
1841                if self.collapsed_entries.remove(&collapsed_entry) {
1842                    self.project
1843                        .update(cx, |project, cx| {
1844                            project.expand_entry(*worktree_id, entry_id, cx)
1845                        })
1846                        .unwrap_or_else(|| Task::ready(Ok(())))
1847                        .detach_and_log_err(cx);
1848                } else {
1849                    self.collapsed_entries.insert(collapsed_entry);
1850                    fold = true;
1851                }
1852            }
1853            PanelEntry::Fs(FsEntry::File(FsEntryFile {
1854                worktree_id,
1855                buffer_id,
1856                ..
1857            })) => {
1858                let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1859                buffers_to_toggle.insert(*buffer_id);
1860                if !self.collapsed_entries.remove(&collapsed_entry) {
1861                    self.collapsed_entries.insert(collapsed_entry);
1862                    fold = true;
1863                }
1864            }
1865            PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1866                let collapsed_entry = CollapsedEntry::ExternalFile(external_file.buffer_id);
1867                buffers_to_toggle.insert(external_file.buffer_id);
1868                if !self.collapsed_entries.remove(&collapsed_entry) {
1869                    self.collapsed_entries.insert(collapsed_entry);
1870                    fold = true;
1871                }
1872            }
1873            PanelEntry::FoldedDirs(FoldedDirsEntry {
1874                worktree_id,
1875                entries: dir_entries,
1876                ..
1877            }) => {
1878                if let Some(dir_entry) = dir_entries.first() {
1879                    let entry_id = dir_entry.id;
1880                    let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1881                    buffers_to_toggle
1882                        .extend(self.buffers_inside_directory(*worktree_id, dir_entry));
1883                    if self.collapsed_entries.remove(&collapsed_entry) {
1884                        self.project
1885                            .update(cx, |project, cx| {
1886                                project.expand_entry(*worktree_id, entry_id, cx)
1887                            })
1888                            .unwrap_or_else(|| Task::ready(Ok(())))
1889                            .detach_and_log_err(cx);
1890                    } else {
1891                        self.collapsed_entries.insert(collapsed_entry);
1892                        fold = true;
1893                    }
1894                }
1895            }
1896            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1897                let collapsed_entry = CollapsedEntry::Excerpt(excerpt.clone());
1898                if !self.collapsed_entries.remove(&collapsed_entry) {
1899                    self.collapsed_entries.insert(collapsed_entry);
1900                }
1901            }
1902            PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1903                let collapsed_entry = CollapsedEntry::Outline(outline.range.clone());
1904                if !self.collapsed_entries.remove(&collapsed_entry) {
1905                    self.collapsed_entries.insert(collapsed_entry);
1906                }
1907            }
1908            _ => {}
1909        }
1910
1911        active_editor.update(cx, |editor, cx| {
1912            buffers_to_toggle.retain(|buffer_id| {
1913                let folded = editor.is_buffer_folded(*buffer_id, cx);
1914                if fold { !folded } else { folded }
1915            });
1916        });
1917
1918        self.select_entry(entry.clone(), true, window, cx);
1919        if buffers_to_toggle.is_empty() {
1920            self.update_cached_entries(None, window, cx);
1921        } else {
1922            self.toggle_buffers_fold(buffers_to_toggle, fold, window, cx)
1923                .detach();
1924        }
1925    }
1926
1927    fn toggle_buffers_fold(
1928        &self,
1929        buffers: HashSet<BufferId>,
1930        fold: bool,
1931        window: &mut Window,
1932        cx: &mut Context<Self>,
1933    ) -> Task<()> {
1934        let Some(active_editor) = self.active_editor() else {
1935            return Task::ready(());
1936        };
1937        cx.spawn_in(window, async move |outline_panel, cx| {
1938            outline_panel
1939                .update_in(cx, |outline_panel, window, cx| {
1940                    active_editor.update(cx, |editor, cx| {
1941                        for buffer_id in buffers {
1942                            outline_panel
1943                                .preserve_selection_on_buffer_fold_toggles
1944                                .insert(buffer_id);
1945                            if fold {
1946                                editor.fold_buffer(buffer_id, cx);
1947                            } else {
1948                                editor.unfold_buffer(buffer_id, cx);
1949                            }
1950                        }
1951                    });
1952                    if let Some(selection) = outline_panel.selected_entry().cloned() {
1953                        outline_panel.scroll_editor_to_entry(&selection, false, false, window, cx);
1954                    }
1955                })
1956                .ok();
1957        })
1958    }
1959
1960    fn copy_path(
1961        &mut self,
1962        _: &zed_actions::workspace::CopyPath,
1963        _: &mut Window,
1964        cx: &mut Context<Self>,
1965    ) {
1966        if let Some(clipboard_text) = self
1967            .selected_entry()
1968            .and_then(|entry| self.abs_path(entry, cx))
1969            .map(|p| p.to_string_lossy().into_owned())
1970        {
1971            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1972        }
1973    }
1974
1975    fn copy_relative_path(
1976        &mut self,
1977        _: &zed_actions::workspace::CopyRelativePath,
1978        _: &mut Window,
1979        cx: &mut Context<Self>,
1980    ) {
1981        let path_style = self.project.read(cx).path_style(cx);
1982        if let Some(clipboard_text) = self
1983            .selected_entry()
1984            .and_then(|entry| match entry {
1985                PanelEntry::Fs(entry) => self.relative_path(entry, cx),
1986                PanelEntry::FoldedDirs(folded_dirs) => {
1987                    folded_dirs.entries.last().map(|entry| entry.path.clone())
1988                }
1989                PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1990            })
1991            .map(|p| p.display(path_style).to_string())
1992        {
1993            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1994        }
1995    }
1996
1997    fn reveal_in_finder(
1998        &mut self,
1999        _: &RevealInFileManager,
2000        _: &mut Window,
2001        cx: &mut Context<Self>,
2002    ) {
2003        if let Some(abs_path) = self
2004            .selected_entry()
2005            .and_then(|entry| self.abs_path(entry, cx))
2006        {
2007            self.project
2008                .update(cx, |project, cx| project.reveal_path(&abs_path, cx));
2009        }
2010    }
2011
2012    fn open_in_terminal(
2013        &mut self,
2014        _: &OpenInTerminal,
2015        window: &mut Window,
2016        cx: &mut Context<Self>,
2017    ) {
2018        let selected_entry = self.selected_entry();
2019        let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx));
2020        let working_directory = if let (
2021            Some(abs_path),
2022            Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))),
2023        ) = (&abs_path, selected_entry)
2024        {
2025            abs_path.parent().map(|p| p.to_owned())
2026        } else {
2027            abs_path
2028        };
2029
2030        if let Some(working_directory) = working_directory {
2031            window.dispatch_action(
2032                workspace::OpenTerminal {
2033                    working_directory,
2034                    local: false,
2035                }
2036                .boxed_clone(),
2037                cx,
2038            )
2039        }
2040    }
2041
2042    fn reveal_entry_for_selection(
2043        &mut self,
2044        editor: Entity<Editor>,
2045        window: &mut Window,
2046        cx: &mut Context<Self>,
2047    ) {
2048        if !self.active
2049            || !OutlinePanelSettings::get_global(cx).auto_reveal_entries
2050            || self.focus_handle.contains_focused(window, cx)
2051        {
2052            return;
2053        }
2054        let project = self.project.clone();
2055        self.reveal_selection_task = cx.spawn_in(window, async move |outline_panel, cx| {
2056            cx.background_executor().timer(UPDATE_DEBOUNCE).await;
2057            let multibuffer_snapshot =
2058                editor.read_with(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx));
2059            let entry_with_selection =
2060                outline_panel.update_in(cx, |outline_panel, window, cx| {
2061                    outline_panel.location_for_editor_selection(&editor, window, cx)
2062                })?;
2063            let Some(entry_with_selection) = entry_with_selection else {
2064                outline_panel.update(cx, |outline_panel, cx| {
2065                    outline_panel.selected_entry = SelectedEntry::None;
2066                    cx.notify();
2067                })?;
2068                return Ok(());
2069            };
2070            let related_buffer_entry = match &entry_with_selection {
2071                PanelEntry::Fs(FsEntry::File(FsEntryFile {
2072                    worktree_id,
2073                    buffer_id,
2074                    ..
2075                })) => project.update(cx, |project, cx| {
2076                    let entry_id = project
2077                        .buffer_for_id(*buffer_id, cx)
2078                        .and_then(|buffer| buffer.read(cx).entry_id(cx));
2079                    project
2080                        .worktree_for_id(*worktree_id, cx)
2081                        .zip(entry_id)
2082                        .and_then(|(worktree, entry_id)| {
2083                            let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
2084                            Some((worktree, entry))
2085                        })
2086                }),
2087                PanelEntry::Outline(outline_entry) => {
2088                    let buffer_id = outline_entry.buffer_id();
2089                    let outline_range = outline_entry.range();
2090                    outline_panel.update(cx, |outline_panel, cx| {
2091                        outline_panel
2092                            .collapsed_entries
2093                            .remove(&CollapsedEntry::ExternalFile(buffer_id));
2094                        if let Some(buffer_snapshot) =
2095                            outline_panel.buffer_snapshot_for_id(buffer_id, cx)
2096                        {
2097                            outline_panel.collapsed_entries.retain(|entry| match entry {
2098                                CollapsedEntry::Excerpt(excerpt_range) => {
2099                                    let intersects = excerpt_range.context.start.buffer_id
2100                                        == buffer_id
2101                                        && (excerpt_range
2102                                            .contains(&outline_range.start, &buffer_snapshot)
2103                                            || excerpt_range
2104                                                .contains(&outline_range.end, &buffer_snapshot));
2105                                    !intersects
2106                                }
2107                                _ => true,
2108                            });
2109                        }
2110                        let project = outline_panel.project.read(cx);
2111                        let entry_id = project
2112                            .buffer_for_id(buffer_id, cx)
2113                            .and_then(|buffer| buffer.read(cx).entry_id(cx));
2114
2115                        entry_id.and_then(|entry_id| {
2116                            project
2117                                .worktree_for_entry(entry_id, cx)
2118                                .and_then(|worktree| {
2119                                    let worktree_id = worktree.read(cx).id();
2120                                    outline_panel
2121                                        .collapsed_entries
2122                                        .remove(&CollapsedEntry::File(worktree_id, buffer_id));
2123                                    let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
2124                                    Some((worktree, entry))
2125                                })
2126                        })
2127                    })?
2128                }
2129                PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
2130                PanelEntry::Search(SearchEntry { match_range, .. }) => multibuffer_snapshot
2131                    .anchor_to_buffer_anchor(match_range.start)
2132                    .map(|(anchor, _)| anchor.buffer_id)
2133                    .map(|buffer_id| {
2134                        outline_panel.update(cx, |outline_panel, cx| {
2135                            outline_panel
2136                                .collapsed_entries
2137                                .remove(&CollapsedEntry::ExternalFile(buffer_id));
2138                            let project = project.read(cx);
2139                            let entry_id = project
2140                                .buffer_for_id(buffer_id, cx)
2141                                .and_then(|buffer| buffer.read(cx).entry_id(cx));
2142
2143                            entry_id.and_then(|entry_id| {
2144                                project
2145                                    .worktree_for_entry(entry_id, cx)
2146                                    .and_then(|worktree| {
2147                                        let worktree_id = worktree.read(cx).id();
2148                                        outline_panel
2149                                            .collapsed_entries
2150                                            .remove(&CollapsedEntry::File(worktree_id, buffer_id));
2151                                        let entry =
2152                                            worktree.read(cx).entry_for_id(entry_id)?.clone();
2153                                        Some((worktree, entry))
2154                                    })
2155                            })
2156                        })
2157                    })
2158                    .transpose()?
2159                    .flatten(),
2160                _ => return anyhow::Ok(()),
2161            };
2162            if let Some((worktree, buffer_entry)) = related_buffer_entry {
2163                outline_panel.update(cx, |outline_panel, cx| {
2164                    let worktree_id = worktree.read(cx).id();
2165                    let mut dirs_to_expand = Vec::new();
2166                    {
2167                        let mut traversal = worktree.read(cx).traverse_from_path(
2168                            true,
2169                            true,
2170                            true,
2171                            buffer_entry.path.as_ref(),
2172                        );
2173                        let mut current_entry = buffer_entry;
2174                        loop {
2175                            if current_entry.is_dir()
2176                                && outline_panel
2177                                    .collapsed_entries
2178                                    .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
2179                            {
2180                                dirs_to_expand.push(current_entry.id);
2181                            }
2182
2183                            if traversal.back_to_parent()
2184                                && let Some(parent_entry) = traversal.entry()
2185                            {
2186                                current_entry = parent_entry.clone();
2187                                continue;
2188                            }
2189                            break;
2190                        }
2191                    }
2192                    for dir_to_expand in dirs_to_expand {
2193                        project
2194                            .update(cx, |project, cx| {
2195                                project.expand_entry(worktree_id, dir_to_expand, cx)
2196                            })
2197                            .unwrap_or_else(|| Task::ready(Ok(())))
2198                            .detach_and_log_err(cx)
2199                    }
2200                })?
2201            }
2202
2203            outline_panel.update_in(cx, |outline_panel, window, cx| {
2204                outline_panel.select_entry(entry_with_selection, false, window, cx);
2205                outline_panel.update_cached_entries(None, window, cx);
2206            })?;
2207
2208            anyhow::Ok(())
2209        });
2210    }
2211
2212    fn render_excerpt(
2213        &self,
2214        excerpt: &ExcerptRange<Anchor>,
2215        depth: usize,
2216        window: &mut Window,
2217        cx: &mut Context<OutlinePanel>,
2218    ) -> Option<Stateful<Div>> {
2219        let item_id = ElementId::from(format!("{excerpt:?}"));
2220        let is_active = match self.selected_entry() {
2221            Some(PanelEntry::Outline(OutlineEntry::Excerpt(selected_excerpt))) => {
2222                selected_excerpt == excerpt
2223            }
2224            _ => false,
2225        };
2226        let has_outlines = self
2227            .buffers
2228            .get(&excerpt.context.start.buffer_id)
2229            .and_then(|buffer| match &buffer.outlines {
2230                OutlineState::Outlines(outlines) => Some(outlines),
2231                OutlineState::Invalidated(outlines) => Some(outlines),
2232                OutlineState::NotFetched => None,
2233            })
2234            .is_some_and(|outlines| !outlines.is_empty());
2235        let is_expanded = !self
2236            .collapsed_entries
2237            .contains(&CollapsedEntry::Excerpt(excerpt.clone()));
2238        let color = entry_label_color(is_active);
2239        let icon = if has_outlines {
2240            FileIcons::get_chevron_icon(is_expanded, cx)
2241                .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2242        } else {
2243            None
2244        }
2245        .unwrap_or_else(empty_icon);
2246
2247        let label = self.excerpt_label(&excerpt, cx)?;
2248        let label_element = Label::new(label)
2249            .single_line()
2250            .color(color)
2251            .into_any_element();
2252
2253        Some(self.entry_element(
2254            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())),
2255            item_id,
2256            depth,
2257            icon,
2258            is_active,
2259            label_element,
2260            window,
2261            cx,
2262        ))
2263    }
2264
2265    fn excerpt_label(&self, range: &ExcerptRange<language::Anchor>, cx: &App) -> Option<String> {
2266        let buffer_snapshot = self.buffer_snapshot_for_id(range.context.start.buffer_id, cx)?;
2267        let excerpt_range = range.context.to_point(&buffer_snapshot);
2268        Some(format!(
2269            "Lines {}- {}",
2270            excerpt_range.start.row + 1,
2271            excerpt_range.end.row + 1,
2272        ))
2273    }
2274
2275    fn render_outline(
2276        &self,
2277        outline: &Outline,
2278        depth: usize,
2279        string_match: Option<&StringMatch>,
2280        window: &mut Window,
2281        cx: &mut Context<Self>,
2282    ) -> Stateful<Div> {
2283        let item_id = ElementId::from(SharedString::from(format!(
2284            "{:?}|{:?}",
2285            outline.range, &outline.text,
2286        )));
2287
2288        let label_element = outline::render_item(
2289            &outline,
2290            string_match
2291                .map(|string_match| string_match.ranges().collect::<Vec<_>>())
2292                .unwrap_or_default(),
2293            cx,
2294        )
2295        .into_any_element();
2296
2297        let is_active = match self.selected_entry() {
2298            Some(PanelEntry::Outline(OutlineEntry::Outline(selected))) => outline == selected,
2299            _ => false,
2300        };
2301
2302        let has_children = self
2303            .outline_children_cache
2304            .get(&outline.range.start.buffer_id)
2305            .and_then(|children_map| {
2306                let key = (outline.range.clone(), outline.depth);
2307                children_map.get(&key)
2308            })
2309            .copied()
2310            .unwrap_or(false);
2311        let is_expanded = !self
2312            .collapsed_entries
2313            .contains(&CollapsedEntry::Outline(outline.range.clone()));
2314
2315        let icon = if has_children {
2316            FileIcons::get_chevron_icon(is_expanded, cx)
2317                .map(|icon_path| {
2318                    Icon::from_path(icon_path)
2319                        .color(entry_label_color(is_active))
2320                        .into_any_element()
2321                })
2322                .unwrap_or_else(empty_icon)
2323        } else {
2324            empty_icon()
2325        };
2326
2327        self.entry_element(
2328            PanelEntry::Outline(OutlineEntry::Outline(outline.clone())),
2329            item_id,
2330            depth,
2331            icon,
2332            is_active,
2333            label_element,
2334            window,
2335            cx,
2336        )
2337    }
2338
2339    fn render_entry(
2340        &self,
2341        rendered_entry: &FsEntry,
2342        depth: usize,
2343        string_match: Option<&StringMatch>,
2344        window: &mut Window,
2345        cx: &mut Context<Self>,
2346    ) -> Stateful<Div> {
2347        let settings = OutlinePanelSettings::get_global(cx);
2348        let is_active = match self.selected_entry() {
2349            Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry,
2350            _ => false,
2351        };
2352        let (item_id, label_element, icon) = match rendered_entry {
2353            FsEntry::File(FsEntryFile {
2354                worktree_id, entry, ..
2355            }) => {
2356                let name = self.entry_name(worktree_id, entry, cx);
2357                let color =
2358                    entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
2359                let icon = if settings.file_icons {
2360                    FileIcons::get_icon(entry.path.as_std_path(), cx)
2361                        .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2362                } else {
2363                    None
2364                };
2365                (
2366                    ElementId::from(entry.id.to_proto() as usize),
2367                    HighlightedLabel::new(
2368                        name,
2369                        string_match
2370                            .map(|string_match| string_match.positions.clone())
2371                            .unwrap_or_default(),
2372                    )
2373                    .color(color)
2374                    .into_any_element(),
2375                    icon.unwrap_or_else(empty_icon),
2376                )
2377            }
2378            FsEntry::Directory(directory) => {
2379                let name = self.entry_name(&directory.worktree_id, &directory.entry, cx);
2380
2381                let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Dir(
2382                    directory.worktree_id,
2383                    directory.entry.id,
2384                ));
2385                let color = entry_git_aware_label_color(
2386                    directory.entry.git_summary,
2387                    directory.entry.is_ignored,
2388                    is_active,
2389                );
2390                let icon = if settings.folder_icons {
2391                    FileIcons::get_folder_icon(is_expanded, directory.entry.path.as_std_path(), cx)
2392                } else {
2393                    FileIcons::get_chevron_icon(is_expanded, cx)
2394                }
2395                .map(Icon::from_path)
2396                .map(|icon| icon.color(color).into_any_element());
2397                (
2398                    ElementId::from(directory.entry.id.to_proto() as usize),
2399                    HighlightedLabel::new(
2400                        name,
2401                        string_match
2402                            .map(|string_match| string_match.positions.clone())
2403                            .unwrap_or_default(),
2404                    )
2405                    .color(color)
2406                    .into_any_element(),
2407                    icon.unwrap_or_else(empty_icon),
2408                )
2409            }
2410            FsEntry::ExternalFile(external_file) => {
2411                let color = entry_label_color(is_active);
2412                let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) {
2413                    Some(buffer_snapshot) => match buffer_snapshot.file() {
2414                        Some(file) => {
2415                            let path = file.path();
2416                            let icon = if settings.file_icons {
2417                                FileIcons::get_icon(path.as_std_path(), cx)
2418                            } else {
2419                                None
2420                            }
2421                            .map(Icon::from_path)
2422                            .map(|icon| icon.color(color).into_any_element());
2423                            (icon, file_name(path.as_std_path()))
2424                        }
2425                        None => (None, "Untitled".to_string()),
2426                    },
2427                    None => (None, "Unknown buffer".to_string()),
2428                };
2429                (
2430                    ElementId::from(external_file.buffer_id.to_proto() as usize),
2431                    HighlightedLabel::new(
2432                        name,
2433                        string_match
2434                            .map(|string_match| string_match.positions.clone())
2435                            .unwrap_or_default(),
2436                    )
2437                    .color(color)
2438                    .into_any_element(),
2439                    icon.unwrap_or_else(empty_icon),
2440                )
2441            }
2442        };
2443
2444        self.entry_element(
2445            PanelEntry::Fs(rendered_entry.clone()),
2446            item_id,
2447            depth,
2448            icon,
2449            is_active,
2450            label_element,
2451            window,
2452            cx,
2453        )
2454    }
2455
2456    fn render_folded_dirs(
2457        &self,
2458        folded_dir: &FoldedDirsEntry,
2459        depth: usize,
2460        string_match: Option<&StringMatch>,
2461        window: &mut Window,
2462        cx: &mut Context<OutlinePanel>,
2463    ) -> Stateful<Div> {
2464        let settings = OutlinePanelSettings::get_global(cx);
2465        let is_active = match self.selected_entry() {
2466            Some(PanelEntry::FoldedDirs(selected_dirs)) => {
2467                selected_dirs.worktree_id == folded_dir.worktree_id
2468                    && selected_dirs.entries == folded_dir.entries
2469            }
2470            _ => false,
2471        };
2472        let (item_id, label_element, icon) = {
2473            let name = self.dir_names_string(&folded_dir.entries, folded_dir.worktree_id, cx);
2474
2475            let is_expanded = folded_dir.entries.iter().all(|dir| {
2476                !self
2477                    .collapsed_entries
2478                    .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id))
2479            });
2480            let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored);
2481            let git_status = folded_dir
2482                .entries
2483                .first()
2484                .map(|entry| entry.git_summary)
2485                .unwrap_or_default();
2486            let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
2487            let icon = if settings.folder_icons {
2488                FileIcons::get_folder_icon(is_expanded, &Path::new(&name), cx)
2489            } else {
2490                FileIcons::get_chevron_icon(is_expanded, cx)
2491            }
2492            .map(Icon::from_path)
2493            .map(|icon| icon.color(color).into_any_element());
2494            (
2495                ElementId::from(
2496                    folded_dir
2497                        .entries
2498                        .last()
2499                        .map(|entry| entry.id.to_proto())
2500                        .unwrap_or_else(|| folded_dir.worktree_id.to_proto())
2501                        as usize,
2502                ),
2503                HighlightedLabel::new(
2504                    name,
2505                    string_match
2506                        .map(|string_match| string_match.positions.clone())
2507                        .unwrap_or_default(),
2508                )
2509                .color(color)
2510                .into_any_element(),
2511                icon.unwrap_or_else(empty_icon),
2512            )
2513        };
2514
2515        self.entry_element(
2516            PanelEntry::FoldedDirs(folded_dir.clone()),
2517            item_id,
2518            depth,
2519            icon,
2520            is_active,
2521            label_element,
2522            window,
2523            cx,
2524        )
2525    }
2526
2527    fn render_search_match(
2528        &mut self,
2529        multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
2530        match_range: &Range<editor::Anchor>,
2531        render_data: &Arc<OnceLock<SearchData>>,
2532        kind: SearchKind,
2533        depth: usize,
2534        string_match: Option<&StringMatch>,
2535        window: &mut Window,
2536        cx: &mut Context<Self>,
2537    ) -> Option<Stateful<Div>> {
2538        let search_data = match render_data.get() {
2539            Some(search_data) => search_data,
2540            None => {
2541                if let ItemsDisplayMode::Search(search_state) = &mut self.mode
2542                    && let Some(multi_buffer_snapshot) = multi_buffer_snapshot
2543                {
2544                    search_state
2545                        .highlight_search_match_tx
2546                        .try_send(HighlightArguments {
2547                            multi_buffer_snapshot: multi_buffer_snapshot.clone(),
2548                            match_range: match_range.clone(),
2549                            search_data: Arc::clone(render_data),
2550                        })
2551                        .ok();
2552                }
2553                return None;
2554            }
2555        };
2556        let search_matches = string_match
2557            .iter()
2558            .flat_map(|string_match| string_match.ranges())
2559            .collect::<Vec<_>>();
2560        let match_ranges = if search_matches.is_empty() {
2561            &search_data.search_match_indices
2562        } else {
2563            &search_matches
2564        };
2565        let label_element = outline::render_item(
2566            &OutlineItem {
2567                depth,
2568                annotation_range: None,
2569                range: search_data.context_range.clone(),
2570                selection_range: search_data.context_range.clone(),
2571                text: search_data.context_text.clone().into(),
2572                source_range_for_text: search_data.context_range.clone(),
2573                highlight_ranges: search_data
2574                    .highlights_data
2575                    .get()
2576                    .cloned()
2577                    .unwrap_or_default(),
2578                name_ranges: search_data.search_match_indices.clone(),
2579                body_range: Some(search_data.context_range.clone()),
2580            },
2581            match_ranges.iter().cloned(),
2582            cx,
2583        );
2584        let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
2585        let entire_label = h_flex()
2586            .justify_center()
2587            .p_0()
2588            .when(search_data.truncated_left, |parent| {
2589                parent.child(truncated_contents_label())
2590            })
2591            .child(label_element)
2592            .when(search_data.truncated_right, |parent| {
2593                parent.child(truncated_contents_label())
2594            })
2595            .into_any_element();
2596
2597        let is_active = match self.selected_entry() {
2598            Some(PanelEntry::Search(SearchEntry {
2599                match_range: selected_match_range,
2600                ..
2601            })) => match_range == selected_match_range,
2602            _ => false,
2603        };
2604        Some(self.entry_element(
2605            PanelEntry::Search(SearchEntry {
2606                kind,
2607                match_range: match_range.clone(),
2608                render_data: render_data.clone(),
2609            }),
2610            ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
2611            depth,
2612            empty_icon(),
2613            is_active,
2614            entire_label,
2615            window,
2616            cx,
2617        ))
2618    }
2619
2620    fn entry_element(
2621        &self,
2622        rendered_entry: PanelEntry,
2623        item_id: ElementId,
2624        depth: usize,
2625        icon_element: AnyElement,
2626        is_active: bool,
2627        label_element: gpui::AnyElement,
2628        window: &mut Window,
2629        cx: &mut Context<OutlinePanel>,
2630    ) -> Stateful<Div> {
2631        let settings = OutlinePanelSettings::get_global(cx);
2632        div()
2633            .text_ui(cx)
2634            .id(item_id.clone())
2635            .on_click({
2636                let clicked_entry = rendered_entry.clone();
2637                cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| {
2638                    if event.is_right_click() || event.first_focus() {
2639                        return;
2640                    }
2641
2642                    let change_focus = event.click_count() > 1;
2643                    outline_panel.toggle_expanded(&clicked_entry, window, cx);
2644
2645                    outline_panel.scroll_editor_to_entry(
2646                        &clicked_entry,
2647                        true,
2648                        change_focus,
2649                        window,
2650                        cx,
2651                    );
2652                })
2653            })
2654            .cursor_pointer()
2655            .child(
2656                ListItem::new(item_id)
2657                    .indent_level(depth)
2658                    .indent_step_size(px(settings.indent_size))
2659                    .toggle_state(is_active)
2660                    .child(
2661                        h_flex()
2662                            .child(h_flex().w(px(16.)).justify_center().child(icon_element))
2663                            .child(h_flex().h_6().child(label_element).ml_1()),
2664                    )
2665                    .on_secondary_mouse_down(cx.listener(
2666                        move |outline_panel, event: &MouseDownEvent, window, cx| {
2667                            // Stop propagation to prevent the catch-all context menu for the project
2668                            // panel from being deployed.
2669                            cx.stop_propagation();
2670                            outline_panel.deploy_context_menu(
2671                                event.position,
2672                                rendered_entry.clone(),
2673                                window,
2674                                cx,
2675                            )
2676                        },
2677                    )),
2678            )
2679            .border_1()
2680            .border_r_2()
2681            .rounded_none()
2682            .hover(|style| {
2683                if is_active {
2684                    style
2685                } else {
2686                    let hover_color = cx.theme().colors().ghost_element_hover;
2687                    style.bg(hover_color).border_color(hover_color)
2688                }
2689            })
2690            .when(
2691                is_active && self.focus_handle.contains_focused(window, cx),
2692                |div| div.border_color(cx.theme().colors().panel_focused_border),
2693            )
2694    }
2695
2696    fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String {
2697        match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2698            Some(worktree) => {
2699                let worktree = worktree.read(cx);
2700                match worktree.snapshot().root_entry() {
2701                    Some(root_entry) => {
2702                        if root_entry.id == entry.id {
2703                            file_name(worktree.abs_path().as_ref())
2704                        } else {
2705                            let path = worktree.absolutize(entry.path.as_ref());
2706                            file_name(&path)
2707                        }
2708                    }
2709                    None => {
2710                        let path = worktree.absolutize(entry.path.as_ref());
2711                        file_name(&path)
2712                    }
2713                }
2714            }
2715            None => file_name(entry.path.as_std_path()),
2716        }
2717    }
2718
2719    fn update_fs_entries(
2720        &mut self,
2721        active_editor: Entity<Editor>,
2722        debounce: Option<Duration>,
2723        window: &mut Window,
2724        cx: &mut Context<Self>,
2725    ) {
2726        if !self.active {
2727            return;
2728        }
2729
2730        if debounce.is_some() && self.fs_entries_update_pending {
2731            return;
2732        }
2733        self.fs_entries_update_pending = true;
2734
2735        self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
2736            if let Some(debounce) = debounce {
2737                cx.background_executor().timer(debounce).await;
2738            }
2739
2740            let mut new_collapsed_entries = HashSet::default();
2741            let mut new_unfolded_dirs = HashMap::default();
2742            let mut root_entries = HashSet::default();
2743            let mut new_buffers = HashMap::<BufferId, BufferOutlines>::default();
2744            let Ok((buffer_excerpts, auto_fold_dirs, repo_snapshots)) =
2745                outline_panel.update(cx, |outline_panel, cx| {
2746                    outline_panel.fs_entries_update_pending = false;
2747                    let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2748                    let active_multi_buffer = active_editor.read(cx).buffer().clone();
2749                    let new_entries = outline_panel.new_entries_for_fs_update.clone();
2750                    let repo_snapshots = outline_panel.project.update(cx, |project, cx| {
2751                        project.git_store().read(cx).repo_snapshots(cx)
2752                    });
2753                    let git_store = outline_panel.project.read(cx).git_store().clone();
2754                    new_collapsed_entries = outline_panel.collapsed_entries.clone();
2755                    new_unfolded_dirs = outline_panel.unfolded_dirs.clone();
2756                    let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
2757
2758                    let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
2759                        HashMap::default(),
2760                        |mut buffer_excerpts, excerpt_range| {
2761                            let Some(buffer_snapshot) = multi_buffer_snapshot
2762                                .buffer_for_id(excerpt_range.context.start.buffer_id)
2763                            else {
2764                                return buffer_excerpts;
2765                            };
2766                            let buffer_id = buffer_snapshot.remote_id();
2767                            let file = File::from_dyn(buffer_snapshot.file());
2768                            let entry_id = file.and_then(|file| file.project_entry_id());
2769                            let worktree = file.map(|file| file.worktree.read(cx).snapshot());
2770                            let is_new = new_entries.contains(&buffer_id)
2771                                || !outline_panel.buffers.contains_key(&buffer_id);
2772                            let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
2773                            let status = git_store
2774                                .read(cx)
2775                                .repository_and_path_for_buffer_id(buffer_id, cx)
2776                                .and_then(|(repo, path)| {
2777                                    Some(repo.read(cx).status_for_path(&path)?.status)
2778                                });
2779                            buffer_excerpts
2780                                .entry(buffer_id)
2781                                .or_insert_with(|| {
2782                                    (is_new, is_folded, Vec::new(), entry_id, worktree, status)
2783                                })
2784                                .2
2785                                .push(excerpt_range.clone());
2786
2787                            new_buffers
2788                                .entry(buffer_id)
2789                                .or_insert_with(|| {
2790                                    let outlines = match outline_panel.buffers.get(&buffer_id) {
2791                                        Some(old_buffer) => match &old_buffer.outlines {
2792                                            OutlineState::Outlines(outlines) => {
2793                                                OutlineState::Outlines(outlines.clone())
2794                                            }
2795                                            OutlineState::Invalidated(_) => {
2796                                                OutlineState::NotFetched
2797                                            }
2798                                            OutlineState::NotFetched => OutlineState::NotFetched,
2799                                        },
2800                                        None => OutlineState::NotFetched,
2801                                    };
2802                                    BufferOutlines {
2803                                        outlines,
2804                                        excerpts: Vec::new(),
2805                                    }
2806                                })
2807                                .excerpts
2808                                .push(excerpt_range);
2809                            buffer_excerpts
2810                        },
2811                    );
2812                    (buffer_excerpts, auto_fold_dirs, repo_snapshots)
2813                })
2814            else {
2815                return;
2816            };
2817
2818            let Some((
2819                new_collapsed_entries,
2820                new_unfolded_dirs,
2821                new_fs_entries,
2822                new_depth_map,
2823                new_children_count,
2824            )) = cx
2825                .background_spawn(async move {
2826                    let mut processed_external_buffers = HashSet::default();
2827                    let mut new_worktree_entries =
2828                        BTreeMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
2829                    let mut worktree_excerpts = HashMap::<
2830                        WorktreeId,
2831                        HashMap<ProjectEntryId, (BufferId, Vec<ExcerptRange<Anchor>>)>,
2832                    >::default();
2833                    let mut external_excerpts = HashMap::default();
2834
2835                    for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree, status)) in
2836                        buffer_excerpts
2837                    {
2838                        if is_folded {
2839                            match &worktree {
2840                                Some(worktree) => {
2841                                    new_collapsed_entries
2842                                        .insert(CollapsedEntry::File(worktree.id(), buffer_id));
2843                                }
2844                                None => {
2845                                    new_collapsed_entries
2846                                        .insert(CollapsedEntry::ExternalFile(buffer_id));
2847                                }
2848                            }
2849                        } else if is_new {
2850                            match &worktree {
2851                                Some(worktree) => {
2852                                    new_collapsed_entries
2853                                        .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
2854                                }
2855                                None => {
2856                                    new_collapsed_entries
2857                                        .remove(&CollapsedEntry::ExternalFile(buffer_id));
2858                                }
2859                            }
2860                        }
2861
2862                        if let Some(worktree) = worktree {
2863                            let worktree_id = worktree.id();
2864                            let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
2865
2866                            match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
2867                                Some(entry) => {
2868                                    let entry = GitEntry {
2869                                        git_summary: status
2870                                            .map(|status| status.summary())
2871                                            .unwrap_or_default(),
2872                                        entry,
2873                                    };
2874                                    let mut traversal = GitTraversal::new(
2875                                        &repo_snapshots,
2876                                        worktree.traverse_from_path(
2877                                            true,
2878                                            true,
2879                                            true,
2880                                            entry.path.as_ref(),
2881                                        ),
2882                                    );
2883
2884                                    let mut entries_to_add = HashMap::default();
2885                                    worktree_excerpts
2886                                        .entry(worktree_id)
2887                                        .or_default()
2888                                        .insert(entry.id, (buffer_id, excerpts));
2889                                    let mut current_entry = entry;
2890                                    loop {
2891                                        if current_entry.is_dir() {
2892                                            let is_root =
2893                                                worktree.root_entry().map(|entry| entry.id)
2894                                                    == Some(current_entry.id);
2895                                            if is_root {
2896                                                root_entries.insert(current_entry.id);
2897                                                if auto_fold_dirs {
2898                                                    unfolded_dirs.insert(current_entry.id);
2899                                                }
2900                                            }
2901                                            if is_new {
2902                                                new_collapsed_entries.remove(&CollapsedEntry::Dir(
2903                                                    worktree_id,
2904                                                    current_entry.id,
2905                                                ));
2906                                            }
2907                                        }
2908
2909                                        let new_entry_added = entries_to_add
2910                                            .insert(current_entry.id, current_entry)
2911                                            .is_none();
2912                                        if new_entry_added
2913                                            && traversal.back_to_parent()
2914                                            && let Some(parent_entry) = traversal.entry()
2915                                        {
2916                                            current_entry = parent_entry.to_owned();
2917                                            continue;
2918                                        }
2919                                        break;
2920                                    }
2921                                    new_worktree_entries
2922                                        .entry(worktree_id)
2923                                        .or_insert_with(HashMap::default)
2924                                        .extend(entries_to_add);
2925                                }
2926                                None => {
2927                                    if processed_external_buffers.insert(buffer_id) {
2928                                        external_excerpts
2929                                            .entry(buffer_id)
2930                                            .or_insert_with(Vec::new)
2931                                            .extend(excerpts);
2932                                    }
2933                                }
2934                            }
2935                        } else if processed_external_buffers.insert(buffer_id) {
2936                            external_excerpts
2937                                .entry(buffer_id)
2938                                .or_insert_with(Vec::new)
2939                                .extend(excerpts);
2940                        }
2941                    }
2942
2943                    let mut new_children_count =
2944                        HashMap::<WorktreeId, HashMap<Arc<RelPath>, FsChildren>>::default();
2945
2946                    let worktree_entries = new_worktree_entries
2947                        .into_iter()
2948                        .map(|(worktree_id, entries)| {
2949                            let mut entries = entries.into_values().collect::<Vec<_>>();
2950                            entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
2951                            (worktree_id, entries)
2952                        })
2953                        .flat_map(|(worktree_id, entries)| {
2954                            {
2955                                entries
2956                                    .into_iter()
2957                                    .filter_map(|entry| {
2958                                        if auto_fold_dirs && let Some(parent) = entry.path.parent()
2959                                        {
2960                                            let children = new_children_count
2961                                                .entry(worktree_id)
2962                                                .or_default()
2963                                                .entry(Arc::from(parent))
2964                                                .or_default();
2965                                            if entry.is_dir() {
2966                                                children.dirs += 1;
2967                                            } else {
2968                                                children.files += 1;
2969                                            }
2970                                        }
2971
2972                                        if entry.is_dir() {
2973                                            Some(FsEntry::Directory(FsEntryDirectory {
2974                                                worktree_id,
2975                                                entry,
2976                                            }))
2977                                        } else {
2978                                            let (buffer_id, excerpts) = worktree_excerpts
2979                                                .get_mut(&worktree_id)
2980                                                .and_then(|worktree_excerpts| {
2981                                                    worktree_excerpts.remove(&entry.id)
2982                                                })?;
2983                                            Some(FsEntry::File(FsEntryFile {
2984                                                worktree_id,
2985                                                buffer_id,
2986                                                entry,
2987                                                excerpts,
2988                                            }))
2989                                        }
2990                                    })
2991                                    .collect::<Vec<_>>()
2992                            }
2993                        })
2994                        .collect::<Vec<_>>();
2995
2996                    let mut visited_dirs = Vec::new();
2997                    let mut new_depth_map = HashMap::default();
2998                    let new_visible_entries = external_excerpts
2999                        .into_iter()
3000                        .sorted_by_key(|(id, _)| *id)
3001                        .map(|(buffer_id, excerpts)| {
3002                            FsEntry::ExternalFile(FsEntryExternalFile {
3003                                buffer_id,
3004                                excerpts,
3005                            })
3006                        })
3007                        .chain(worktree_entries)
3008                        .filter(|visible_item| {
3009                            match visible_item {
3010                                FsEntry::Directory(directory) => {
3011                                    let parent_id = back_to_common_visited_parent(
3012                                        &mut visited_dirs,
3013                                        &directory.worktree_id,
3014                                        &directory.entry,
3015                                    );
3016
3017                                    let mut depth = 0;
3018                                    if !root_entries.contains(&directory.entry.id) {
3019                                        if auto_fold_dirs {
3020                                            let children = new_children_count
3021                                                .get(&directory.worktree_id)
3022                                                .and_then(|children_count| {
3023                                                    children_count.get(&directory.entry.path)
3024                                                })
3025                                                .copied()
3026                                                .unwrap_or_default();
3027
3028                                            if !children.may_be_fold_part()
3029                                                || (children.dirs == 0
3030                                                    && visited_dirs
3031                                                        .last()
3032                                                        .map(|(parent_dir_id, _)| {
3033                                                            new_unfolded_dirs
3034                                                                .get(&directory.worktree_id)
3035                                                                .is_none_or(|unfolded_dirs| {
3036                                                                    unfolded_dirs
3037                                                                        .contains(parent_dir_id)
3038                                                                })
3039                                                        })
3040                                                        .unwrap_or(true))
3041                                            {
3042                                                new_unfolded_dirs
3043                                                    .entry(directory.worktree_id)
3044                                                    .or_default()
3045                                                    .insert(directory.entry.id);
3046                                            }
3047                                        }
3048
3049                                        depth = parent_id
3050                                            .and_then(|(worktree_id, id)| {
3051                                                new_depth_map.get(&(worktree_id, id)).copied()
3052                                            })
3053                                            .unwrap_or(0)
3054                                            + 1;
3055                                    };
3056                                    visited_dirs
3057                                        .push((directory.entry.id, directory.entry.path.clone()));
3058                                    new_depth_map
3059                                        .insert((directory.worktree_id, directory.entry.id), depth);
3060                                }
3061                                FsEntry::File(FsEntryFile {
3062                                    worktree_id,
3063                                    entry: file_entry,
3064                                    ..
3065                                }) => {
3066                                    let parent_id = back_to_common_visited_parent(
3067                                        &mut visited_dirs,
3068                                        worktree_id,
3069                                        file_entry,
3070                                    );
3071                                    let depth = if root_entries.contains(&file_entry.id) {
3072                                        0
3073                                    } else {
3074                                        parent_id
3075                                            .and_then(|(worktree_id, id)| {
3076                                                new_depth_map.get(&(worktree_id, id)).copied()
3077                                            })
3078                                            .unwrap_or(0)
3079                                            + 1
3080                                    };
3081                                    new_depth_map.insert((*worktree_id, file_entry.id), depth);
3082                                }
3083                                FsEntry::ExternalFile(..) => {
3084                                    visited_dirs.clear();
3085                                }
3086                            }
3087
3088                            true
3089                        })
3090                        .collect::<Vec<_>>();
3091
3092                    anyhow::Ok((
3093                        new_collapsed_entries,
3094                        new_unfolded_dirs,
3095                        new_visible_entries,
3096                        new_depth_map,
3097                        new_children_count,
3098                    ))
3099                })
3100                .await
3101                .log_err()
3102            else {
3103                return;
3104            };
3105
3106            outline_panel
3107                .update_in(cx, |outline_panel, window, cx| {
3108                    outline_panel.new_entries_for_fs_update.clear();
3109                    outline_panel.buffers = new_buffers;
3110                    outline_panel.collapsed_entries = new_collapsed_entries;
3111                    outline_panel.unfolded_dirs = new_unfolded_dirs;
3112                    outline_panel.fs_entries = new_fs_entries;
3113                    outline_panel.fs_entries_depth = new_depth_map;
3114                    outline_panel.fs_children_count = new_children_count;
3115                    outline_panel.update_non_fs_items(window, cx);
3116
3117                    // Only update cached entries if we don't have outlines to fetch
3118                    // If we do have outlines to fetch, let fetch_outdated_outlines handle the update
3119                    if outline_panel.buffers_to_fetch().is_empty() {
3120                        outline_panel.update_cached_entries(debounce, window, cx);
3121                    }
3122
3123                    cx.notify();
3124                })
3125                .ok();
3126        });
3127    }
3128
3129    fn replace_active_editor(
3130        &mut self,
3131        new_active_item: Box<dyn ItemHandle>,
3132        new_active_editor: Entity<Editor>,
3133        window: &mut Window,
3134        cx: &mut Context<Self>,
3135    ) {
3136        self.clear_previous(window, cx);
3137
3138        let default_expansion_depth =
3139            OutlinePanelSettings::get_global(cx).expand_outlines_with_depth;
3140        // We'll apply the expansion depth after outlines are loaded
3141        self.pending_default_expansion_depth = Some(default_expansion_depth);
3142
3143        let buffer_search_subscription = cx.subscribe_in(
3144            &new_active_editor,
3145            window,
3146            |outline_panel: &mut Self,
3147             _,
3148             e: &SearchEvent,
3149             window: &mut Window,
3150             cx: &mut Context<Self>| {
3151                if matches!(e, SearchEvent::MatchesInvalidated)
3152                    && outline_panel.update_search_matches(window, cx)
3153                {
3154                    outline_panel.selected_entry.invalidate();
3155                    outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
3156                }
3157            },
3158        );
3159        self.active_item = Some(ActiveItem {
3160            _buffer_search_subscription: buffer_search_subscription,
3161            _editor_subscription: subscribe_for_editor_events(&new_active_editor, window, cx),
3162            item_handle: new_active_item.downgrade_item(),
3163            active_editor: new_active_editor.downgrade(),
3164        });
3165        self.new_entries_for_fs_update.extend(
3166            new_active_editor
3167                .read(cx)
3168                .buffer()
3169                .read(cx)
3170                .snapshot(cx)
3171                .excerpts()
3172                .map(|excerpt| excerpt.context.start.buffer_id),
3173        );
3174        self.selected_entry.invalidate();
3175        self.update_fs_entries(new_active_editor, None, window, cx);
3176    }
3177
3178    fn clear_previous(&mut self, window: &mut Window, cx: &mut App) {
3179        self.fs_entries_update_task = Task::ready(());
3180        self.fs_entries_update_pending = false;
3181        self.outline_fetch_tasks.clear();
3182        self.cached_entries_update_task = Task::ready(());
3183        self.cached_entries_update_pending = false;
3184        self.reveal_selection_task = Task::ready(Ok(()));
3185        self.filter_editor
3186            .update(cx, |editor, cx| editor.clear(window, cx));
3187        self.collapsed_entries.clear();
3188        self.unfolded_dirs.clear();
3189        self.active_item = None;
3190        self.fs_entries.clear();
3191        self.fs_entries_depth.clear();
3192        self.fs_children_count.clear();
3193        self.buffers.clear();
3194        self.cached_entries = Vec::new();
3195        self.selected_entry = SelectedEntry::None;
3196        self.pinned = false;
3197        self.mode = ItemsDisplayMode::Outline;
3198        self.pending_default_expansion_depth = None;
3199    }
3200
3201    fn location_for_editor_selection(
3202        &self,
3203        editor: &Entity<Editor>,
3204        window: &mut Window,
3205        cx: &mut Context<Self>,
3206    ) -> Option<PanelEntry> {
3207        let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
3208        let multi_buffer = editor.read(cx).buffer();
3209        let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
3210        let anchor = editor.update(cx, |editor, _| editor.selections.newest_anchor().head());
3211        let selection_display_point = anchor.to_display_point(&editor_snapshot);
3212        let (anchor, _) = multi_buffer_snapshot.anchor_to_buffer_anchor(anchor)?;
3213
3214        if editor.read(cx).is_buffer_folded(anchor.buffer_id, cx) {
3215            return self
3216                .fs_entries
3217                .iter()
3218                .find(|fs_entry| match fs_entry {
3219                    FsEntry::Directory(..) => false,
3220                    FsEntry::File(FsEntryFile {
3221                        buffer_id: other_buffer_id,
3222                        ..
3223                    })
3224                    | FsEntry::ExternalFile(FsEntryExternalFile {
3225                        buffer_id: other_buffer_id,
3226                        ..
3227                    }) => anchor.buffer_id == *other_buffer_id,
3228                })
3229                .cloned()
3230                .map(PanelEntry::Fs);
3231        }
3232
3233        match &self.mode {
3234            ItemsDisplayMode::Search(search_state) => search_state
3235                .matches
3236                .iter()
3237                .rev()
3238                .min_by_key(|&(match_range, _)| {
3239                    let match_display_range =
3240                        match_range.clone().to_display_points(&editor_snapshot);
3241                    let start_distance = if selection_display_point < match_display_range.start {
3242                        match_display_range.start - selection_display_point
3243                    } else {
3244                        selection_display_point - match_display_range.start
3245                    };
3246                    let end_distance = if selection_display_point < match_display_range.end {
3247                        match_display_range.end - selection_display_point
3248                    } else {
3249                        selection_display_point - match_display_range.end
3250                    };
3251                    start_distance + end_distance
3252                })
3253                .and_then(|(closest_range, _)| {
3254                    self.cached_entries.iter().find_map(|cached_entry| {
3255                        if let PanelEntry::Search(SearchEntry { match_range, .. }) =
3256                            &cached_entry.entry
3257                        {
3258                            if match_range == closest_range {
3259                                Some(cached_entry.entry.clone())
3260                            } else {
3261                                None
3262                            }
3263                        } else {
3264                            None
3265                        }
3266                    })
3267                }),
3268            ItemsDisplayMode::Outline => self.outline_location(
3269                anchor,
3270                multi_buffer_snapshot,
3271                editor_snapshot,
3272                selection_display_point,
3273                cx,
3274            ),
3275        }
3276    }
3277
3278    fn outline_location(
3279        &self,
3280        selection_anchor: Anchor,
3281        multi_buffer_snapshot: editor::MultiBufferSnapshot,
3282        editor_snapshot: editor::EditorSnapshot,
3283        selection_display_point: DisplayPoint,
3284        cx: &App,
3285    ) -> Option<PanelEntry> {
3286        let excerpt_outlines = self
3287            .buffers
3288            .get(&selection_anchor.buffer_id)
3289            .into_iter()
3290            .flat_map(|buffer| buffer.iter_outlines())
3291            .flat_map(|outline| {
3292                let range = multi_buffer_snapshot
3293                    .buffer_anchor_range_to_anchor_range(outline.range.clone())?;
3294                Some((
3295                    range.start.to_display_point(&editor_snapshot)
3296                        ..range.end.to_display_point(&editor_snapshot),
3297                    outline,
3298                ))
3299            })
3300            .collect::<Vec<_>>();
3301
3302        let mut matching_outline_indices = Vec::new();
3303        let mut children = HashMap::default();
3304        let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
3305
3306        for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
3307            if outline_range
3308                .to_inclusive()
3309                .contains(&selection_display_point)
3310            {
3311                matching_outline_indices.push(i);
3312            } else if (outline_range.start.row()..outline_range.end.row())
3313                .to_inclusive()
3314                .contains(&selection_display_point.row())
3315            {
3316                matching_outline_indices.push(i);
3317            }
3318
3319            while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
3320                if parent_outline.depth >= outline.depth
3321                    || !parent_range.contains(&outline_range.start)
3322                {
3323                    parents_stack.pop();
3324                } else {
3325                    break;
3326                }
3327            }
3328            if let Some((_, _, parent_index)) = parents_stack.last_mut() {
3329                children
3330                    .entry(*parent_index)
3331                    .or_insert_with(Vec::new)
3332                    .push(i);
3333            }
3334            parents_stack.push((outline_range, outline, i));
3335        }
3336
3337        let outline_item = matching_outline_indices
3338            .into_iter()
3339            .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
3340            .filter(|(i, _)| {
3341                children
3342                    .get(i)
3343                    .map(|children| {
3344                        children.iter().all(|child_index| {
3345                            excerpt_outlines
3346                                .get(*child_index)
3347                                .map(|(child_range, _)| child_range.start > selection_display_point)
3348                                .unwrap_or(false)
3349                        })
3350                    })
3351                    .unwrap_or(true)
3352            })
3353            .min_by_key(|(_, (outline_range, outline))| {
3354                let distance_from_start = if outline_range.start > selection_display_point {
3355                    outline_range.start - selection_display_point
3356                } else {
3357                    selection_display_point - outline_range.start
3358                };
3359                let distance_from_end = if outline_range.end > selection_display_point {
3360                    outline_range.end - selection_display_point
3361                } else {
3362                    selection_display_point - outline_range.end
3363                };
3364
3365                // An outline item's range can extend to the same row the next
3366                // item starts on, so when the cursor is at the start of that
3367                // row, prefer the item that starts there over any item whose
3368                // range merely overlaps that row.
3369                let cursor_not_at_outline_start = outline_range.start != selection_display_point;
3370                (
3371                    cursor_not_at_outline_start,
3372                    cmp::Reverse(outline.depth),
3373                    distance_from_start,
3374                    distance_from_end,
3375                )
3376            })
3377            .map(|(_, (_, outline))| *outline)
3378            .cloned();
3379
3380        let closest_container = match outline_item {
3381            Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(outline)),
3382            None => {
3383                self.cached_entries.iter().rev().find_map(|cached_entry| {
3384                    match &cached_entry.entry {
3385                        PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
3386                            if excerpt.context.start.buffer_id == selection_anchor.buffer_id
3387                                && let Some(buffer_snapshot) =
3388                                    self.buffer_snapshot_for_id(excerpt.context.start.buffer_id, cx)
3389                                && excerpt.contains(&selection_anchor, &buffer_snapshot)
3390                            {
3391                                Some(cached_entry.entry.clone())
3392                            } else {
3393                                None
3394                            }
3395                        }
3396                        PanelEntry::Fs(
3397                            FsEntry::ExternalFile(FsEntryExternalFile {
3398                                buffer_id: file_buffer_id,
3399                                excerpts: file_excerpts,
3400                                ..
3401                            })
3402                            | FsEntry::File(FsEntryFile {
3403                                buffer_id: file_buffer_id,
3404                                excerpts: file_excerpts,
3405                                ..
3406                            }),
3407                        ) => {
3408                            if *file_buffer_id == selection_anchor.buffer_id
3409                                && let Some(buffer_snapshot) =
3410                                    self.buffer_snapshot_for_id(*file_buffer_id, cx)
3411                                && file_excerpts.iter().any(|excerpt| {
3412                                    excerpt.contains(&selection_anchor, &buffer_snapshot)
3413                                })
3414                            {
3415                                Some(cached_entry.entry.clone())
3416                            } else {
3417                                None
3418                            }
3419                        }
3420                        _ => None,
3421                    }
3422                })?
3423            }
3424        };
3425        Some(closest_container)
3426    }
3427
3428    fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3429        let buffers_to_fetch = self.buffers_to_fetch();
3430        if buffers_to_fetch.is_empty() {
3431            return;
3432        }
3433
3434        let first_update = Arc::new(AtomicBool::new(true));
3435        for buffer_id in buffers_to_fetch {
3436            let outline_task = self.active_editor().map(|editor| {
3437                editor.update(cx, |editor, cx| editor.buffer_outline_items(buffer_id, cx))
3438            });
3439
3440            let first_update = first_update.clone();
3441
3442            self.outline_fetch_tasks.insert(
3443                buffer_id,
3444                cx.spawn_in(window, async move |outline_panel, cx| {
3445                    let Some(outline_task) = outline_task else {
3446                        return;
3447                    };
3448                    let fetched_outlines = outline_task.await;
3449                    let outlines_with_children = fetched_outlines
3450                        .array_windows::<2>()
3451                        .filter_map(|[current, next]| {
3452                            if next.depth > current.depth {
3453                                Some((current.range.clone(), current.depth))
3454                            } else {
3455                                None
3456                            }
3457                        })
3458                        .collect::<HashSet<_>>();
3459
3460                    outline_panel
3461                        .update_in(cx, |outline_panel, window, cx| {
3462                            let pending_default_depth =
3463                                outline_panel.pending_default_expansion_depth.take();
3464
3465                            let debounce =
3466                                if first_update.fetch_and(false, atomic::Ordering::AcqRel) {
3467                                    None
3468                                } else {
3469                                    Some(UPDATE_DEBOUNCE)
3470                                };
3471
3472                            if let Some(buffer) = outline_panel.buffers.get_mut(&buffer_id) {
3473                                buffer.outlines = OutlineState::Outlines(fetched_outlines.clone());
3474
3475                                if let Some(default_depth) = pending_default_depth
3476                                    && let OutlineState::Outlines(outlines) = &buffer.outlines
3477                                {
3478                                    outlines
3479                                        .iter()
3480                                        .filter(|outline| {
3481                                            (default_depth == 0 || outline.depth >= default_depth)
3482                                                && outlines_with_children.contains(&(
3483                                                    outline.range.clone(),
3484                                                    outline.depth,
3485                                                ))
3486                                        })
3487                                        .for_each(|outline| {
3488                                            outline_panel.collapsed_entries.insert(
3489                                                CollapsedEntry::Outline(outline.range.clone()),
3490                                            );
3491                                        });
3492                                }
3493                            }
3494
3495                            outline_panel.update_cached_entries(debounce, window, cx);
3496                        })
3497                        .ok();
3498                }),
3499            );
3500        }
3501    }
3502
3503    fn is_singleton_active(&self, cx: &App) -> bool {
3504        self.active_editor()
3505            .is_some_and(|active_editor| active_editor.read(cx).buffer().read(cx).is_singleton())
3506    }
3507
3508    fn invalidate_outlines(&mut self, ids: &[BufferId]) {
3509        self.outline_fetch_tasks.clear();
3510        let mut ids = ids.iter().collect::<HashSet<_>>();
3511        for (buffer_id, buffer) in self.buffers.iter_mut() {
3512            if ids.remove(&buffer_id) {
3513                buffer.invalidate_outlines();
3514            }
3515            if ids.is_empty() {
3516                break;
3517            }
3518        }
3519    }
3520
3521    fn buffers_to_fetch(&self) -> HashSet<BufferId> {
3522        self.fs_entries
3523            .iter()
3524            .fold(HashSet::default(), |mut buffers_to_fetch, fs_entry| {
3525                match fs_entry {
3526                    FsEntry::File(FsEntryFile { buffer_id, .. })
3527                    | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3528                        if let Some(buffer) = self.buffers.get(buffer_id)
3529                            && buffer.should_fetch_outlines()
3530                        {
3531                            buffers_to_fetch.insert(*buffer_id);
3532                        }
3533                    }
3534                    FsEntry::Directory(..) => {}
3535                }
3536                buffers_to_fetch
3537            })
3538    }
3539
3540    fn buffer_snapshot_for_id(&self, buffer_id: BufferId, cx: &App) -> Option<BufferSnapshot> {
3541        let editor = self.active_editor()?;
3542        Some(
3543            editor
3544                .read(cx)
3545                .buffer()
3546                .read(cx)
3547                .buffer(buffer_id)?
3548                .read(cx)
3549                .snapshot(),
3550        )
3551    }
3552
3553    fn abs_path(&self, entry: &PanelEntry, cx: &App) -> Option<PathBuf> {
3554        match entry {
3555            PanelEntry::Fs(
3556                FsEntry::File(FsEntryFile { buffer_id, .. })
3557                | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }),
3558            ) => self
3559                .buffer_snapshot_for_id(*buffer_id, cx)
3560                .and_then(|buffer_snapshot| {
3561                    let file = File::from_dyn(buffer_snapshot.file())?;
3562                    Some(file.worktree.read(cx).absolutize(&file.path))
3563                }),
3564            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3565                worktree_id, entry, ..
3566            })) => Some(
3567                self.project
3568                    .read(cx)
3569                    .worktree_for_id(*worktree_id, cx)?
3570                    .read(cx)
3571                    .absolutize(&entry.path),
3572            ),
3573            PanelEntry::FoldedDirs(FoldedDirsEntry {
3574                worktree_id,
3575                entries: dirs,
3576                ..
3577            }) => dirs.last().and_then(|entry| {
3578                self.project
3579                    .read(cx)
3580                    .worktree_for_id(*worktree_id, cx)
3581                    .map(|worktree| worktree.read(cx).absolutize(&entry.path))
3582            }),
3583            PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
3584        }
3585    }
3586
3587    fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<RelPath>> {
3588        match entry {
3589            FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3590                let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
3591                Some(buffer_snapshot.file()?.path().clone())
3592            }
3593            FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()),
3594            FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()),
3595        }
3596    }
3597
3598    fn update_cached_entries(
3599        &mut self,
3600        debounce: Option<Duration>,
3601        window: &mut Window,
3602        cx: &mut Context<OutlinePanel>,
3603    ) {
3604        if !self.active {
3605            return;
3606        }
3607
3608        // A pending debounced update will read the latest state when it fires,
3609        // so we don't need to reschedule. Constantly rescheduling under a steady stream
3610        // of events (e.g. project search streaming results) would starve the task forever.
3611        if debounce.is_some() && self.cached_entries_update_pending {
3612            return;
3613        }
3614        self.cached_entries_update_pending = true;
3615
3616        self.cached_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
3617            if let Some(debounce) = debounce {
3618                cx.background_executor().timer(debounce).await;
3619            }
3620            let Some(new_cached_entries) = outline_panel
3621                .update_in(cx, |outline_panel, window, cx| {
3622                    outline_panel.cached_entries_update_pending = false;
3623                    let is_singleton = outline_panel.is_singleton_active(cx);
3624                    let query = outline_panel.query(cx);
3625                    outline_panel.generate_cached_entries(is_singleton, query, window, cx)
3626                })
3627                .ok()
3628            else {
3629                return;
3630            };
3631            let (new_cached_entries, max_width_item_index) = new_cached_entries.await;
3632            outline_panel
3633                .update_in(cx, |outline_panel, window, cx| {
3634                    outline_panel.cached_entries = new_cached_entries;
3635                    outline_panel.max_width_item_index = max_width_item_index;
3636                    if (outline_panel.selected_entry.is_invalidated()
3637                        || matches!(outline_panel.selected_entry, SelectedEntry::None))
3638                        && let Some(new_selected_entry) =
3639                            outline_panel.active_editor().and_then(|active_editor| {
3640                                outline_panel.location_for_editor_selection(
3641                                    &active_editor,
3642                                    window,
3643                                    cx,
3644                                )
3645                            })
3646                    {
3647                        outline_panel.select_entry(new_selected_entry, false, window, cx);
3648                    }
3649
3650                    cx.notify();
3651                })
3652                .ok();
3653        });
3654    }
3655
3656    fn generate_cached_entries(
3657        &self,
3658        is_singleton: bool,
3659        query: Option<String>,
3660        window: &mut Window,
3661        cx: &mut Context<Self>,
3662    ) -> Task<(Vec<CachedEntry>, Option<usize>)> {
3663        let project = self.project.clone();
3664        let Some(active_editor) = self.active_editor() else {
3665            return Task::ready((Vec::new(), None));
3666        };
3667        cx.spawn_in(window, async move |outline_panel, cx| {
3668            let mut generation_state = GenerationState::default();
3669
3670            let Ok(()) = outline_panel.update(cx, |outline_panel, cx| {
3671                let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
3672                let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>;
3673                let track_matches = query.is_some();
3674
3675                #[derive(Debug)]
3676                struct ParentStats {
3677                    path: Arc<RelPath>,
3678                    folded: bool,
3679                    expanded: bool,
3680                    depth: usize,
3681                }
3682
3683                let search_precomputed =
3684                    if let ItemsDisplayMode::Search(search_state) = &outline_panel.mode {
3685                        let multi_buffer_snapshot =
3686                            active_editor.read(cx).buffer().read(cx).snapshot(cx);
3687                        let mut folded_buffers = HashSet::default();
3688                        let mut not_folded_buffers = HashSet::default();
3689                        let mut matches_by_buffer = HashMap::default();
3690
3691                        for (match_range, search_data) in &search_state.matches {
3692                            let Some((start_anchor, _)) =
3693                                multi_buffer_snapshot.anchor_to_buffer_anchor(match_range.start)
3694                            else {
3695                                continue;
3696                            };
3697                            let start_buffer_id = start_anchor.buffer_id;
3698                            let end_buffer_id = multi_buffer_snapshot
3699                                .anchor_to_buffer_anchor(match_range.end)
3700                                .map(|(anchor, _)| anchor.buffer_id);
3701
3702                            let mut any_folded = false;
3703                            for buffer_id in
3704                                [Some(start_buffer_id), end_buffer_id].into_iter().flatten()
3705                            {
3706                                if folded_buffers.contains(&buffer_id) {
3707                                    any_folded = true;
3708                                } else if !not_folded_buffers.contains(&buffer_id) {
3709                                    if active_editor.read(cx).is_buffer_folded(buffer_id, cx) {
3710                                        folded_buffers.insert(buffer_id);
3711                                        any_folded = true;
3712                                    } else {
3713                                        not_folded_buffers.insert(buffer_id);
3714                                    }
3715                                }
3716                            }
3717                            if any_folded {
3718                                continue;
3719                            }
3720
3721                            matches_by_buffer
3722                                .entry(start_buffer_id)
3723                                .or_insert_with(Vec::new)
3724                                .push((match_range.clone(), Arc::clone(search_data)));
3725                        }
3726
3727                        Some(SearchPrecomputed {
3728                            multi_buffer_snapshot,
3729                            matches_by_buffer,
3730                            folded_buffers,
3731                        })
3732                    } else {
3733                        None
3734                    };
3735
3736                let mut parent_dirs = Vec::<ParentStats>::new();
3737                for entry in outline_panel.fs_entries.clone() {
3738                    let is_expanded = outline_panel.is_expanded(&entry);
3739                    let (depth, should_add) = match &entry {
3740                        FsEntry::Directory(directory_entry) => {
3741                            let mut should_add = true;
3742                            let is_root = project
3743                                .read(cx)
3744                                .worktree_for_id(directory_entry.worktree_id, cx)
3745                                .is_some_and(|worktree| {
3746                                    worktree.read(cx).root_entry() == Some(&directory_entry.entry)
3747                                });
3748                            let folded = auto_fold_dirs
3749                                && !is_root
3750                                && outline_panel
3751                                    .unfolded_dirs
3752                                    .get(&directory_entry.worktree_id)
3753                                    .is_none_or(|unfolded_dirs| {
3754                                        !unfolded_dirs.contains(&directory_entry.entry.id)
3755                                    });
3756                            let fs_depth = outline_panel
3757                                .fs_entries_depth
3758                                .get(&(directory_entry.worktree_id, directory_entry.entry.id))
3759                                .copied()
3760                                .unwrap_or(0);
3761                            while let Some(parent) = parent_dirs.last() {
3762                                if !is_root && directory_entry.entry.path.starts_with(&parent.path)
3763                                {
3764                                    break;
3765                                }
3766                                parent_dirs.pop();
3767                            }
3768                            let auto_fold = match parent_dirs.last() {
3769                                Some(parent) => {
3770                                    parent.folded
3771                                        && Some(parent.path.as_ref())
3772                                            == directory_entry.entry.path.parent()
3773                                        && outline_panel
3774                                            .fs_children_count
3775                                            .get(&directory_entry.worktree_id)
3776                                            .and_then(|entries| {
3777                                                entries.get(&directory_entry.entry.path)
3778                                            })
3779                                            .copied()
3780                                            .unwrap_or_default()
3781                                            .may_be_fold_part()
3782                                }
3783                                None => false,
3784                            };
3785                            let folded = folded || auto_fold;
3786                            let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
3787                                Some(parent) => {
3788                                    let parent_folded = parent.folded;
3789                                    let parent_expanded = parent.expanded;
3790                                    let new_depth = if parent_folded {
3791                                        parent.depth
3792                                    } else {
3793                                        parent.depth + 1
3794                                    };
3795                                    parent_dirs.push(ParentStats {
3796                                        path: directory_entry.entry.path.clone(),
3797                                        folded,
3798                                        expanded: parent_expanded && is_expanded,
3799                                        depth: new_depth,
3800                                    });
3801                                    (new_depth, parent_expanded, parent_folded)
3802                                }
3803                                None => {
3804                                    parent_dirs.push(ParentStats {
3805                                        path: directory_entry.entry.path.clone(),
3806                                        folded,
3807                                        expanded: is_expanded,
3808                                        depth: fs_depth,
3809                                    });
3810                                    (fs_depth, true, false)
3811                                }
3812                            };
3813
3814                            if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take()
3815                            {
3816                                if folded
3817                                    && directory_entry.worktree_id == folded_dirs.worktree_id
3818                                    && directory_entry.entry.path.parent()
3819                                        == folded_dirs
3820                                            .entries
3821                                            .last()
3822                                            .map(|entry| entry.path.as_ref())
3823                                {
3824                                    folded_dirs.entries.push(directory_entry.entry.clone());
3825                                    folded_dirs_entry = Some((folded_depth, folded_dirs))
3826                                } else {
3827                                    if !is_singleton {
3828                                        let start_of_collapsed_dir_sequence = !parent_expanded
3829                                            && parent_dirs
3830                                                .iter()
3831                                                .rev()
3832                                                .nth(folded_dirs.entries.len() + 1)
3833                                                .is_none_or(|parent| parent.expanded);
3834                                        if start_of_collapsed_dir_sequence
3835                                            || parent_expanded
3836                                            || query.is_some()
3837                                        {
3838                                            if parent_folded {
3839                                                folded_dirs
3840                                                    .entries
3841                                                    .push(directory_entry.entry.clone());
3842                                                should_add = false;
3843                                            }
3844                                            let new_folded_dirs =
3845                                                PanelEntry::FoldedDirs(folded_dirs.clone());
3846                                            outline_panel.push_entry(
3847                                                &mut generation_state,
3848                                                track_matches,
3849                                                new_folded_dirs,
3850                                                folded_depth,
3851                                                cx,
3852                                            );
3853                                        }
3854                                    }
3855
3856                                    folded_dirs_entry = if parent_folded {
3857                                        None
3858                                    } else {
3859                                        Some((
3860                                            depth,
3861                                            FoldedDirsEntry {
3862                                                worktree_id: directory_entry.worktree_id,
3863                                                entries: vec![directory_entry.entry.clone()],
3864                                            },
3865                                        ))
3866                                    };
3867                                }
3868                            } else if folded {
3869                                folded_dirs_entry = Some((
3870                                    depth,
3871                                    FoldedDirsEntry {
3872                                        worktree_id: directory_entry.worktree_id,
3873                                        entries: vec![directory_entry.entry.clone()],
3874                                    },
3875                                ));
3876                            }
3877
3878                            let should_add =
3879                                should_add && parent_expanded && folded_dirs_entry.is_none();
3880                            (depth, should_add)
3881                        }
3882                        FsEntry::ExternalFile(..) => {
3883                            if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() {
3884                                let parent_expanded = parent_dirs
3885                                    .iter()
3886                                    .rev()
3887                                    .find(|parent| {
3888                                        folded_dir
3889                                            .entries
3890                                            .iter()
3891                                            .all(|entry| entry.path != parent.path)
3892                                    })
3893                                    .is_none_or(|parent| parent.expanded);
3894                                if !is_singleton && (parent_expanded || query.is_some()) {
3895                                    outline_panel.push_entry(
3896                                        &mut generation_state,
3897                                        track_matches,
3898                                        PanelEntry::FoldedDirs(folded_dir),
3899                                        folded_depth,
3900                                        cx,
3901                                    );
3902                                }
3903                            }
3904                            parent_dirs.clear();
3905                            (0, true)
3906                        }
3907                        FsEntry::File(file) => {
3908                            if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3909                                let parent_expanded = parent_dirs
3910                                    .iter()
3911                                    .rev()
3912                                    .find(|parent| {
3913                                        folded_dirs
3914                                            .entries
3915                                            .iter()
3916                                            .all(|entry| entry.path != parent.path)
3917                                    })
3918                                    .is_none_or(|parent| parent.expanded);
3919                                if !is_singleton && (parent_expanded || query.is_some()) {
3920                                    outline_panel.push_entry(
3921                                        &mut generation_state,
3922                                        track_matches,
3923                                        PanelEntry::FoldedDirs(folded_dirs),
3924                                        folded_depth,
3925                                        cx,
3926                                    );
3927                                }
3928                            }
3929
3930                            let fs_depth = outline_panel
3931                                .fs_entries_depth
3932                                .get(&(file.worktree_id, file.entry.id))
3933                                .copied()
3934                                .unwrap_or(0);
3935                            while let Some(parent) = parent_dirs.last() {
3936                                if file.entry.path.starts_with(&parent.path) {
3937                                    break;
3938                                }
3939                                parent_dirs.pop();
3940                            }
3941                            match parent_dirs.last() {
3942                                Some(parent) => {
3943                                    let new_depth = parent.depth + 1;
3944                                    (new_depth, parent.expanded)
3945                                }
3946                                None => (fs_depth, true),
3947                            }
3948                        }
3949                    };
3950
3951                    if !is_singleton
3952                        && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3953                    {
3954                        outline_panel.push_entry(
3955                            &mut generation_state,
3956                            track_matches,
3957                            PanelEntry::Fs(entry.clone()),
3958                            depth,
3959                            cx,
3960                        );
3961                    }
3962
3963                    match outline_panel.mode {
3964                        ItemsDisplayMode::Search(_) => {
3965                            if (is_singleton || query.is_some() || (should_add && is_expanded))
3966                                && let Some(search) = &search_precomputed
3967                            {
3968                                outline_panel.add_search_entries(
3969                                    &mut generation_state,
3970                                    search,
3971                                    &entry,
3972                                    depth,
3973                                    query.is_some(),
3974                                    is_singleton,
3975                                    cx,
3976                                );
3977                            }
3978                        }
3979                        ItemsDisplayMode::Outline => {
3980                            let excerpts_to_consider =
3981                                if is_singleton || query.is_some() || (should_add && is_expanded) {
3982                                    match &entry {
3983                                        FsEntry::File(FsEntryFile {
3984                                            buffer_id,
3985                                            excerpts,
3986                                            ..
3987                                        })
3988                                        | FsEntry::ExternalFile(FsEntryExternalFile {
3989                                            buffer_id,
3990                                            excerpts,
3991                                            ..
3992                                        }) => Some((*buffer_id, excerpts)),
3993                                        _ => None,
3994                                    }
3995                                } else {
3996                                    None
3997                                };
3998                            if let Some((buffer_id, _entry_excerpts)) = excerpts_to_consider
3999                                && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
4000                            {
4001                                outline_panel.add_buffer_entries(
4002                                    &mut generation_state,
4003                                    buffer_id,
4004                                    depth,
4005                                    track_matches,
4006                                    is_singleton,
4007                                    query.as_deref(),
4008                                    cx,
4009                                );
4010                            }
4011                        }
4012                    }
4013
4014                    if is_singleton
4015                        && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
4016                        && !generation_state.entries.iter().any(|item| {
4017                            matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
4018                        })
4019                    {
4020                        outline_panel.push_entry(
4021                            &mut generation_state,
4022                            track_matches,
4023                            PanelEntry::Fs(entry.clone()),
4024                            0,
4025                            cx,
4026                        );
4027                    }
4028                }
4029
4030                if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
4031                    let parent_expanded = parent_dirs
4032                        .iter()
4033                        .rev()
4034                        .find(|parent| {
4035                            folded_dirs
4036                                .entries
4037                                .iter()
4038                                .all(|entry| entry.path != parent.path)
4039                        })
4040                        .is_none_or(|parent| parent.expanded);
4041                    if parent_expanded || query.is_some() {
4042                        outline_panel.push_entry(
4043                            &mut generation_state,
4044                            track_matches,
4045                            PanelEntry::FoldedDirs(folded_dirs),
4046                            folded_depth,
4047                            cx,
4048                        );
4049                    }
4050                }
4051            }) else {
4052                return (Vec::new(), None);
4053            };
4054
4055            let Some(query) = query else {
4056                return (
4057                    generation_state.entries,
4058                    generation_state
4059                        .max_width_estimate_and_index
4060                        .map(|(_, index)| index),
4061                );
4062            };
4063
4064            let mut matched_ids = match_strings(
4065                &generation_state.match_candidates,
4066                &query,
4067                true,
4068                true,
4069                usize::MAX,
4070                &AtomicBool::default(),
4071                cx.background_executor().clone(),
4072            )
4073            .await
4074            .into_iter()
4075            .map(|string_match| (string_match.candidate_id, string_match))
4076            .collect::<HashMap<_, _>>();
4077
4078            let mut id = 0;
4079            generation_state.entries.retain_mut(|cached_entry| {
4080                let retain = match matched_ids.remove(&id) {
4081                    Some(string_match) => {
4082                        cached_entry.string_match = Some(string_match);
4083                        true
4084                    }
4085                    None => false,
4086                };
4087                id += 1;
4088                retain
4089            });
4090
4091            (
4092                generation_state.entries,
4093                generation_state
4094                    .max_width_estimate_and_index
4095                    .map(|(_, index)| index),
4096            )
4097        })
4098    }
4099
4100    fn push_entry(
4101        &self,
4102        state: &mut GenerationState,
4103        track_matches: bool,
4104        entry: PanelEntry,
4105        depth: usize,
4106        cx: &mut App,
4107    ) {
4108        let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry {
4109            match folded_dirs_entry.entries.len() {
4110                0 => {
4111                    debug_panic!("Empty folded dirs receiver");
4112                    return;
4113                }
4114                1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
4115                    worktree_id: folded_dirs_entry.worktree_id,
4116                    entry: folded_dirs_entry.entries[0].clone(),
4117                })),
4118                _ => entry,
4119            }
4120        } else {
4121            entry
4122        };
4123
4124        if track_matches {
4125            let id = state.entries.len();
4126            match &entry {
4127                PanelEntry::Fs(fs_entry) => {
4128                    if let Some(file_name) = self
4129                        .relative_path(fs_entry, cx)
4130                        .and_then(|path| Some(path.file_name()?.to_string()))
4131                    {
4132                        state
4133                            .match_candidates
4134                            .push(StringMatchCandidate::new(id, &file_name));
4135                    }
4136                }
4137                PanelEntry::FoldedDirs(folded_dir_entry) => {
4138                    let dir_names = self.dir_names_string(
4139                        &folded_dir_entry.entries,
4140                        folded_dir_entry.worktree_id,
4141                        cx,
4142                    );
4143                    {
4144                        state
4145                            .match_candidates
4146                            .push(StringMatchCandidate::new(id, &dir_names));
4147                    }
4148                }
4149                PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state
4150                    .match_candidates
4151                    .push(StringMatchCandidate::new(id, &outline_entry.text)),
4152                PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {}
4153                PanelEntry::Search(new_search_entry) => {
4154                    if let Some(search_data) = new_search_entry.render_data.get() {
4155                        state
4156                            .match_candidates
4157                            .push(StringMatchCandidate::new(id, &search_data.context_text));
4158                    }
4159                }
4160            }
4161        }
4162
4163        let width_estimate = self.width_estimate(depth, &entry, cx);
4164        if Some(width_estimate)
4165            > state
4166                .max_width_estimate_and_index
4167                .map(|(estimate, _)| estimate)
4168        {
4169            state.max_width_estimate_and_index = Some((width_estimate, state.entries.len()));
4170        }
4171        state.entries.push(CachedEntry {
4172            depth,
4173            entry,
4174            string_match: None,
4175        });
4176    }
4177
4178    fn dir_names_string(&self, entries: &[GitEntry], worktree_id: WorktreeId, cx: &App) -> String {
4179        let dir_names_segment = entries
4180            .iter()
4181            .map(|entry| self.entry_name(&worktree_id, entry, cx))
4182            .collect::<PathBuf>();
4183        dir_names_segment.to_string_lossy().into_owned()
4184    }
4185
4186    fn query(&self, cx: &App) -> Option<String> {
4187        let query = self.filter_editor.read(cx).text(cx);
4188        if query.trim().is_empty() {
4189            None
4190        } else {
4191            Some(query)
4192        }
4193    }
4194
4195    fn is_expanded(&self, entry: &FsEntry) -> bool {
4196        let entry_to_check = match entry {
4197            FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
4198                CollapsedEntry::ExternalFile(*buffer_id)
4199            }
4200            FsEntry::File(FsEntryFile {
4201                worktree_id,
4202                buffer_id,
4203                ..
4204            }) => CollapsedEntry::File(*worktree_id, *buffer_id),
4205            FsEntry::Directory(FsEntryDirectory {
4206                worktree_id, entry, ..
4207            }) => CollapsedEntry::Dir(*worktree_id, entry.id),
4208        };
4209        !self.collapsed_entries.contains(&entry_to_check)
4210    }
4211
4212    fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) -> bool {
4213        if !self.active {
4214            return false;
4215        }
4216
4217        let mut update_cached_items = false;
4218        update_cached_items |= self.update_search_matches(window, cx);
4219        self.fetch_outdated_outlines(window, cx);
4220        if update_cached_items {
4221            self.selected_entry.invalidate();
4222        }
4223        update_cached_items
4224    }
4225
4226    fn update_search_matches(
4227        &mut self,
4228        window: &mut Window,
4229        cx: &mut Context<OutlinePanel>,
4230    ) -> bool {
4231        if !self.active {
4232            return false;
4233        }
4234
4235        let project_search = self
4236            .active_item()
4237            .and_then(|item| item.downcast::<ProjectSearchView>());
4238        let project_search_matches = project_search
4239            .as_ref()
4240            .map(|project_search| project_search.read(cx).get_matches(cx))
4241            .unwrap_or_default();
4242
4243        let buffer_search = self
4244            .active_item()
4245            .as_deref()
4246            .and_then(|active_item| {
4247                self.workspace
4248                    .upgrade()
4249                    .and_then(|workspace| workspace.read(cx).pane_for(active_item))
4250            })
4251            .and_then(|pane| {
4252                pane.read(cx)
4253                    .toolbar()
4254                    .read(cx)
4255                    .item_of_type::<BufferSearchBar>()
4256            });
4257        let buffer_search_matches = self
4258            .active_editor()
4259            .map(|active_editor| {
4260                active_editor.update(cx, |editor, cx| editor.get_matches(window, cx).0)
4261            })
4262            .unwrap_or_default();
4263
4264        let mut update_cached_entries = false;
4265        if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
4266            if matches!(self.mode, ItemsDisplayMode::Search(_)) {
4267                self.mode = ItemsDisplayMode::Outline;
4268                update_cached_entries = true;
4269            }
4270        } else {
4271            let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
4272                (
4273                    SearchKind::Project,
4274                    project_search_matches,
4275                    project_search
4276                        .map(|project_search| project_search.read(cx).search_query_text(cx))
4277                        .unwrap_or_default(),
4278                )
4279            } else {
4280                (
4281                    SearchKind::Buffer,
4282                    buffer_search_matches,
4283                    buffer_search
4284                        .map(|buffer_search| buffer_search.read(cx).query(cx))
4285                        .unwrap_or_default(),
4286                )
4287            };
4288
4289            let changed = match &self.mode {
4290                ItemsDisplayMode::Search(current) => {
4291                    current.query != new_search_query
4292                        || current.kind != kind
4293                        || current.matches.len() != new_search_matches.len()
4294                        || current
4295                            .matches
4296                            .iter()
4297                            .zip(&new_search_matches)
4298                            .any(|((existing, _), incoming)| existing != incoming)
4299                }
4300                ItemsDisplayMode::Outline => true,
4301            };
4302            if changed {
4303                let previous_matches = match &mut self.mode {
4304                    ItemsDisplayMode::Search(current) if current.kind == kind => {
4305                        current.matches.drain(..).collect()
4306                    }
4307                    _ => HashMap::default(),
4308                };
4309                self.mode = ItemsDisplayMode::Search(SearchState::new(
4310                    kind,
4311                    new_search_query,
4312                    previous_matches,
4313                    new_search_matches,
4314                    cx.theme().syntax().clone(),
4315                    window,
4316                    cx,
4317                ));
4318                update_cached_entries = true;
4319            }
4320        }
4321        update_cached_entries
4322    }
4323
4324    fn add_buffer_entries(
4325        &mut self,
4326        state: &mut GenerationState,
4327        buffer_id: BufferId,
4328        parent_depth: usize,
4329        track_matches: bool,
4330        is_singleton: bool,
4331        query: Option<&str>,
4332        cx: &mut Context<Self>,
4333    ) {
4334        let Some(buffer) = self.buffers.get(&buffer_id) else {
4335            return;
4336        };
4337
4338        let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx);
4339
4340        for excerpt in &buffer.excerpts {
4341            let excerpt_depth = parent_depth + 1;
4342            self.push_entry(
4343                state,
4344                track_matches,
4345                PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())),
4346                excerpt_depth,
4347                cx,
4348            );
4349
4350            let mut outline_base_depth = excerpt_depth + 1;
4351            if is_singleton {
4352                outline_base_depth = 0;
4353                state.clear();
4354            } else if query.is_none()
4355                && self
4356                    .collapsed_entries
4357                    .contains(&CollapsedEntry::Excerpt(excerpt.clone()))
4358            {
4359                continue;
4360            }
4361
4362            let mut last_depth_at_level: Vec<Option<Range<Anchor>>> = vec![None; 10];
4363
4364            let all_outlines: Vec<_> = buffer.iter_outlines().collect();
4365
4366            let mut outline_has_children = HashMap::default();
4367            let mut visible_outlines = Vec::new();
4368            let mut collapsed_state: Option<(usize, Range<Anchor>)> = None;
4369
4370            for (i, &outline) in all_outlines.iter().enumerate() {
4371                let has_children = all_outlines
4372                    .get(i + 1)
4373                    .map(|next| next.depth > outline.depth)
4374                    .unwrap_or(false);
4375
4376                outline_has_children.insert((outline.range.clone(), outline.depth), has_children);
4377
4378                let mut should_include = true;
4379
4380                if let Some((collapsed_depth, collapsed_range)) = &collapsed_state {
4381                    if outline.depth <= *collapsed_depth {
4382                        collapsed_state = None;
4383                    } else if let Some(buffer_snapshot) = buffer_snapshot.as_ref() {
4384                        let outline_start = outline.range.start;
4385                        if outline_start
4386                            .cmp(&collapsed_range.start, buffer_snapshot)
4387                            .is_ge()
4388                            && outline_start
4389                                .cmp(&collapsed_range.end, buffer_snapshot)
4390                                .is_lt()
4391                        {
4392                            should_include = false; // Skip - inside collapsed range
4393                        } else {
4394                            collapsed_state = None;
4395                        }
4396                    }
4397                }
4398
4399                // Check if this outline itself is collapsed
4400                if should_include
4401                    && self
4402                        .collapsed_entries
4403                        .contains(&CollapsedEntry::Outline(outline.range.clone()))
4404                {
4405                    collapsed_state = Some((outline.depth, outline.range.clone()));
4406                }
4407
4408                if should_include {
4409                    visible_outlines.push(outline);
4410                }
4411            }
4412
4413            self.outline_children_cache
4414                .entry(buffer_id)
4415                .or_default()
4416                .extend(outline_has_children);
4417
4418            for outline in visible_outlines {
4419                let outline_entry = outline.clone();
4420
4421                if outline.depth < last_depth_at_level.len() {
4422                    last_depth_at_level[outline.depth] = Some(outline.range.clone());
4423                    // Clear deeper levels when we go back to a shallower depth
4424                    for d in (outline.depth + 1)..last_depth_at_level.len() {
4425                        last_depth_at_level[d] = None;
4426                    }
4427                }
4428
4429                self.push_entry(
4430                    state,
4431                    track_matches,
4432                    PanelEntry::Outline(OutlineEntry::Outline(outline_entry)),
4433                    outline_base_depth + outline.depth,
4434                    cx,
4435                );
4436            }
4437        }
4438    }
4439
4440    fn add_search_entries(
4441        &mut self,
4442        state: &mut GenerationState,
4443        search: &SearchPrecomputed,
4444        parent_entry: &FsEntry,
4445        parent_depth: usize,
4446        track_matches: bool,
4447        is_singleton: bool,
4448        cx: &mut Context<Self>,
4449    ) {
4450        let ItemsDisplayMode::Search(search_state) = &self.mode else {
4451            return;
4452        };
4453        let kind = search_state.kind;
4454
4455        let (buffer_id, excerpts) = match parent_entry {
4456            FsEntry::Directory(_) => return,
4457            FsEntry::ExternalFile(external) => (external.buffer_id, &external.excerpts),
4458            FsEntry::File(file) => (file.buffer_id, &file.excerpts),
4459        };
4460
4461        if search.folded_buffers.contains(&buffer_id) {
4462            return;
4463        }
4464        let Some(buffer_matches) = search.matches_by_buffer.get(&buffer_id) else {
4465            return;
4466        };
4467
4468        let excerpt_ranges = excerpts
4469            .iter()
4470            .filter_map(|excerpt| {
4471                let start = search
4472                    .multi_buffer_snapshot
4473                    .anchor_in_buffer(excerpt.context.start)?;
4474                let end = search
4475                    .multi_buffer_snapshot
4476                    .anchor_in_buffer(excerpt.context.end)?;
4477                Some(start..end)
4478            })
4479            .collect::<Vec<_>>();
4480
4481        let depth = if is_singleton { 0 } else { parent_depth + 1 };
4482        for (match_range, search_data) in buffer_matches.iter().filter(|(match_range, _)| {
4483            excerpt_ranges.iter().any(|excerpt_range| {
4484                excerpt_range.overlaps(match_range, &search.multi_buffer_snapshot)
4485            })
4486        }) {
4487            self.push_entry(
4488                state,
4489                track_matches,
4490                PanelEntry::Search(SearchEntry {
4491                    match_range: match_range.clone(),
4492                    kind,
4493                    render_data: Arc::clone(search_data),
4494                }),
4495                depth,
4496                cx,
4497            );
4498        }
4499    }
4500
4501    fn active_editor(&self) -> Option<Entity<Editor>> {
4502        self.active_item.as_ref()?.active_editor.upgrade()
4503    }
4504
4505    fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
4506        self.active_item.as_ref()?.item_handle.upgrade()
4507    }
4508
4509    fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
4510        self.active_item().is_none_or(|active_item| {
4511            !self.pinned && active_item.item_id() != new_active_item.item_id()
4512        })
4513    }
4514
4515    pub fn toggle_active_editor_pin(
4516        &mut self,
4517        _: &ToggleActiveEditorPin,
4518        window: &mut Window,
4519        cx: &mut Context<Self>,
4520    ) {
4521        self.pinned = !self.pinned;
4522        if !self.pinned
4523            && let Some((active_item, active_editor)) = self
4524                .workspace
4525                .upgrade()
4526                .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
4527            && self.should_replace_active_item(active_item.as_ref())
4528        {
4529            self.replace_active_editor(active_item, active_editor, window, cx);
4530        }
4531
4532        cx.notify();
4533    }
4534
4535    fn selected_entry(&self) -> Option<&PanelEntry> {
4536        match &self.selected_entry {
4537            SelectedEntry::Invalidated(entry) => entry.as_ref(),
4538            SelectedEntry::Valid(entry, _) => Some(entry),
4539            SelectedEntry::None => None,
4540        }
4541    }
4542
4543    fn select_entry(
4544        &mut self,
4545        entry: PanelEntry,
4546        focus: bool,
4547        window: &mut Window,
4548        cx: &mut Context<Self>,
4549    ) {
4550        if focus {
4551            self.focus_handle.focus(window, cx);
4552        }
4553        let ix = self
4554            .cached_entries
4555            .iter()
4556            .enumerate()
4557            .find(|(_, cached_entry)| &cached_entry.entry == &entry)
4558            .map(|(i, _)| i)
4559            .unwrap_or_default();
4560
4561        self.selected_entry = SelectedEntry::Valid(entry, ix);
4562
4563        self.autoscroll(cx);
4564        cx.notify();
4565    }
4566
4567    fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
4568        let item_text_chars = match entry {
4569            PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
4570                .buffer_snapshot_for_id(external.buffer_id, cx)
4571                .and_then(|snapshot| Some(snapshot.file()?.path().file_name()?.len()))
4572                .unwrap_or_default(),
4573            PanelEntry::Fs(FsEntry::Directory(directory)) => directory
4574                .entry
4575                .path
4576                .file_name()
4577                .map(|name| name.len())
4578                .unwrap_or_default(),
4579            PanelEntry::Fs(FsEntry::File(file)) => file
4580                .entry
4581                .path
4582                .file_name()
4583                .map(|name| name.len())
4584                .unwrap_or_default(),
4585            PanelEntry::FoldedDirs(folded_dirs) => {
4586                folded_dirs
4587                    .entries
4588                    .iter()
4589                    .map(|dir| {
4590                        dir.path
4591                            .file_name()
4592                            .map(|name| name.len())
4593                            .unwrap_or_default()
4594                    })
4595                    .sum::<usize>()
4596                    + folded_dirs.entries.len().saturating_sub(1) * "/".len()
4597            }
4598            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
4599                .excerpt_label(&excerpt, cx)
4600                .map(|label| label.len())
4601                .unwrap_or_default(),
4602            PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.text.len(),
4603            PanelEntry::Search(search) => search
4604                .render_data
4605                .get()
4606                .map(|data| data.context_text.len())
4607                .unwrap_or_default(),
4608        };
4609
4610        (item_text_chars + depth) as u64
4611    }
4612
4613    fn render_main_contents(
4614        &mut self,
4615        query: Option<String>,
4616        show_indent_guides: bool,
4617        indent_size: f32,
4618        window: &mut Window,
4619        cx: &mut Context<Self>,
4620    ) -> impl IntoElement {
4621        let contents = if self.cached_entries.is_empty() {
4622            let header = if query.is_some() {
4623                "No matches for query"
4624            } else {
4625                "No outlines available"
4626            };
4627
4628            v_flex()
4629                .id("empty-outline-state")
4630                .gap_0p5()
4631                .flex_1()
4632                .justify_center()
4633                .size_full()
4634                .child(h_flex().justify_center().child(Label::new(header)))
4635                .when_some(query, |panel, query| {
4636                    panel.child(
4637                        h_flex()
4638                            .px_0p5()
4639                            .justify_center()
4640                            .bg(cx.theme().colors().element_selected.opacity(0.2))
4641                            .child(Label::new(query)),
4642                    )
4643                })
4644                .child(
4645                    h_flex()
4646                        .gap_1()
4647                        .justify_center()
4648                        .child(Label::new("Toggle Panel With").color(Color::Muted))
4649                        .child({
4650                            let key_binding = match self.position(window, cx) {
4651                                DockPosition::Left => {
4652                                    KeyBinding::for_action(&workspace::ToggleLeftDock, cx)
4653                                        .into_any_element()
4654                                }
4655                                DockPosition::Bottom => {
4656                                    KeyBinding::for_action(&workspace::ToggleBottomDock, cx)
4657                                        .into_any_element()
4658                                }
4659                                DockPosition::Right => {
4660                                    KeyBinding::for_action(&workspace::ToggleRightDock, cx)
4661                                        .into_any_element()
4662                                }
4663                            };
4664
4665                            key_binding
4666                        }),
4667                )
4668        } else {
4669            let list_contents = {
4670                let items_len = self.cached_entries.len();
4671                let multi_buffer_snapshot = self
4672                    .active_editor()
4673                    .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
4674                uniform_list(
4675                    "entries",
4676                    items_len,
4677                    cx.processor(move |outline_panel, range: Range<usize>, window, cx| {
4678                        outline_panel.rendered_entries_len = range.end - range.start;
4679                        let entries = outline_panel.cached_entries.get(range);
4680                        entries
4681                            .map(|entries| entries.to_vec())
4682                            .unwrap_or_default()
4683                            .into_iter()
4684                            .filter_map(|cached_entry| match cached_entry.entry {
4685                                PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4686                                    &entry,
4687                                    cached_entry.depth,
4688                                    cached_entry.string_match.as_ref(),
4689                                    window,
4690                                    cx,
4691                                )),
4692                                PanelEntry::FoldedDirs(folded_dirs_entry) => {
4693                                    Some(outline_panel.render_folded_dirs(
4694                                        &folded_dirs_entry,
4695                                        cached_entry.depth,
4696                                        cached_entry.string_match.as_ref(),
4697                                        window,
4698                                        cx,
4699                                    ))
4700                                }
4701                                PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4702                                    outline_panel.render_excerpt(
4703                                        &excerpt,
4704                                        cached_entry.depth,
4705                                        window,
4706                                        cx,
4707                                    )
4708                                }
4709                                PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4710                                    Some(outline_panel.render_outline(
4711                                        &entry,
4712                                        cached_entry.depth,
4713                                        cached_entry.string_match.as_ref(),
4714                                        window,
4715                                        cx,
4716                                    ))
4717                                }
4718                                PanelEntry::Search(SearchEntry {
4719                                    match_range,
4720                                    render_data,
4721                                    kind,
4722                                    ..
4723                                }) => outline_panel.render_search_match(
4724                                    multi_buffer_snapshot.as_ref(),
4725                                    &match_range,
4726                                    &render_data,
4727                                    kind,
4728                                    cached_entry.depth,
4729                                    cached_entry.string_match.as_ref(),
4730                                    window,
4731                                    cx,
4732                                ),
4733                            })
4734                            .collect()
4735                    }),
4736                )
4737                .with_sizing_behavior(ListSizingBehavior::Infer)
4738                .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4739                .with_width_from_item(self.max_width_item_index)
4740                .track_scroll(&self.scroll_handle)
4741                .when(show_indent_guides, |list| {
4742                    list.with_decoration(
4743                        ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
4744                            .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| {
4745                                let entries = outline_panel.cached_entries.get(range);
4746                                if let Some(entries) = entries {
4747                                    entries.iter().map(|item| item.depth).collect()
4748                                } else {
4749                                    smallvec::SmallVec::new()
4750                                }
4751                            })
4752                            .with_render_fn(cx.entity(), move |outline_panel, params, _, _| {
4753                                const LEFT_OFFSET: Pixels = ui::LIST_ITEM_INDENT_GUIDE_LEFT_OFFSET;
4754
4755                                let indent_size = params.indent_size;
4756                                let item_height = params.item_height;
4757                                let active_indent_guide_ix = find_active_indent_guide_ix(
4758                                    outline_panel,
4759                                    &params.indent_guides,
4760                                );
4761
4762                                params
4763                                    .indent_guides
4764                                    .into_iter()
4765                                    .enumerate()
4766                                    .map(|(ix, layout)| {
4767                                        let bounds = Bounds::new(
4768                                            point(
4769                                                layout.offset.x * indent_size + LEFT_OFFSET,
4770                                                layout.offset.y * item_height,
4771                                            ),
4772                                            size(px(1.), layout.length * item_height),
4773                                        );
4774                                        ui::RenderedIndentGuide {
4775                                            bounds,
4776                                            layout,
4777                                            is_active: active_indent_guide_ix == Some(ix),
4778                                            hitbox: None,
4779                                        }
4780                                    })
4781                                    .collect()
4782                            }),
4783                    )
4784                })
4785            };
4786
4787            v_flex()
4788                .flex_shrink_1()
4789                .size_full()
4790                .child(list_contents.size_full().flex_shrink_1())
4791                .custom_scrollbars(
4792                    Scrollbars::for_settings::<OutlinePanelSettingsScrollbarProxy>()
4793                        .tracked_scroll_handle(&self.scroll_handle.clone())
4794                        .with_track_along(
4795                            ScrollAxes::Horizontal,
4796                            cx.theme().colors().panel_background,
4797                        )
4798                        .tracked_entity(cx.entity_id()),
4799                    window,
4800                    cx,
4801                )
4802        }
4803        .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4804            deferred(
4805                anchored()
4806                    .position(*position)
4807                    .anchor(gpui::Anchor::TopLeft)
4808                    .child(menu.clone()),
4809            )
4810            .with_priority(1)
4811        }));
4812
4813        v_flex().w_full().flex_1().overflow_hidden().child(contents)
4814    }
4815
4816    fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4817        let (pin_button_id, icon, icon_tooltip) = if pinned {
4818            ("unpin_button", IconName::Unpin, "Unpin Outline")
4819        } else {
4820            ("pin_button", IconName::Pin, "Pin Active Outline")
4821        };
4822
4823        let has_query = self.query(cx).is_some();
4824
4825        h_flex()
4826            .p_2()
4827            .h(Tab::container_height(cx))
4828            .justify_between()
4829            .border_b_1()
4830            .border_color(cx.theme().colors().border)
4831            .child(
4832                h_flex()
4833                    .w_full()
4834                    .gap_1p5()
4835                    .child(
4836                        Icon::new(IconName::MagnifyingGlass)
4837                            .size(IconSize::Small)
4838                            .color(Color::Muted),
4839                    )
4840                    .child(self.filter_editor.clone()),
4841            )
4842            .child(
4843                h_flex()
4844                    .when(has_query, |this| {
4845                        this.child(
4846                            IconButton::new("clear_filter", IconName::Close)
4847                                .shape(IconButtonShape::Square)
4848                                .tooltip(Tooltip::text("Clear Filter"))
4849                                .on_click(cx.listener(|outline_panel, _, window, cx| {
4850                                    outline_panel.filter_editor.update(cx, |editor, cx| {
4851                                        editor.set_text("", window, cx);
4852                                    });
4853                                    cx.notify();
4854                                })),
4855                        )
4856                    })
4857                    .child(
4858                        IconButton::new(pin_button_id, icon)
4859                            .tooltip(Tooltip::text(icon_tooltip))
4860                            .shape(IconButtonShape::Square)
4861                            .on_click(cx.listener(|outline_panel, _, window, cx| {
4862                                outline_panel.toggle_active_editor_pin(
4863                                    &ToggleActiveEditorPin,
4864                                    window,
4865                                    cx,
4866                                );
4867                            })),
4868                    ),
4869            )
4870    }
4871
4872    fn buffers_inside_directory(
4873        &self,
4874        dir_worktree: WorktreeId,
4875        dir_entry: &GitEntry,
4876    ) -> HashSet<BufferId> {
4877        if !dir_entry.is_dir() {
4878            debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4879            return HashSet::default();
4880        }
4881
4882        self.fs_entries
4883            .iter()
4884            .skip_while(|fs_entry| match fs_entry {
4885                FsEntry::Directory(directory) => {
4886                    directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4887                }
4888                _ => true,
4889            })
4890            .skip(1)
4891            .take_while(|fs_entry| match fs_entry {
4892                FsEntry::ExternalFile(..) => false,
4893                FsEntry::Directory(directory) => {
4894                    directory.worktree_id == dir_worktree
4895                        && directory.entry.path.starts_with(&dir_entry.path)
4896                }
4897                FsEntry::File(file) => {
4898                    file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4899                }
4900            })
4901            .filter_map(|fs_entry| match fs_entry {
4902                FsEntry::File(file) => Some(file.buffer_id),
4903                _ => None,
4904            })
4905            .collect()
4906    }
4907}
4908
4909fn workspace_active_editor(
4910    workspace: &Workspace,
4911    cx: &App,
4912) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4913    let active_item = workspace.active_item(cx)?;
4914    let active_editor = active_item
4915        .act_as::<Editor>(cx)
4916        .filter(|editor| editor.read(cx).mode().is_full())?;
4917    Some((active_item, active_editor))
4918}
4919
4920fn back_to_common_visited_parent(
4921    visited_dirs: &mut Vec<(ProjectEntryId, Arc<RelPath>)>,
4922    worktree_id: &WorktreeId,
4923    new_entry: &Entry,
4924) -> Option<(WorktreeId, ProjectEntryId)> {
4925    while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4926        match new_entry.path.parent() {
4927            Some(parent_path) => {
4928                if parent_path == visited_path.as_ref() {
4929                    return Some((*worktree_id, *visited_dir_id));
4930                }
4931            }
4932            None => {
4933                break;
4934            }
4935        }
4936        visited_dirs.pop();
4937    }
4938    None
4939}
4940
4941fn file_name(path: &Path) -> String {
4942    let mut current_path = path;
4943    loop {
4944        if let Some(file_name) = current_path.file_name() {
4945            return file_name.to_string_lossy().into_owned();
4946        }
4947        match current_path.parent() {
4948            Some(parent) => current_path = parent,
4949            None => return path.to_string_lossy().into_owned(),
4950        }
4951    }
4952}
4953
4954impl Panel for OutlinePanel {
4955    fn persistent_name() -> &'static str {
4956        "Outline Panel"
4957    }
4958
4959    fn panel_key() -> &'static str {
4960        OUTLINE_PANEL_KEY
4961    }
4962
4963    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4964        match OutlinePanelSettings::get_global(cx).dock {
4965            DockSide::Left => DockPosition::Left,
4966            DockSide::Right => DockPosition::Right,
4967        }
4968    }
4969
4970    fn position_is_valid(&self, position: DockPosition) -> bool {
4971        matches!(position, DockPosition::Left | DockPosition::Right)
4972    }
4973
4974    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4975        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
4976            let dock = match position {
4977                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
4978                DockPosition::Right => DockSide::Right,
4979            };
4980            settings.outline_panel.get_or_insert_default().dock = Some(dock);
4981        });
4982    }
4983
4984    fn default_size(&self, _: &Window, cx: &App) -> Pixels {
4985        OutlinePanelSettings::get_global(cx).default_width
4986    }
4987
4988    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4989        OutlinePanelSettings::get_global(cx)
4990            .button
4991            .then_some(IconName::ListTree)
4992    }
4993
4994    fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
4995        Some("Outline Panel")
4996    }
4997
4998    fn toggle_action(&self) -> Box<dyn Action> {
4999        Box::new(ToggleFocus)
5000    }
5001
5002    fn starts_open(&self, _window: &Window, _: &App) -> bool {
5003        self.active
5004    }
5005
5006    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
5007        cx.spawn_in(window, async move |outline_panel, cx| {
5008            outline_panel
5009                .update_in(cx, |outline_panel, window, cx| {
5010                    let old_active = outline_panel.active;
5011                    outline_panel.active = active;
5012                    if old_active != active {
5013                        if active
5014                            && let Some((active_item, active_editor)) =
5015                                outline_panel.workspace.upgrade().and_then(|workspace| {
5016                                    workspace_active_editor(workspace.read(cx), cx)
5017                                })
5018                        {
5019                            if outline_panel.should_replace_active_item(active_item.as_ref()) {
5020                                outline_panel.replace_active_editor(
5021                                    active_item,
5022                                    active_editor,
5023                                    window,
5024                                    cx,
5025                                );
5026                            } else {
5027                                outline_panel.update_fs_entries(active_editor, None, window, cx)
5028                            }
5029                            return;
5030                        }
5031
5032                        if !outline_panel.pinned {
5033                            outline_panel.clear_previous(window, cx);
5034                        }
5035                    }
5036                    outline_panel.serialize(cx);
5037                })
5038                .ok();
5039        })
5040        .detach()
5041    }
5042
5043    fn activation_priority(&self) -> u32 {
5044        6
5045    }
5046
5047    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
5048        Some(workspace::HideStatusItem::new(|settings| {
5049            settings.outline_panel.get_or_insert_default().button = Some(false);
5050        }))
5051    }
5052}
5053
5054impl Focusable for OutlinePanel {
5055    fn focus_handle(&self, cx: &App) -> FocusHandle {
5056        self.filter_editor.focus_handle(cx)
5057    }
5058}
5059
5060impl EventEmitter<Event> for OutlinePanel {}
5061
5062impl EventEmitter<PanelEvent> for OutlinePanel {}
5063
5064impl Render for OutlinePanel {
5065    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5066        let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| {
5067            (project.is_local(), project.is_via_remote_server())
5068        });
5069        let query = self.query(cx);
5070        let pinned = self.pinned;
5071        let settings = OutlinePanelSettings::get_global(cx);
5072        let indent_size = settings.indent_size;
5073        let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
5074
5075        let search_query = match &self.mode {
5076            ItemsDisplayMode::Search(search_query) => Some(search_query),
5077            _ => None,
5078        };
5079
5080        let search_query_text = search_query.map(|sq| sq.query.to_string());
5081
5082        v_flex()
5083            .id("outline-panel")
5084            .size_full()
5085            .overflow_hidden()
5086            .relative()
5087            .key_context(self.dispatch_context(window, cx))
5088            .on_action(cx.listener(Self::open_selected_entry))
5089            .on_action(cx.listener(Self::cancel))
5090            .on_action(cx.listener(Self::scroll_up))
5091            .on_action(cx.listener(Self::scroll_down))
5092            .on_action(cx.listener(Self::select_next))
5093            .on_action(cx.listener(Self::scroll_cursor_center))
5094            .on_action(cx.listener(Self::scroll_cursor_top))
5095            .on_action(cx.listener(Self::scroll_cursor_bottom))
5096            .on_action(cx.listener(Self::select_previous))
5097            .on_action(cx.listener(Self::select_first))
5098            .on_action(cx.listener(Self::select_last))
5099            .on_action(cx.listener(Self::select_parent))
5100            .on_action(cx.listener(Self::expand_selected_entry))
5101            .on_action(cx.listener(Self::collapse_selected_entry))
5102            .on_action(cx.listener(Self::expand_all_entries))
5103            .on_action(cx.listener(Self::collapse_all_entries))
5104            .on_action(cx.listener(Self::copy_path))
5105            .on_action(cx.listener(Self::copy_relative_path))
5106            .on_action(cx.listener(Self::toggle_active_editor_pin))
5107            .on_action(cx.listener(Self::unfold_directory))
5108            .on_action(cx.listener(Self::fold_directory))
5109            .on_action(cx.listener(Self::open_excerpts))
5110            .on_action(cx.listener(Self::open_excerpts_split))
5111            .when(is_local, |el| {
5112                el.on_action(cx.listener(Self::reveal_in_finder))
5113            })
5114            .when(is_local || is_via_ssh, |el| {
5115                el.on_action(cx.listener(Self::open_in_terminal))
5116            })
5117            .on_mouse_down(
5118                MouseButton::Right,
5119                cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
5120                    if let Some(entry) = outline_panel.selected_entry().cloned() {
5121                        outline_panel.deploy_context_menu(event.position, entry, window, cx)
5122                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
5123                        outline_panel.deploy_context_menu(
5124                            event.position,
5125                            PanelEntry::Fs(entry),
5126                            window,
5127                            cx,
5128                        )
5129                    }
5130                }),
5131            )
5132            .track_focus(&self.focus_handle)
5133            .child(self.render_filter_footer(pinned, cx))
5134            .when_some(search_query_text, |outline_panel, query_text| {
5135                outline_panel.child(
5136                    h_flex()
5137                        .py_1p5()
5138                        .px_2()
5139                        .h(Tab::container_height(cx))
5140                        .gap_0p5()
5141                        .border_b_1()
5142                        .border_color(cx.theme().colors().border_variant)
5143                        .child(Label::new("Searching:").color(Color::Muted))
5144                        .child(Label::new(query_text)),
5145                )
5146            })
5147            .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
5148    }
5149}
5150
5151fn find_active_indent_guide_ix(
5152    outline_panel: &OutlinePanel,
5153    candidates: &[IndentGuideLayout],
5154) -> Option<usize> {
5155    let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
5156        return None;
5157    };
5158    let target_depth = outline_panel
5159        .cached_entries
5160        .get(*target_ix)
5161        .map(|cached_entry| cached_entry.depth)?;
5162
5163    let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
5164        .cached_entries
5165        .get(target_ix + 1)
5166        .filter(|cached_entry| cached_entry.depth > target_depth)
5167        .map(|entry| entry.depth)
5168    {
5169        (target_ix + 1, target_depth.saturating_sub(1))
5170    } else {
5171        (*target_ix, target_depth.saturating_sub(1))
5172    };
5173
5174    candidates
5175        .iter()
5176        .enumerate()
5177        .find(|(_, guide)| {
5178            guide.offset.y <= target_ix
5179                && target_ix < guide.offset.y + guide.length
5180                && guide.offset.x == target_depth
5181        })
5182        .map(|(ix, _)| ix)
5183}
5184
5185fn subscribe_for_editor_events(
5186    editor: &Entity<Editor>,
5187    window: &mut Window,
5188    cx: &mut Context<OutlinePanel>,
5189) -> Subscription {
5190    let debounce = Some(UPDATE_DEBOUNCE);
5191    cx.subscribe_in(
5192        editor,
5193        window,
5194        move |outline_panel, editor, e: &EditorEvent, window, cx| {
5195            if !outline_panel.active {
5196                return;
5197            }
5198            match e {
5199                EditorEvent::SelectionsChanged { local: true } => {
5200                    outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5201                    cx.notify();
5202                }
5203                EditorEvent::BuffersRemoved { removed_buffer_ids } => {
5204                    outline_panel
5205                        .buffers
5206                        .retain(|buffer_id, _| !removed_buffer_ids.contains(buffer_id));
5207                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5208                }
5209                EditorEvent::BufferRangesUpdated { buffer, .. } => {
5210                    outline_panel
5211                        .new_entries_for_fs_update
5212                        .insert(buffer.read(cx).remote_id());
5213                    outline_panel.invalidate_outlines(&[buffer.read(cx).remote_id()]);
5214                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5215                }
5216                EditorEvent::BuffersEdited { buffer_ids } => {
5217                    outline_panel.invalidate_outlines(buffer_ids);
5218                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5219                    if update_cached_items {
5220                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5221                    }
5222                }
5223                EditorEvent::BufferFoldToggled { ids, .. } => {
5224                    outline_panel.invalidate_outlines(ids);
5225                    let mut latest_unfolded_buffer_id = None;
5226                    let mut latest_folded_buffer_id = None;
5227                    let mut ignore_selections_change = false;
5228                    outline_panel.new_entries_for_fs_update.extend(
5229                        ids.iter()
5230                            .filter(|id| {
5231                                if outline_panel.buffers.contains_key(&id) {
5232                                    ignore_selections_change |= outline_panel
5233                                        .preserve_selection_on_buffer_fold_toggles
5234                                        .remove(&id);
5235                                    if editor.read(cx).is_buffer_folded(**id, cx) {
5236                                        latest_folded_buffer_id = Some(**id);
5237                                        false
5238                                    } else {
5239                                        latest_unfolded_buffer_id = Some(**id);
5240                                        true
5241                                    }
5242                                } else {
5243                                    false
5244                                }
5245                            })
5246                            .copied(),
5247                    );
5248                    if !ignore_selections_change
5249                        && let Some(entry_to_select) = latest_unfolded_buffer_id
5250                            .or(latest_folded_buffer_id)
5251                            .and_then(|toggled_buffer_id| {
5252                                outline_panel.fs_entries.iter().find_map(
5253                                    |fs_entry| match fs_entry {
5254                                        FsEntry::ExternalFile(external) => {
5255                                            if external.buffer_id == toggled_buffer_id {
5256                                                Some(fs_entry.clone())
5257                                            } else {
5258                                                None
5259                                            }
5260                                        }
5261                                        FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5262                                            if *buffer_id == toggled_buffer_id {
5263                                                Some(fs_entry.clone())
5264                                            } else {
5265                                                None
5266                                            }
5267                                        }
5268                                        FsEntry::Directory(..) => None,
5269                                    },
5270                                )
5271                            })
5272                            .map(PanelEntry::Fs)
5273                    {
5274                        outline_panel.select_entry(entry_to_select, true, window, cx);
5275                    }
5276
5277                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5278                }
5279                EditorEvent::Reparsed(buffer_id) => {
5280                    if let Some(buffer) = outline_panel.buffers.get_mut(buffer_id) {
5281                        buffer.invalidate_outlines();
5282                    }
5283                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5284                    if update_cached_items {
5285                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5286                    }
5287                }
5288                EditorEvent::OutlineSymbolsChanged => {
5289                    for buffer in outline_panel.buffers.values_mut() {
5290                        buffer.invalidate_outlines();
5291                    }
5292                    if matches!(
5293                        outline_panel.selected_entry(),
5294                        Some(PanelEntry::Outline(..)),
5295                    ) {
5296                        outline_panel.selected_entry.invalidate();
5297                    }
5298                    if outline_panel.update_non_fs_items(window, cx) {
5299                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5300                    }
5301                }
5302                EditorEvent::TitleChanged => {
5303                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5304                }
5305                _ => {}
5306            }
5307        },
5308    )
5309}
5310
5311fn empty_icon() -> AnyElement {
5312    h_flex()
5313        .size(IconSize::default().rems())
5314        .invisible()
5315        .flex_none()
5316        .into_any_element()
5317}
5318
5319#[derive(Debug, Default)]
5320struct GenerationState {
5321    entries: Vec<CachedEntry>,
5322    match_candidates: Vec<StringMatchCandidate>,
5323    max_width_estimate_and_index: Option<(u64, usize)>,
5324}
5325
5326impl GenerationState {
5327    fn clear(&mut self) {
5328        self.entries.clear();
5329        self.match_candidates.clear();
5330        self.max_width_estimate_and_index = None;
5331    }
5332}
5333
5334#[cfg(test)]
5335mod tests {
5336    use db::indoc;
5337    use futures::stream::StreamExt as _;
5338    use gpui::{TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle};
5339    use language::{self, FakeLspAdapter, markdown_lang, rust_lang};
5340    use pretty_assertions::assert_eq;
5341    use project::FakeFs;
5342    use search::{
5343        buffer_search,
5344        project_search::{self, perform_project_search},
5345    };
5346    use serde_json::json;
5347    use util::path;
5348    use workspace::{MultiWorkspace, OpenOptions, OpenVisible, ToolbarItemView};
5349
5350    use super::*;
5351
5352    const SELECTED_MARKER: &str = "  <==== selected";
5353
5354    #[gpui::test(iterations = 10)]
5355    async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5356        init_test(cx);
5357
5358        let fs = FakeFs::new(cx.background_executor.clone());
5359        let root = path!("/rust-analyzer");
5360        populate_with_test_ra_project(&fs, root).await;
5361        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5362        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
5363        let (window, workspace) = add_outline_panel(&project, cx).await;
5364        let cx = &mut VisualTestContext::from_window(window.into(), cx);
5365        let outline_panel = outline_panel(&workspace, cx);
5366        outline_panel.update_in(cx, |outline_panel, window, cx| {
5367            outline_panel.set_active(true, window, cx)
5368        });
5369
5370        workspace.update_in(cx, |workspace, window, cx| {
5371            ProjectSearchView::deploy_search(
5372                workspace,
5373                &workspace::DeploySearch::default(),
5374                window,
5375                cx,
5376            )
5377        });
5378        let search_view = workspace.update_in(cx, |workspace, _window, cx| {
5379            workspace
5380                .active_pane()
5381                .read(cx)
5382                .items()
5383                .find_map(|item| item.downcast::<ProjectSearchView>())
5384                .expect("Project search view expected to appear after new search event trigger")
5385        });
5386
5387        let query = "param_names_for_lifetime_elision_hints";
5388        perform_project_search(&search_view, query, cx);
5389        search_view.update(cx, |search_view, cx| {
5390            search_view
5391                .results_editor()
5392                .update(cx, |results_editor, cx| {
5393                    assert_eq!(
5394                        results_editor.display_text(cx).match_indices(query).count(),
5395                        9
5396                    );
5397                });
5398        });
5399
5400        let all_matches = r#"rust-analyzer/
5401  crates/
5402    ide/src/
5403      inlay_hints/
5404        fn_lifetime_fn.rs
5405          search: match config.«param_names_for_lifetime_elision_hints» {
5406          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5407          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5408          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5409      inlay_hints.rs
5410        search: pub «param_names_for_lifetime_elision_hints»: bool,
5411        search: «param_names_for_lifetime_elision_hints»: self
5412      static_index.rs
5413        search: «param_names_for_lifetime_elision_hints»: false,
5414    rust-analyzer/src/
5415      cli/
5416        analysis_stats.rs
5417          search: «param_names_for_lifetime_elision_hints»: true,
5418      config.rs
5419        search: «param_names_for_lifetime_elision_hints»: self"#
5420            .to_string();
5421
5422        let select_first_in_all_matches = |line_to_select: &str| {
5423            assert!(
5424                all_matches.contains(line_to_select),
5425                "`{line_to_select}` was not found in all matches `{all_matches}`"
5426            );
5427            all_matches.replacen(
5428                line_to_select,
5429                &format!("{line_to_select}{SELECTED_MARKER}"),
5430                1,
5431            )
5432        };
5433
5434        cx.executor()
5435            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5436        cx.run_until_parked();
5437        outline_panel.update(cx, |outline_panel, cx| {
5438            assert_eq!(
5439                display_entries(
5440                    &project,
5441                    &snapshot(outline_panel, cx),
5442                    &outline_panel.cached_entries,
5443                    outline_panel.selected_entry(),
5444                    cx,
5445                ),
5446                select_first_in_all_matches(
5447                    "search: match config.«param_names_for_lifetime_elision_hints» {"
5448                )
5449            );
5450        });
5451
5452        outline_panel.update_in(cx, |outline_panel, window, cx| {
5453            outline_panel.select_parent(&SelectParent, window, cx);
5454            assert_eq!(
5455                display_entries(
5456                    &project,
5457                    &snapshot(outline_panel, cx),
5458                    &outline_panel.cached_entries,
5459                    outline_panel.selected_entry(),
5460                    cx,
5461                ),
5462                select_first_in_all_matches("fn_lifetime_fn.rs")
5463            );
5464        });
5465        outline_panel.update_in(cx, |outline_panel, window, cx| {
5466            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5467        });
5468        cx.executor()
5469            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5470        cx.run_until_parked();
5471        outline_panel.update(cx, |outline_panel, cx| {
5472            assert_eq!(
5473                display_entries(
5474                    &project,
5475                    &snapshot(outline_panel, cx),
5476                    &outline_panel.cached_entries,
5477                    outline_panel.selected_entry(),
5478                    cx,
5479                ),
5480                format!(
5481                    r#"rust-analyzer/
5482  crates/
5483    ide/src/
5484      inlay_hints/
5485        fn_lifetime_fn.rs{SELECTED_MARKER}
5486      inlay_hints.rs
5487        search: pub «param_names_for_lifetime_elision_hints»: bool,
5488        search: «param_names_for_lifetime_elision_hints»: self
5489      static_index.rs
5490        search: «param_names_for_lifetime_elision_hints»: false,
5491    rust-analyzer/src/
5492      cli/
5493        analysis_stats.rs
5494          search: «param_names_for_lifetime_elision_hints»: true,
5495      config.rs
5496        search: «param_names_for_lifetime_elision_hints»: self"#,
5497                )
5498            );
5499        });
5500
5501        outline_panel.update_in(cx, |outline_panel, window, cx| {
5502            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5503        });
5504        cx.executor()
5505            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5506        cx.run_until_parked();
5507        outline_panel.update_in(cx, |outline_panel, window, cx| {
5508            outline_panel.select_parent(&SelectParent, window, cx);
5509            assert_eq!(
5510                display_entries(
5511                    &project,
5512                    &snapshot(outline_panel, cx),
5513                    &outline_panel.cached_entries,
5514                    outline_panel.selected_entry(),
5515                    cx,
5516                ),
5517                select_first_in_all_matches("inlay_hints/")
5518            );
5519        });
5520
5521        outline_panel.update_in(cx, |outline_panel, window, cx| {
5522            outline_panel.select_parent(&SelectParent, window, cx);
5523            assert_eq!(
5524                display_entries(
5525                    &project,
5526                    &snapshot(outline_panel, cx),
5527                    &outline_panel.cached_entries,
5528                    outline_panel.selected_entry(),
5529                    cx,
5530                ),
5531                select_first_in_all_matches("ide/src/")
5532            );
5533        });
5534
5535        outline_panel.update_in(cx, |outline_panel, window, cx| {
5536            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5537        });
5538        cx.executor()
5539            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5540        cx.run_until_parked();
5541        outline_panel.update(cx, |outline_panel, cx| {
5542            assert_eq!(
5543                display_entries(
5544                    &project,
5545                    &snapshot(outline_panel, cx),
5546                    &outline_panel.cached_entries,
5547                    outline_panel.selected_entry(),
5548                    cx,
5549                ),
5550                format!(
5551                    r#"rust-analyzer/
5552  crates/
5553    ide/src/{SELECTED_MARKER}
5554    rust-analyzer/src/
5555      cli/
5556        analysis_stats.rs
5557          search: «param_names_for_lifetime_elision_hints»: true,
5558      config.rs
5559        search: «param_names_for_lifetime_elision_hints»: self"#,
5560                )
5561            );
5562        });
5563        outline_panel.update_in(cx, |outline_panel, window, cx| {
5564            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5565        });
5566        cx.executor()
5567            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5568        cx.run_until_parked();
5569        outline_panel.update(cx, |outline_panel, cx| {
5570            assert_eq!(
5571                display_entries(
5572                    &project,
5573                    &snapshot(outline_panel, cx),
5574                    &outline_panel.cached_entries,
5575                    outline_panel.selected_entry(),
5576                    cx,
5577                ),
5578                select_first_in_all_matches("ide/src/")
5579            );
5580        });
5581    }
5582
5583    #[gpui::test(iterations = 10)]
5584    async fn test_item_filtering(cx: &mut TestAppContext) {
5585        init_test(cx);
5586
5587        let fs = FakeFs::new(cx.background_executor.clone());
5588        let root = path!("/rust-analyzer");
5589        populate_with_test_ra_project(&fs, root).await;
5590        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5591        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
5592        let (window, workspace) = add_outline_panel(&project, cx).await;
5593        let cx = &mut VisualTestContext::from_window(window.into(), cx);
5594        let outline_panel = outline_panel(&workspace, cx);
5595        outline_panel.update_in(cx, |outline_panel, window, cx| {
5596            outline_panel.set_active(true, window, cx)
5597        });
5598
5599        workspace.update_in(cx, |workspace, window, cx| {
5600            ProjectSearchView::deploy_search(
5601                workspace,
5602                &workspace::DeploySearch::default(),
5603                window,
5604                cx,
5605            )
5606        });
5607        let search_view = workspace.update_in(cx, |workspace, _window, cx| {
5608            workspace
5609                .active_pane()
5610                .read(cx)
5611                .items()
5612                .find_map(|item| item.downcast::<ProjectSearchView>())
5613                .expect("Project search view expected to appear after new search event trigger")
5614        });
5615
5616        let query = "param_names_for_lifetime_elision_hints";
5617        perform_project_search(&search_view, query, cx);
5618        search_view.update(cx, |search_view, cx| {
5619            search_view
5620                .results_editor()
5621                .update(cx, |results_editor, cx| {
5622                    assert_eq!(
5623                        results_editor.display_text(cx).match_indices(query).count(),
5624                        9
5625                    );
5626                });
5627        });
5628        let all_matches = r#"rust-analyzer/
5629  crates/
5630    ide/src/
5631      inlay_hints/
5632        fn_lifetime_fn.rs
5633          search: match config.«param_names_for_lifetime_elision_hints» {
5634          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5635          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5636          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5637      inlay_hints.rs
5638        search: pub «param_names_for_lifetime_elision_hints»: bool,
5639        search: «param_names_for_lifetime_elision_hints»: self
5640      static_index.rs
5641        search: «param_names_for_lifetime_elision_hints»: false,
5642    rust-analyzer/src/
5643      cli/
5644        analysis_stats.rs
5645          search: «param_names_for_lifetime_elision_hints»: true,
5646      config.rs
5647        search: «param_names_for_lifetime_elision_hints»: self"#
5648            .to_string();
5649
5650        cx.executor()
5651            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5652        cx.run_until_parked();
5653        outline_panel.update(cx, |outline_panel, cx| {
5654            assert_eq!(
5655                display_entries(
5656                    &project,
5657                    &snapshot(outline_panel, cx),
5658                    &outline_panel.cached_entries,
5659                    None,
5660                    cx,
5661                ),
5662                all_matches,
5663            );
5664        });
5665
5666        let filter_text = "a";
5667        outline_panel.update_in(cx, |outline_panel, window, cx| {
5668            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5669                filter_editor.set_text(filter_text, window, cx);
5670            });
5671        });
5672        cx.executor()
5673            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5674        cx.run_until_parked();
5675
5676        outline_panel.update(cx, |outline_panel, cx| {
5677            assert_eq!(
5678                display_entries(
5679                    &project,
5680                    &snapshot(outline_panel, cx),
5681                    &outline_panel.cached_entries,
5682                    None,
5683                    cx,
5684                ),
5685                all_matches
5686                    .lines()
5687                    .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5688                    .filter(|item| item.contains(filter_text))
5689                    .collect::<Vec<_>>()
5690                    .join("\n"),
5691            );
5692        });
5693
5694        outline_panel.update_in(cx, |outline_panel, window, cx| {
5695            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5696                filter_editor.set_text("", window, cx);
5697            });
5698        });
5699        cx.executor()
5700            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5701        cx.run_until_parked();
5702        outline_panel.update(cx, |outline_panel, cx| {
5703            assert_eq!(
5704                display_entries(
5705                    &project,
5706                    &snapshot(outline_panel, cx),
5707                    &outline_panel.cached_entries,
5708                    None,
5709                    cx,
5710                ),
5711                all_matches,
5712            );
5713        });
5714    }
5715
5716    #[gpui::test(iterations = 10)]
5717    async fn test_item_opening(cx: &mut TestAppContext) {
5718        init_test(cx);
5719
5720        let fs = FakeFs::new(cx.background_executor.clone());
5721        let root = path!("/rust-analyzer");
5722        populate_with_test_ra_project(&fs, root).await;
5723        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5724        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
5725        let (window, workspace) = add_outline_panel(&project, cx).await;
5726        let cx = &mut VisualTestContext::from_window(window.into(), cx);
5727        let outline_panel = outline_panel(&workspace, cx);
5728        outline_panel.update_in(cx, |outline_panel, window, cx| {
5729            outline_panel.set_active(true, window, cx)
5730        });
5731
5732        workspace.update_in(cx, |workspace, window, cx| {
5733            ProjectSearchView::deploy_search(
5734                workspace,
5735                &workspace::DeploySearch::default(),
5736                window,
5737                cx,
5738            )
5739        });
5740        let search_view = workspace.update_in(cx, |workspace, _window, cx| {
5741            workspace
5742                .active_pane()
5743                .read(cx)
5744                .items()
5745                .find_map(|item| item.downcast::<ProjectSearchView>())
5746                .expect("Project search view expected to appear after new search event trigger")
5747        });
5748
5749        let query = "param_names_for_lifetime_elision_hints";
5750        perform_project_search(&search_view, query, cx);
5751        search_view.update(cx, |search_view, cx| {
5752            search_view
5753                .results_editor()
5754                .update(cx, |results_editor, cx| {
5755                    assert_eq!(
5756                        results_editor.display_text(cx).match_indices(query).count(),
5757                        9
5758                    );
5759                });
5760        });
5761        let all_matches = r#"rust-analyzer/
5762  crates/
5763    ide/src/
5764      inlay_hints/
5765        fn_lifetime_fn.rs
5766          search: match config.«param_names_for_lifetime_elision_hints» {
5767          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5768          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5769          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5770      inlay_hints.rs
5771        search: pub «param_names_for_lifetime_elision_hints»: bool,
5772        search: «param_names_for_lifetime_elision_hints»: self
5773      static_index.rs
5774        search: «param_names_for_lifetime_elision_hints»: false,
5775    rust-analyzer/src/
5776      cli/
5777        analysis_stats.rs
5778          search: «param_names_for_lifetime_elision_hints»: true,
5779      config.rs
5780        search: «param_names_for_lifetime_elision_hints»: self"#
5781            .to_string();
5782        let select_first_in_all_matches = |line_to_select: &str| {
5783            assert!(
5784                all_matches.contains(line_to_select),
5785                "`{line_to_select}` was not found in all matches `{all_matches}`"
5786            );
5787            all_matches.replacen(
5788                line_to_select,
5789                &format!("{line_to_select}{SELECTED_MARKER}"),
5790                1,
5791            )
5792        };
5793        let clear_outline_metadata = |input: &str| {
5794            input
5795                .replace("search: ", "")
5796                .replace("«", "")
5797                .replace("»", "")
5798        };
5799
5800        cx.executor()
5801            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5802        cx.run_until_parked();
5803
5804        let active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5805            outline_panel
5806                .active_editor()
5807                .expect("should have an active editor open")
5808        });
5809        let initial_outline_selection =
5810            "search: match config.«param_names_for_lifetime_elision_hints» {";
5811        outline_panel.update_in(cx, |outline_panel, window, cx| {
5812            assert_eq!(
5813                display_entries(
5814                    &project,
5815                    &snapshot(outline_panel, cx),
5816                    &outline_panel.cached_entries,
5817                    outline_panel.selected_entry(),
5818                    cx,
5819                ),
5820                select_first_in_all_matches(initial_outline_selection)
5821            );
5822            assert_eq!(
5823                selected_row_text(&active_editor, cx),
5824                clear_outline_metadata(initial_outline_selection),
5825                "Should place the initial editor selection on the corresponding search result"
5826            );
5827
5828            outline_panel.select_next(&SelectNext, window, cx);
5829            outline_panel.select_next(&SelectNext, window, cx);
5830        });
5831
5832        let navigated_outline_selection =
5833            "search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {";
5834        outline_panel.update(cx, |outline_panel, cx| {
5835            assert_eq!(
5836                display_entries(
5837                    &project,
5838                    &snapshot(outline_panel, cx),
5839                    &outline_panel.cached_entries,
5840                    outline_panel.selected_entry(),
5841                    cx,
5842                ),
5843                select_first_in_all_matches(navigated_outline_selection)
5844            );
5845        });
5846        cx.executor()
5847            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5848        outline_panel.update(cx, |_, cx| {
5849            assert_eq!(
5850                selected_row_text(&active_editor, cx),
5851                clear_outline_metadata(navigated_outline_selection),
5852                "Should still have the initial caret position after SelectNext calls"
5853            );
5854        });
5855
5856        outline_panel.update_in(cx, |outline_panel, window, cx| {
5857            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5858        });
5859        outline_panel.update(cx, |_outline_panel, cx| {
5860            assert_eq!(
5861                selected_row_text(&active_editor, cx),
5862                clear_outline_metadata(navigated_outline_selection),
5863                "After opening, should move the caret to the opened outline entry's position"
5864            );
5865        });
5866
5867        outline_panel.update_in(cx, |outline_panel, window, cx| {
5868            outline_panel.select_next(&SelectNext, window, cx);
5869        });
5870        let next_navigated_outline_selection = "search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },";
5871        outline_panel.update(cx, |outline_panel, cx| {
5872            assert_eq!(
5873                display_entries(
5874                    &project,
5875                    &snapshot(outline_panel, cx),
5876                    &outline_panel.cached_entries,
5877                    outline_panel.selected_entry(),
5878                    cx,
5879                ),
5880                select_first_in_all_matches(next_navigated_outline_selection)
5881            );
5882        });
5883        cx.executor()
5884            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5885        outline_panel.update(cx, |_outline_panel, cx| {
5886            assert_eq!(
5887                selected_row_text(&active_editor, cx),
5888                clear_outline_metadata(next_navigated_outline_selection),
5889                "Should again preserve the selection after another SelectNext call"
5890            );
5891        });
5892
5893        outline_panel.update_in(cx, |outline_panel, window, cx| {
5894            outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx);
5895        });
5896        cx.executor()
5897            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5898        cx.run_until_parked();
5899        let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5900            outline_panel
5901                .active_editor()
5902                .expect("should have an active editor open")
5903        });
5904        outline_panel.update(cx, |outline_panel, cx| {
5905            assert_ne!(
5906                active_editor, new_active_editor,
5907                "After opening an excerpt, new editor should be open"
5908            );
5909            assert_eq!(
5910                display_entries(
5911                    &project,
5912                    &snapshot(outline_panel, cx),
5913                    &outline_panel.cached_entries,
5914                    outline_panel.selected_entry(),
5915                    cx,
5916                ),
5917                "outline: pub(super) fn hints
5918outline: fn hints_lifetimes_named  <==== selected"
5919            );
5920            assert_eq!(
5921                selected_row_text(&new_active_editor, cx),
5922                clear_outline_metadata(next_navigated_outline_selection),
5923                "When opening the excerpt, should navigate to the place corresponding the outline entry"
5924            );
5925        });
5926    }
5927
5928    #[gpui::test]
5929    async fn test_multiple_worktrees(cx: &mut TestAppContext) {
5930        init_test(cx);
5931
5932        let fs = FakeFs::new(cx.background_executor.clone());
5933        fs.insert_tree(
5934            path!("/root"),
5935            json!({
5936                "one": {
5937                    "a.txt": "aaa aaa"
5938                },
5939                "two": {
5940                    "b.txt": "a aaa"
5941                }
5942
5943            }),
5944        )
5945        .await;
5946        let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await;
5947        let (window, workspace) = add_outline_panel(&project, cx).await;
5948        let cx = &mut VisualTestContext::from_window(window.into(), cx);
5949        let outline_panel = outline_panel(&workspace, cx);
5950        outline_panel.update_in(cx, |outline_panel, window, cx| {
5951            outline_panel.set_active(true, window, cx)
5952        });
5953
5954        let items = workspace
5955            .update_in(cx, |workspace, window, cx| {
5956                workspace.open_paths(
5957                    vec![PathBuf::from(path!("/root/two"))],
5958                    OpenOptions {
5959                        visible: Some(OpenVisible::OnlyDirectories),
5960                        ..Default::default()
5961                    },
5962                    None,
5963                    window,
5964                    cx,
5965                )
5966            })
5967            .await;
5968        assert_eq!(items.len(), 1, "Were opening another worktree directory");
5969        assert!(
5970            items[0].is_none(),
5971            "Directory should be opened successfully"
5972        );
5973
5974        workspace.update_in(cx, |workspace, window, cx| {
5975            ProjectSearchView::deploy_search(
5976                workspace,
5977                &workspace::DeploySearch::default(),
5978                window,
5979                cx,
5980            )
5981        });
5982        let search_view = workspace.update_in(cx, |workspace, _window, cx| {
5983            workspace
5984                .active_pane()
5985                .read(cx)
5986                .items()
5987                .find_map(|item| item.downcast::<ProjectSearchView>())
5988                .expect("Project search view expected to appear after new search event trigger")
5989        });
5990
5991        let query = "aaa";
5992        perform_project_search(&search_view, query, cx);
5993        search_view.update(cx, |search_view, cx| {
5994            search_view
5995                .results_editor()
5996                .update(cx, |results_editor, cx| {
5997                    assert_eq!(
5998                        results_editor.display_text(cx).match_indices(query).count(),
5999                        3
6000                    );
6001                });
6002        });
6003
6004        cx.executor()
6005            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6006        cx.run_until_parked();
6007        outline_panel.update(cx, |outline_panel, cx| {
6008            assert_eq!(
6009                display_entries(
6010                    &project,
6011                    &snapshot(outline_panel, cx),
6012                    &outline_panel.cached_entries,
6013                    outline_panel.selected_entry(),
6014                    cx,
6015                ),
6016                format!(
6017                    r#"one/
6018  a.txt
6019    search: «aaa» aaa  <==== selected
6020    search: aaa «aaa»
6021two/
6022  b.txt
6023    search: a «aaa»"#,
6024                ),
6025            );
6026        });
6027
6028        outline_panel.update_in(cx, |outline_panel, window, cx| {
6029            outline_panel.select_previous(&SelectPrevious, window, cx);
6030            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6031        });
6032        cx.executor()
6033            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6034        cx.run_until_parked();
6035        outline_panel.update(cx, |outline_panel, cx| {
6036            assert_eq!(
6037                display_entries(
6038                    &project,
6039                    &snapshot(outline_panel, cx),
6040                    &outline_panel.cached_entries,
6041                    outline_panel.selected_entry(),
6042                    cx,
6043                ),
6044                format!(
6045                    r#"one/
6046  a.txt  <==== selected
6047two/
6048  b.txt
6049    search: a «aaa»"#,
6050                ),
6051            );
6052        });
6053
6054        outline_panel.update_in(cx, |outline_panel, window, cx| {
6055            outline_panel.select_next(&SelectNext, window, cx);
6056            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6057        });
6058        cx.executor()
6059            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6060        cx.run_until_parked();
6061        outline_panel.update(cx, |outline_panel, cx| {
6062            assert_eq!(
6063                display_entries(
6064                    &project,
6065                    &snapshot(outline_panel, cx),
6066                    &outline_panel.cached_entries,
6067                    outline_panel.selected_entry(),
6068                    cx,
6069                ),
6070                format!(
6071                    r#"one/
6072  a.txt
6073two/  <==== selected"#,
6074                ),
6075            );
6076        });
6077
6078        outline_panel.update_in(cx, |outline_panel, window, cx| {
6079            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
6080        });
6081        cx.executor()
6082            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6083        cx.run_until_parked();
6084        outline_panel.update(cx, |outline_panel, cx| {
6085            assert_eq!(
6086                display_entries(
6087                    &project,
6088                    &snapshot(outline_panel, cx),
6089                    &outline_panel.cached_entries,
6090                    outline_panel.selected_entry(),
6091                    cx,
6092                ),
6093                format!(
6094                    r#"one/
6095  a.txt
6096two/  <==== selected
6097  b.txt
6098    search: a «aaa»"#,
6099                )
6100            );
6101        });
6102    }
6103
6104    #[gpui::test]
6105    async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
6106        init_test(cx);
6107
6108        let root = path!("/root");
6109        let fs = FakeFs::new(cx.background_executor.clone());
6110        fs.insert_tree(
6111            root,
6112            json!({
6113                "src": {
6114                    "lib.rs": indoc!("
6115#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6116struct OutlineEntryExcerpt {
6117    id: ExcerptId,
6118    buffer_id: BufferId,
6119    range: ExcerptRange<language::Anchor>,
6120}"),
6121                }
6122            }),
6123        )
6124        .await;
6125        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6126        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
6127        let (window, workspace) = add_outline_panel(&project, cx).await;
6128        let cx = &mut VisualTestContext::from_window(window.into(), cx);
6129        let outline_panel = outline_panel(&workspace, cx);
6130        cx.update(|window, cx| {
6131            outline_panel.update(cx, |outline_panel, cx| {
6132                outline_panel.set_active(true, window, cx)
6133            });
6134        });
6135
6136        let _editor = workspace
6137            .update_in(cx, |workspace, window, cx| {
6138                workspace.open_abs_path(
6139                    PathBuf::from(path!("/root/src/lib.rs")),
6140                    OpenOptions {
6141                        visible: Some(OpenVisible::All),
6142                        ..Default::default()
6143                    },
6144                    window,
6145                    cx,
6146                )
6147            })
6148            .await
6149            .expect("Failed to open Rust source file")
6150            .downcast::<Editor>()
6151            .expect("Should open an editor for Rust source file");
6152
6153        cx.executor()
6154            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6155        cx.run_until_parked();
6156        outline_panel.update(cx, |outline_panel, cx| {
6157            assert_eq!(
6158                display_entries(
6159                    &project,
6160                    &snapshot(outline_panel, cx),
6161                    &outline_panel.cached_entries,
6162                    outline_panel.selected_entry(),
6163                    cx,
6164                ),
6165                indoc!(
6166                    "
6167outline: struct OutlineEntryExcerpt
6168  outline: id
6169  outline: buffer_id
6170  outline: range"
6171                )
6172            );
6173        });
6174
6175        cx.update(|window, cx| {
6176            outline_panel.update(cx, |outline_panel, cx| {
6177                outline_panel.select_next(&SelectNext, window, cx);
6178            });
6179        });
6180        cx.executor()
6181            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6182        cx.run_until_parked();
6183        outline_panel.update(cx, |outline_panel, cx| {
6184            assert_eq!(
6185                display_entries(
6186                    &project,
6187                    &snapshot(outline_panel, cx),
6188                    &outline_panel.cached_entries,
6189                    outline_panel.selected_entry(),
6190                    cx,
6191                ),
6192                indoc!(
6193                    "
6194outline: struct OutlineEntryExcerpt  <==== selected
6195  outline: id
6196  outline: buffer_id
6197  outline: range"
6198                )
6199            );
6200        });
6201
6202        cx.update(|window, cx| {
6203            outline_panel.update(cx, |outline_panel, cx| {
6204                outline_panel.select_next(&SelectNext, window, cx);
6205            });
6206        });
6207        cx.executor()
6208            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6209        cx.run_until_parked();
6210        outline_panel.update(cx, |outline_panel, cx| {
6211            assert_eq!(
6212                display_entries(
6213                    &project,
6214                    &snapshot(outline_panel, cx),
6215                    &outline_panel.cached_entries,
6216                    outline_panel.selected_entry(),
6217                    cx,
6218                ),
6219                indoc!(
6220                    "
6221outline: struct OutlineEntryExcerpt
6222  outline: id  <==== selected
6223  outline: buffer_id
6224  outline: range"
6225                )
6226            );
6227        });
6228
6229        cx.update(|window, cx| {
6230            outline_panel.update(cx, |outline_panel, cx| {
6231                outline_panel.select_next(&SelectNext, window, cx);
6232            });
6233        });
6234        cx.executor()
6235            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6236        cx.run_until_parked();
6237        outline_panel.update(cx, |outline_panel, cx| {
6238            assert_eq!(
6239                display_entries(
6240                    &project,
6241                    &snapshot(outline_panel, cx),
6242                    &outline_panel.cached_entries,
6243                    outline_panel.selected_entry(),
6244                    cx,
6245                ),
6246                indoc!(
6247                    "
6248outline: struct OutlineEntryExcerpt
6249  outline: id
6250  outline: buffer_id  <==== selected
6251  outline: range"
6252                )
6253            );
6254        });
6255
6256        cx.update(|window, cx| {
6257            outline_panel.update(cx, |outline_panel, cx| {
6258                outline_panel.select_next(&SelectNext, window, cx);
6259            });
6260        });
6261        cx.executor()
6262            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6263        cx.run_until_parked();
6264        outline_panel.update(cx, |outline_panel, cx| {
6265            assert_eq!(
6266                display_entries(
6267                    &project,
6268                    &snapshot(outline_panel, cx),
6269                    &outline_panel.cached_entries,
6270                    outline_panel.selected_entry(),
6271                    cx,
6272                ),
6273                indoc!(
6274                    "
6275outline: struct OutlineEntryExcerpt
6276  outline: id
6277  outline: buffer_id
6278  outline: range  <==== selected"
6279                )
6280            );
6281        });
6282
6283        cx.update(|window, cx| {
6284            outline_panel.update(cx, |outline_panel, cx| {
6285                outline_panel.select_next(&SelectNext, window, cx);
6286            });
6287        });
6288        cx.executor()
6289            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6290        cx.run_until_parked();
6291        outline_panel.update(cx, |outline_panel, cx| {
6292            assert_eq!(
6293                display_entries(
6294                    &project,
6295                    &snapshot(outline_panel, cx),
6296                    &outline_panel.cached_entries,
6297                    outline_panel.selected_entry(),
6298                    cx,
6299                ),
6300                indoc!(
6301                    "
6302outline: struct OutlineEntryExcerpt  <==== selected
6303  outline: id
6304  outline: buffer_id
6305  outline: range"
6306                )
6307            );
6308        });
6309
6310        cx.update(|window, cx| {
6311            outline_panel.update(cx, |outline_panel, cx| {
6312                outline_panel.select_previous(&SelectPrevious, window, cx);
6313            });
6314        });
6315        cx.executor()
6316            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6317        cx.run_until_parked();
6318        outline_panel.update(cx, |outline_panel, cx| {
6319            assert_eq!(
6320                display_entries(
6321                    &project,
6322                    &snapshot(outline_panel, cx),
6323                    &outline_panel.cached_entries,
6324                    outline_panel.selected_entry(),
6325                    cx,
6326                ),
6327                indoc!(
6328                    "
6329outline: struct OutlineEntryExcerpt
6330  outline: id
6331  outline: buffer_id
6332  outline: range  <==== selected"
6333                )
6334            );
6335        });
6336
6337        cx.update(|window, cx| {
6338            outline_panel.update(cx, |outline_panel, cx| {
6339                outline_panel.select_previous(&SelectPrevious, window, cx);
6340            });
6341        });
6342        cx.executor()
6343            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6344        cx.run_until_parked();
6345        outline_panel.update(cx, |outline_panel, cx| {
6346            assert_eq!(
6347                display_entries(
6348                    &project,
6349                    &snapshot(outline_panel, cx),
6350                    &outline_panel.cached_entries,
6351                    outline_panel.selected_entry(),
6352                    cx,
6353                ),
6354                indoc!(
6355                    "
6356outline: struct OutlineEntryExcerpt
6357  outline: id
6358  outline: buffer_id  <==== selected
6359  outline: range"
6360                )
6361            );
6362        });
6363
6364        cx.update(|window, cx| {
6365            outline_panel.update(cx, |outline_panel, cx| {
6366                outline_panel.select_previous(&SelectPrevious, window, cx);
6367            });
6368        });
6369        cx.executor()
6370            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6371        cx.run_until_parked();
6372        outline_panel.update(cx, |outline_panel, cx| {
6373            assert_eq!(
6374                display_entries(
6375                    &project,
6376                    &snapshot(outline_panel, cx),
6377                    &outline_panel.cached_entries,
6378                    outline_panel.selected_entry(),
6379                    cx,
6380                ),
6381                indoc!(
6382                    "
6383outline: struct OutlineEntryExcerpt
6384  outline: id  <==== selected
6385  outline: buffer_id
6386  outline: range"
6387                )
6388            );
6389        });
6390
6391        cx.update(|window, cx| {
6392            outline_panel.update(cx, |outline_panel, cx| {
6393                outline_panel.select_previous(&SelectPrevious, window, cx);
6394            });
6395        });
6396        cx.executor()
6397            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6398        cx.run_until_parked();
6399        outline_panel.update(cx, |outline_panel, cx| {
6400            assert_eq!(
6401                display_entries(
6402                    &project,
6403                    &snapshot(outline_panel, cx),
6404                    &outline_panel.cached_entries,
6405                    outline_panel.selected_entry(),
6406                    cx,
6407                ),
6408                indoc!(
6409                    "
6410outline: struct OutlineEntryExcerpt  <==== selected
6411  outline: id
6412  outline: buffer_id
6413  outline: range"
6414                )
6415            );
6416        });
6417
6418        cx.update(|window, cx| {
6419            outline_panel.update(cx, |outline_panel, cx| {
6420                outline_panel.select_previous(&SelectPrevious, window, cx);
6421            });
6422        });
6423        cx.executor()
6424            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6425        cx.run_until_parked();
6426        outline_panel.update(cx, |outline_panel, cx| {
6427            assert_eq!(
6428                display_entries(
6429                    &project,
6430                    &snapshot(outline_panel, cx),
6431                    &outline_panel.cached_entries,
6432                    outline_panel.selected_entry(),
6433                    cx,
6434                ),
6435                indoc!(
6436                    "
6437outline: struct OutlineEntryExcerpt
6438  outline: id
6439  outline: buffer_id
6440  outline: range  <==== selected"
6441                )
6442            );
6443        });
6444    }
6445
6446    #[gpui::test(iterations = 10)]
6447    async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6448        init_test(cx);
6449
6450        let root = path!("/frontend-project");
6451        let fs = FakeFs::new(cx.background_executor.clone());
6452        fs.insert_tree(
6453            root,
6454            json!({
6455                "public": {
6456                    "lottie": {
6457                        "syntax-tree.json": r#"{ "something": "static" }"#
6458                    }
6459                },
6460                "src": {
6461                    "app": {
6462                        "(site)": {
6463                            "(about)": {
6464                                "jobs": {
6465                                    "[slug]": {
6466                                        "page.tsx": r#"static"#
6467                                    }
6468                                }
6469                            },
6470                            "(blog)": {
6471                                "post": {
6472                                    "[slug]": {
6473                                        "page.tsx": r#"static"#
6474                                    }
6475                                }
6476                            },
6477                        }
6478                    },
6479                    "components": {
6480                        "ErrorBoundary.tsx": r#"static"#,
6481                    }
6482                }
6483
6484            }),
6485        )
6486        .await;
6487        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6488        let (window, workspace) = add_outline_panel(&project, cx).await;
6489        let cx = &mut VisualTestContext::from_window(window.into(), cx);
6490        let outline_panel = outline_panel(&workspace, cx);
6491        outline_panel.update_in(cx, |outline_panel, window, cx| {
6492            outline_panel.set_active(true, window, cx)
6493        });
6494
6495        workspace.update_in(cx, |workspace, window, cx| {
6496            ProjectSearchView::deploy_search(
6497                workspace,
6498                &workspace::DeploySearch::default(),
6499                window,
6500                cx,
6501            )
6502        });
6503        let search_view = workspace.update_in(cx, |workspace, _window, cx| {
6504            workspace
6505                .active_pane()
6506                .read(cx)
6507                .items()
6508                .find_map(|item| item.downcast::<ProjectSearchView>())
6509                .expect("Project search view expected to appear after new search event trigger")
6510        });
6511
6512        let query = "static";
6513        perform_project_search(&search_view, query, cx);
6514        search_view.update(cx, |search_view, cx| {
6515            search_view
6516                .results_editor()
6517                .update(cx, |results_editor, cx| {
6518                    assert_eq!(
6519                        results_editor.display_text(cx).match_indices(query).count(),
6520                        4
6521                    );
6522                });
6523        });
6524
6525        cx.executor()
6526            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6527        cx.run_until_parked();
6528        outline_panel.update(cx, |outline_panel, cx| {
6529            assert_eq!(
6530                display_entries(
6531                    &project,
6532                    &snapshot(outline_panel, cx),
6533                    &outline_panel.cached_entries,
6534                    outline_panel.selected_entry(),
6535                    cx,
6536                ),
6537                format!(
6538                    r#"frontend-project/
6539  public/lottie/
6540    syntax-tree.json
6541      search: {{ "something": "«static»" }}  <==== selected
6542  src/
6543    app/(site)/
6544      (about)/jobs/[slug]/
6545        page.tsx
6546          search: «static»
6547      (blog)/post/[slug]/
6548        page.tsx
6549          search: «static»
6550    components/
6551      ErrorBoundary.tsx
6552        search: «static»"#
6553                )
6554            );
6555        });
6556
6557        outline_panel.update_in(cx, |outline_panel, window, cx| {
6558            // Move to 5th element in the list, 3 items down.
6559            for _ in 0..2 {
6560                outline_panel.select_next(&SelectNext, window, cx);
6561            }
6562            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6563        });
6564        cx.executor()
6565            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6566        cx.run_until_parked();
6567        outline_panel.update(cx, |outline_panel, cx| {
6568            assert_eq!(
6569                display_entries(
6570                    &project,
6571                    &snapshot(outline_panel, cx),
6572                    &outline_panel.cached_entries,
6573                    outline_panel.selected_entry(),
6574                    cx,
6575                ),
6576                format!(
6577                    r#"frontend-project/
6578  public/lottie/
6579    syntax-tree.json
6580      search: {{ "something": "«static»" }}
6581  src/
6582    app/(site)/  <==== selected
6583    components/
6584      ErrorBoundary.tsx
6585        search: «static»"#
6586                )
6587            );
6588        });
6589
6590        outline_panel.update_in(cx, |outline_panel, window, cx| {
6591            // Move to the next visible non-FS entry
6592            for _ in 0..3 {
6593                outline_panel.select_next(&SelectNext, window, cx);
6594            }
6595        });
6596        cx.run_until_parked();
6597        outline_panel.update(cx, |outline_panel, cx| {
6598            assert_eq!(
6599                display_entries(
6600                    &project,
6601                    &snapshot(outline_panel, cx),
6602                    &outline_panel.cached_entries,
6603                    outline_panel.selected_entry(),
6604                    cx,
6605                ),
6606                format!(
6607                    r#"frontend-project/
6608  public/lottie/
6609    syntax-tree.json
6610      search: {{ "something": "«static»" }}
6611  src/
6612    app/(site)/
6613    components/
6614      ErrorBoundary.tsx
6615        search: «static»  <==== selected"#
6616                )
6617            );
6618        });
6619
6620        outline_panel.update_in(cx, |outline_panel, window, cx| {
6621            outline_panel
6622                .active_editor()
6623                .expect("Should have an active editor")
6624                .update(cx, |editor, cx| {
6625                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6626                });
6627        });
6628        cx.executor()
6629            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6630        cx.run_until_parked();
6631        outline_panel.update(cx, |outline_panel, cx| {
6632            assert_eq!(
6633                display_entries(
6634                    &project,
6635                    &snapshot(outline_panel, cx),
6636                    &outline_panel.cached_entries,
6637                    outline_panel.selected_entry(),
6638                    cx,
6639                ),
6640                format!(
6641                    r#"frontend-project/
6642  public/lottie/
6643    syntax-tree.json
6644      search: {{ "something": "«static»" }}
6645  src/
6646    app/(site)/
6647    components/
6648      ErrorBoundary.tsx  <==== selected"#
6649                )
6650            );
6651        });
6652
6653        outline_panel.update_in(cx, |outline_panel, window, cx| {
6654            outline_panel
6655                .active_editor()
6656                .expect("Should have an active editor")
6657                .update(cx, |editor, cx| {
6658                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6659                });
6660        });
6661        cx.executor()
6662            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6663        cx.run_until_parked();
6664        outline_panel.update(cx, |outline_panel, cx| {
6665            assert_eq!(
6666                display_entries(
6667                    &project,
6668                    &snapshot(outline_panel, cx),
6669                    &outline_panel.cached_entries,
6670                    outline_panel.selected_entry(),
6671                    cx,
6672                ),
6673                format!(
6674                    r#"frontend-project/
6675  public/lottie/
6676    syntax-tree.json
6677      search: {{ "something": "«static»" }}
6678  src/
6679    app/(site)/
6680    components/
6681      ErrorBoundary.tsx  <==== selected
6682        search: «static»"#
6683                )
6684            );
6685        });
6686
6687        outline_panel.update_in(cx, |outline_panel, window, cx| {
6688            outline_panel.collapse_all_entries(&CollapseAllEntries, window, cx);
6689        });
6690        cx.executor()
6691            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6692        cx.run_until_parked();
6693        outline_panel.update(cx, |outline_panel, cx| {
6694            assert_eq!(
6695                display_entries(
6696                    &project,
6697                    &snapshot(outline_panel, cx),
6698                    &outline_panel.cached_entries,
6699                    outline_panel.selected_entry(),
6700                    cx,
6701                ),
6702                format!(r#"frontend-project/"#)
6703            );
6704        });
6705
6706        outline_panel.update_in(cx, |outline_panel, window, cx| {
6707            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
6708        });
6709        cx.executor()
6710            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6711        cx.run_until_parked();
6712        outline_panel.update(cx, |outline_panel, cx| {
6713            assert_eq!(
6714                display_entries(
6715                    &project,
6716                    &snapshot(outline_panel, cx),
6717                    &outline_panel.cached_entries,
6718                    outline_panel.selected_entry(),
6719                    cx,
6720                ),
6721                format!(
6722                    r#"frontend-project/
6723  public/lottie/
6724    syntax-tree.json
6725      search: {{ "something": "«static»" }}
6726  src/
6727    app/(site)/
6728      (about)/jobs/[slug]/
6729        page.tsx
6730          search: «static»
6731      (blog)/post/[slug]/
6732        page.tsx
6733          search: «static»
6734    components/
6735      ErrorBoundary.tsx  <==== selected
6736        search: «static»"#
6737                )
6738            );
6739        });
6740    }
6741
6742    async fn add_outline_panel(
6743        project: &Entity<Project>,
6744        cx: &mut TestAppContext,
6745    ) -> (WindowHandle<MultiWorkspace>, Entity<Workspace>) {
6746        let window =
6747            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6748        let workspace = window
6749            .read_with(cx, |mw, _| mw.workspace().clone())
6750            .unwrap();
6751
6752        let workspace_weak = workspace.downgrade();
6753        let outline_panel = window
6754            .update(cx, |_, window, cx| {
6755                cx.spawn_in(window, async move |_this, cx| {
6756                    OutlinePanel::load(workspace_weak, cx.clone()).await
6757                })
6758            })
6759            .unwrap()
6760            .await
6761            .expect("Failed to load outline panel");
6762
6763        window
6764            .update(cx, |multi_workspace, window, cx| {
6765                multi_workspace.workspace().update(cx, |workspace, cx| {
6766                    workspace.add_panel(outline_panel, window, cx);
6767                });
6768            })
6769            .unwrap();
6770        (window, workspace)
6771    }
6772
6773    fn outline_panel(
6774        workspace: &Entity<Workspace>,
6775        cx: &mut VisualTestContext,
6776    ) -> Entity<OutlinePanel> {
6777        workspace.update_in(cx, |workspace, _window, cx| {
6778            workspace
6779                .panel::<OutlinePanel>(cx)
6780                .expect("no outline panel")
6781        })
6782    }
6783
6784    fn display_entries(
6785        project: &Entity<Project>,
6786        multi_buffer_snapshot: &MultiBufferSnapshot,
6787        cached_entries: &[CachedEntry],
6788        selected_entry: Option<&PanelEntry>,
6789        cx: &mut App,
6790    ) -> String {
6791        let project = project.read(cx);
6792        let mut display_string = String::new();
6793        for entry in cached_entries {
6794            if !display_string.is_empty() {
6795                display_string += "\n";
6796            }
6797            for _ in 0..entry.depth {
6798                display_string += "  ";
6799            }
6800            display_string += &match &entry.entry {
6801                PanelEntry::Fs(entry) => match entry {
6802                    FsEntry::ExternalFile(_) => {
6803                        panic!("Did not cover external files with tests")
6804                    }
6805                    FsEntry::Directory(directory) => {
6806                        let path = if let Some(worktree) = project
6807                            .worktree_for_id(directory.worktree_id, cx)
6808                            .filter(|worktree| {
6809                                worktree.read(cx).root_entry() == Some(&directory.entry.entry)
6810                            }) {
6811                            worktree
6812                                .read(cx)
6813                                .root_name()
6814                                .join(&directory.entry.path)
6815                                .as_unix_str()
6816                                .to_string()
6817                        } else {
6818                            directory
6819                                .entry
6820                                .path
6821                                .file_name()
6822                                .unwrap_or_default()
6823                                .to_string()
6824                        };
6825                        format!("{path}/")
6826                    }
6827                    FsEntry::File(file) => file
6828                        .entry
6829                        .path
6830                        .file_name()
6831                        .map(|name| name.to_string())
6832                        .unwrap_or_default(),
6833                },
6834                PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6835                    .entries
6836                    .iter()
6837                    .filter_map(|dir| dir.path.file_name())
6838                    .map(|name| name.to_string() + "/")
6839                    .collect(),
6840                PanelEntry::Outline(outline_entry) => match outline_entry {
6841                    OutlineEntry::Excerpt(_) => continue,
6842                    OutlineEntry::Outline(outline_entry) => {
6843                        format!("outline: {}", outline_entry.text)
6844                    }
6845                },
6846                PanelEntry::Search(search_entry) => {
6847                    let search_data = search_entry.render_data.get_or_init(|| {
6848                        SearchData::new(&search_entry.match_range, multi_buffer_snapshot)
6849                    });
6850                    let mut search_result = String::new();
6851                    let mut last_end = 0;
6852                    for range in &search_data.search_match_indices {
6853                        search_result.push_str(&search_data.context_text[last_end..range.start]);
6854                        search_result.push('«');
6855                        search_result.push_str(&search_data.context_text[range.start..range.end]);
6856                        search_result.push('»');
6857                        last_end = range.end;
6858                    }
6859                    search_result.push_str(&search_data.context_text[last_end..]);
6860
6861                    format!("search: {search_result}")
6862                }
6863            };
6864
6865            if Some(&entry.entry) == selected_entry {
6866                display_string += SELECTED_MARKER;
6867            }
6868        }
6869        display_string
6870    }
6871
6872    fn init_test(cx: &mut TestAppContext) {
6873        cx.update(|cx| {
6874            let settings = SettingsStore::test(cx);
6875            cx.set_global(settings);
6876
6877            theme_settings::init(theme::LoadThemes::JustBase, cx);
6878
6879            editor::init(cx);
6880            project_search::init(cx);
6881            buffer_search::init(cx);
6882            super::init(cx);
6883        });
6884    }
6885
6886    // Based on https://github.com/rust-lang/rust-analyzer/
6887    async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6888        fs.insert_tree(
6889            root,
6890            json!({
6891                    "crates": {
6892                        "ide": {
6893                            "src": {
6894                                "inlay_hints": {
6895                                    "fn_lifetime_fn.rs": r##"
6896        pub(super) fn hints(
6897            acc: &mut Vec<InlayHint>,
6898            config: &InlayHintsConfig,
6899            func: ast::Fn,
6900        ) -> Option<()> {
6901            // ... snip
6902
6903            let mut used_names: FxHashMap<SmolStr, usize> =
6904                match config.param_names_for_lifetime_elision_hints {
6905                    true => generic_param_list
6906                        .iter()
6907                        .flat_map(|gpl| gpl.lifetime_params())
6908                        .filter_map(|param| param.lifetime())
6909                        .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6910                        .collect(),
6911                    false => Default::default(),
6912                };
6913            {
6914                let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6915                if self_param.is_some() && potential_lt_refs.next().is_some() {
6916                    allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6917                        // self can't be used as a lifetime, so no need to check for collisions
6918                        "'self".into()
6919                    } else {
6920                        gen_idx_name()
6921                    });
6922                }
6923                potential_lt_refs.for_each(|(name, ..)| {
6924                    let name = match name {
6925                        Some(it) if config.param_names_for_lifetime_elision_hints => {
6926                            if let Some(c) = used_names.get_mut(it.text().as_str()) {
6927                                *c += 1;
6928                                SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6929                            } else {
6930                                used_names.insert(it.text().as_str().into(), 0);
6931                                SmolStr::from_iter(["\'", it.text().as_str()])
6932                            }
6933                        }
6934                        _ => gen_idx_name(),
6935                    };
6936                    allocated_lifetimes.push(name);
6937                });
6938            }
6939
6940            // ... snip
6941        }
6942
6943        // ... snip
6944
6945            #[test]
6946            fn hints_lifetimes_named() {
6947                check_with_config(
6948                    InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6949                    r#"
6950        fn nested_in<'named>(named: &        &X<      &()>) {}
6951        //          ^'named1, 'named2, 'named3, $
6952                                  //^'named1 ^'named2 ^'named3
6953        "#,
6954                );
6955            }
6956
6957        // ... snip
6958        "##,
6959                                },
6960                        "inlay_hints.rs": r#"
6961    #[derive(Clone, Debug, PartialEq, Eq)]
6962    pub struct InlayHintsConfig {
6963        // ... snip
6964        pub param_names_for_lifetime_elision_hints: bool,
6965        pub max_length: Option<usize>,
6966        // ... snip
6967    }
6968
6969    impl Config {
6970        pub fn inlay_hints(&self) -> InlayHintsConfig {
6971            InlayHintsConfig {
6972                // ... snip
6973                param_names_for_lifetime_elision_hints: self
6974                    .inlayHints_lifetimeElisionHints_useParameterNames()
6975                    .to_owned(),
6976                max_length: self.inlayHints_maxLength().to_owned(),
6977                // ... snip
6978            }
6979        }
6980    }
6981    "#,
6982                        "static_index.rs": r#"
6983// ... snip
6984        fn add_file(&mut self, file_id: FileId) {
6985            let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6986            let folds = self.analysis.folding_ranges(file_id).unwrap();
6987            let inlay_hints = self
6988                .analysis
6989                .inlay_hints(
6990                    &InlayHintsConfig {
6991                        // ... snip
6992                        closure_style: hir::ClosureStyle::ImplFn,
6993                        param_names_for_lifetime_elision_hints: false,
6994                        binding_mode_hints: false,
6995                        max_length: Some(25),
6996                        closure_capture_hints: false,
6997                        // ... snip
6998                    },
6999                    file_id,
7000                    None,
7001                )
7002                .unwrap();
7003            // ... snip
7004    }
7005// ... snip
7006    "#
7007                            }
7008                        },
7009                        "rust-analyzer": {
7010                            "src": {
7011                                "cli": {
7012                                    "analysis_stats.rs": r#"
7013        // ... snip
7014                for &file_id in &file_ids {
7015                    _ = analysis.inlay_hints(
7016                        &InlayHintsConfig {
7017                            // ... snip
7018                            implicit_drop_hints: true,
7019                            lifetime_elision_hints: ide::LifetimeElisionHints::Always,
7020                            param_names_for_lifetime_elision_hints: true,
7021                            hide_named_constructor_hints: false,
7022                            hide_closure_initialization_hints: false,
7023                            closure_style: hir::ClosureStyle::ImplFn,
7024                            max_length: Some(25),
7025                            closing_brace_hints_min_lines: Some(20),
7026                            fields_to_resolve: InlayFieldsToResolve::empty(),
7027                            range_exclusive_hints: true,
7028                        },
7029                        file_id.into(),
7030                        None,
7031                    );
7032                }
7033        // ... snip
7034                                    "#,
7035                                },
7036                                "config.rs": r#"
7037                config_data! {
7038                    /// Configs that only make sense when they are set by a client. As such they can only be defined
7039                    /// by setting them using client's settings (e.g `settings.json` on VS Code).
7040                    client: struct ClientDefaultConfigData <- ClientConfigInput -> {
7041                        // ... snip
7042                        /// Maximum length for inlay hints. Set to null to have an unlimited length.
7043                        inlayHints_maxLength: Option<usize>                        = Some(25),
7044                        // ... snip
7045                        /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
7046                        inlayHints_lifetimeElisionHints_useParameterNames: bool    = false,
7047                        // ... snip
7048                    }
7049                }
7050
7051                impl Config {
7052                    // ... snip
7053                    pub fn inlay_hints(&self) -> InlayHintsConfig {
7054                        InlayHintsConfig {
7055                            // ... snip
7056                            param_names_for_lifetime_elision_hints: self
7057                                .inlayHints_lifetimeElisionHints_useParameterNames()
7058                                .to_owned(),
7059                            max_length: self.inlayHints_maxLength().to_owned(),
7060                            // ... snip
7061                        }
7062                    }
7063                    // ... snip
7064                }
7065                "#
7066                                }
7067                        }
7068                    }
7069            }),
7070        )
7071        .await;
7072    }
7073
7074    fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
7075        outline_panel
7076            .active_editor()
7077            .unwrap()
7078            .read(cx)
7079            .buffer()
7080            .read(cx)
7081            .snapshot(cx)
7082    }
7083
7084    fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
7085        editor.update(cx, |editor, cx| {
7086            let selections = editor.selections.all::<language::Point>(&editor.display_snapshot(cx));
7087            assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
7088            let selection = selections.first().unwrap();
7089            let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
7090            let line_start = language::Point::new(selection.start.row, 0);
7091            let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
7092            multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
7093        })
7094    }
7095
7096    #[gpui::test]
7097    async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) {
7098        init_test(cx);
7099
7100        let fs = FakeFs::new(cx.background_executor.clone());
7101        fs.insert_tree(
7102            "/test",
7103            json!({
7104                "src": {
7105                    "lib.rs": indoc!("
7106                            mod outer {
7107                                pub struct OuterStruct {
7108                                    field: String,
7109                                }
7110                                impl OuterStruct {
7111                                    pub fn new() -> Self {
7112                                        Self { field: String::new() }
7113                                    }
7114                                    pub fn method(&self) {
7115                                        println!(\"{}\", self.field);
7116                                    }
7117                                }
7118                                mod inner {
7119                                    pub fn inner_function() {
7120                                        let x = 42;
7121                                        println!(\"{}\", x);
7122                                    }
7123                                    pub struct InnerStruct {
7124                                        value: i32,
7125                                    }
7126                                }
7127                            }
7128                            fn main() {
7129                                let s = outer::OuterStruct::new();
7130                                s.method();
7131                            }
7132                        "),
7133                }
7134            }),
7135        )
7136        .await;
7137
7138        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7139        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
7140        let (window, workspace) = add_outline_panel(&project, cx).await;
7141        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7142        let outline_panel = outline_panel(&workspace, cx);
7143
7144        outline_panel.update_in(cx, |outline_panel, window, cx| {
7145            outline_panel.set_active(true, window, cx)
7146        });
7147
7148        workspace
7149            .update_in(cx, |workspace, window, cx| {
7150                workspace.open_abs_path(
7151                    PathBuf::from("/test/src/lib.rs"),
7152                    OpenOptions {
7153                        visible: Some(OpenVisible::All),
7154                        ..Default::default()
7155                    },
7156                    window,
7157                    cx,
7158                )
7159            })
7160            .await
7161            .unwrap();
7162
7163        cx.executor()
7164            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7165        cx.run_until_parked();
7166
7167        // Force another update cycle to ensure outlines are fetched
7168        outline_panel.update_in(cx, |panel, window, cx| {
7169            panel.update_non_fs_items(window, cx);
7170            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7171        });
7172        cx.executor()
7173            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7174        cx.run_until_parked();
7175
7176        outline_panel.update(cx, |outline_panel, cx| {
7177            assert_eq!(
7178                display_entries(
7179                    &project,
7180                    &snapshot(outline_panel, cx),
7181                    &outline_panel.cached_entries,
7182                    outline_panel.selected_entry(),
7183                    cx,
7184                ),
7185                indoc!(
7186                    "
7187outline: mod outer  <==== selected
7188  outline: pub struct OuterStruct
7189    outline: field
7190  outline: impl OuterStruct
7191    outline: pub fn new
7192    outline: pub fn method
7193  outline: mod inner
7194    outline: pub fn inner_function
7195    outline: pub struct InnerStruct
7196      outline: value
7197outline: fn main"
7198                )
7199            );
7200        });
7201
7202        let parent_outline = outline_panel
7203            .read_with(cx, |panel, _cx| {
7204                panel
7205                    .cached_entries
7206                    .iter()
7207                    .find_map(|entry| match &entry.entry {
7208                        PanelEntry::Outline(OutlineEntry::Outline(outline))
7209                            if panel
7210                                .outline_children_cache
7211                                .get(&outline.range.start.buffer_id)
7212                                .and_then(|children_map| {
7213                                    let key = (outline.range.clone(), outline.depth);
7214                                    children_map.get(&key)
7215                                })
7216                                .copied()
7217                                .unwrap_or(false) =>
7218                        {
7219                            Some(entry.entry.clone())
7220                        }
7221                        _ => None,
7222                    })
7223            })
7224            .expect("Should find an outline with children");
7225
7226        outline_panel.update_in(cx, |panel, window, cx| {
7227            panel.select_entry(parent_outline.clone(), true, window, cx);
7228            panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7229        });
7230        cx.executor()
7231            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7232        cx.run_until_parked();
7233
7234        outline_panel.update(cx, |outline_panel, cx| {
7235            assert_eq!(
7236                display_entries(
7237                    &project,
7238                    &snapshot(outline_panel, cx),
7239                    &outline_panel.cached_entries,
7240                    outline_panel.selected_entry(),
7241                    cx,
7242                ),
7243                indoc!(
7244                    "
7245outline: mod outer  <==== selected
7246outline: fn main"
7247                )
7248            );
7249        });
7250
7251        outline_panel.update_in(cx, |panel, window, cx| {
7252            panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7253        });
7254        cx.executor()
7255            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7256        cx.run_until_parked();
7257
7258        outline_panel.update(cx, |outline_panel, cx| {
7259            assert_eq!(
7260                display_entries(
7261                    &project,
7262                    &snapshot(outline_panel, cx),
7263                    &outline_panel.cached_entries,
7264                    outline_panel.selected_entry(),
7265                    cx,
7266                ),
7267                indoc!(
7268                    "
7269outline: mod outer  <==== selected
7270  outline: pub struct OuterStruct
7271    outline: field
7272  outline: impl OuterStruct
7273    outline: pub fn new
7274    outline: pub fn method
7275  outline: mod inner
7276    outline: pub fn inner_function
7277    outline: pub struct InnerStruct
7278      outline: value
7279outline: fn main"
7280                )
7281            );
7282        });
7283
7284        outline_panel.update_in(cx, |panel, window, cx| {
7285            panel.collapsed_entries.clear();
7286            panel.update_cached_entries(None, window, cx);
7287        });
7288        cx.executor()
7289            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7290        cx.run_until_parked();
7291
7292        outline_panel.update_in(cx, |panel, window, cx| {
7293            let outlines_with_children: Vec<_> = panel
7294                .cached_entries
7295                .iter()
7296                .filter_map(|entry| match &entry.entry {
7297                    PanelEntry::Outline(OutlineEntry::Outline(outline))
7298                        if panel
7299                            .outline_children_cache
7300                            .get(&outline.range.start.buffer_id)
7301                            .and_then(|children_map| {
7302                                let key = (outline.range.clone(), outline.depth);
7303                                children_map.get(&key)
7304                            })
7305                            .copied()
7306                            .unwrap_or(false) =>
7307                    {
7308                        Some(entry.entry.clone())
7309                    }
7310                    _ => None,
7311                })
7312                .collect();
7313
7314            for outline in outlines_with_children {
7315                panel.select_entry(outline, false, window, cx);
7316                panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7317            }
7318        });
7319        cx.executor()
7320            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7321        cx.run_until_parked();
7322
7323        outline_panel.update(cx, |outline_panel, cx| {
7324            assert_eq!(
7325                display_entries(
7326                    &project,
7327                    &snapshot(outline_panel, cx),
7328                    &outline_panel.cached_entries,
7329                    outline_panel.selected_entry(),
7330                    cx,
7331                ),
7332                indoc!(
7333                    "
7334outline: mod outer
7335outline: fn main"
7336                )
7337            );
7338        });
7339
7340        let collapsed_entries_count =
7341            outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len());
7342        assert!(
7343            collapsed_entries_count > 0,
7344            "Should have collapsed entries tracked"
7345        );
7346    }
7347
7348    #[gpui::test]
7349    async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) {
7350        init_test(cx);
7351
7352        let fs = FakeFs::new(cx.background_executor.clone());
7353        fs.insert_tree(
7354            "/test",
7355            json!({
7356                "src": {
7357                    "main.rs": indoc!("
7358                            struct Config {
7359                                name: String,
7360                                value: i32,
7361                            }
7362                            impl Config {
7363                                fn new(name: String) -> Self {
7364                                    Self { name, value: 0 }
7365                                }
7366                                fn get_value(&self) -> i32 {
7367                                    self.value
7368                                }
7369                            }
7370                            enum Status {
7371                                Active,
7372                                Inactive,
7373                            }
7374                            fn process_config(config: Config) -> Status {
7375                                if config.get_value() > 0 {
7376                                    Status::Active
7377                                } else {
7378                                    Status::Inactive
7379                                }
7380                            }
7381                            fn main() {
7382                                let config = Config::new(\"test\".to_string());
7383                                let status = process_config(config);
7384                            }
7385                        "),
7386                }
7387            }),
7388        )
7389        .await;
7390
7391        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7392        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
7393
7394        let (window, workspace) = add_outline_panel(&project, cx).await;
7395        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7396        let outline_panel = outline_panel(&workspace, cx);
7397
7398        outline_panel.update_in(cx, |outline_panel, window, cx| {
7399            outline_panel.set_active(true, window, cx)
7400        });
7401
7402        let _editor = workspace
7403            .update_in(cx, |workspace, window, cx| {
7404                workspace.open_abs_path(
7405                    PathBuf::from("/test/src/main.rs"),
7406                    OpenOptions {
7407                        visible: Some(OpenVisible::All),
7408                        ..Default::default()
7409                    },
7410                    window,
7411                    cx,
7412                )
7413            })
7414            .await
7415            .unwrap();
7416
7417        cx.executor()
7418            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7419        cx.run_until_parked();
7420
7421        outline_panel.update(cx, |outline_panel, _cx| {
7422            outline_panel.selected_entry = SelectedEntry::None;
7423        });
7424
7425        // Check initial state - all entries should be expanded by default
7426        outline_panel.update(cx, |outline_panel, cx| {
7427            assert_eq!(
7428                display_entries(
7429                    &project,
7430                    &snapshot(outline_panel, cx),
7431                    &outline_panel.cached_entries,
7432                    outline_panel.selected_entry(),
7433                    cx,
7434                ),
7435                indoc!(
7436                    "
7437outline: struct Config
7438  outline: name
7439  outline: value
7440outline: impl Config
7441  outline: fn new
7442  outline: fn get_value
7443outline: enum Status
7444  outline: Active
7445  outline: Inactive
7446outline: fn process_config
7447outline: fn main"
7448                )
7449            );
7450        });
7451
7452        outline_panel.update(cx, |outline_panel, _cx| {
7453            outline_panel.selected_entry = SelectedEntry::None;
7454        });
7455
7456        cx.update(|window, cx| {
7457            outline_panel.update(cx, |outline_panel, cx| {
7458                outline_panel.select_first(&SelectFirst, window, cx);
7459            });
7460        });
7461
7462        cx.executor()
7463            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7464        cx.run_until_parked();
7465
7466        outline_panel.update(cx, |outline_panel, cx| {
7467            assert_eq!(
7468                display_entries(
7469                    &project,
7470                    &snapshot(outline_panel, cx),
7471                    &outline_panel.cached_entries,
7472                    outline_panel.selected_entry(),
7473                    cx,
7474                ),
7475                indoc!(
7476                    "
7477outline: struct Config  <==== selected
7478  outline: name
7479  outline: value
7480outline: impl Config
7481  outline: fn new
7482  outline: fn get_value
7483outline: enum Status
7484  outline: Active
7485  outline: Inactive
7486outline: fn process_config
7487outline: fn main"
7488                )
7489            );
7490        });
7491
7492        cx.update(|window, cx| {
7493            outline_panel.update(cx, |outline_panel, cx| {
7494                outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7495            });
7496        });
7497
7498        cx.executor()
7499            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7500        cx.run_until_parked();
7501
7502        outline_panel.update(cx, |outline_panel, cx| {
7503            assert_eq!(
7504                display_entries(
7505                    &project,
7506                    &snapshot(outline_panel, cx),
7507                    &outline_panel.cached_entries,
7508                    outline_panel.selected_entry(),
7509                    cx,
7510                ),
7511                indoc!(
7512                    "
7513outline: struct Config  <==== selected
7514outline: impl Config
7515  outline: fn new
7516  outline: fn get_value
7517outline: enum Status
7518  outline: Active
7519  outline: Inactive
7520outline: fn process_config
7521outline: fn main"
7522                )
7523            );
7524        });
7525
7526        cx.update(|window, cx| {
7527            outline_panel.update(cx, |outline_panel, cx| {
7528                outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7529            });
7530        });
7531
7532        cx.executor()
7533            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7534        cx.run_until_parked();
7535
7536        outline_panel.update(cx, |outline_panel, cx| {
7537            assert_eq!(
7538                display_entries(
7539                    &project,
7540                    &snapshot(outline_panel, cx),
7541                    &outline_panel.cached_entries,
7542                    outline_panel.selected_entry(),
7543                    cx,
7544                ),
7545                indoc!(
7546                    "
7547outline: struct Config  <==== selected
7548  outline: name
7549  outline: value
7550outline: impl Config
7551  outline: fn new
7552  outline: fn get_value
7553outline: enum Status
7554  outline: Active
7555  outline: Inactive
7556outline: fn process_config
7557outline: fn main"
7558                )
7559            );
7560        });
7561    }
7562
7563    #[gpui::test]
7564    async fn test_outline_expand_collapse_all(cx: &mut TestAppContext) {
7565        init_test(cx);
7566
7567        let fs = FakeFs::new(cx.background_executor.clone());
7568        fs.insert_tree(
7569            "/test",
7570            json!({
7571                "src": {
7572                    "lib.rs": indoc!("
7573                            mod outer {
7574                                pub struct OuterStruct {
7575                                    field: String,
7576                                }
7577                                impl OuterStruct {
7578                                    pub fn new() -> Self {
7579                                        Self { field: String::new() }
7580                                    }
7581                                    pub fn method(&self) {
7582                                        println!(\"{}\", self.field);
7583                                    }
7584                                }
7585                                mod inner {
7586                                    pub fn inner_function() {
7587                                        let x = 42;
7588                                        println!(\"{}\", x);
7589                                    }
7590                                    pub struct InnerStruct {
7591                                        value: i32,
7592                                    }
7593                                }
7594                            }
7595                            fn main() {
7596                                let s = outer::OuterStruct::new();
7597                                s.method();
7598                            }
7599                        "),
7600                }
7601            }),
7602        )
7603        .await;
7604
7605        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7606        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
7607        let (window, workspace) = add_outline_panel(&project, cx).await;
7608        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7609        let outline_panel = outline_panel(&workspace, cx);
7610
7611        outline_panel.update_in(cx, |outline_panel, window, cx| {
7612            outline_panel.set_active(true, window, cx)
7613        });
7614
7615        workspace
7616            .update_in(cx, |workspace, window, cx| {
7617                workspace.open_abs_path(
7618                    PathBuf::from("/test/src/lib.rs"),
7619                    OpenOptions {
7620                        visible: Some(OpenVisible::All),
7621                        ..Default::default()
7622                    },
7623                    window,
7624                    cx,
7625                )
7626            })
7627            .await
7628            .unwrap();
7629
7630        cx.executor()
7631            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7632        cx.run_until_parked();
7633
7634        // Force another update cycle to ensure outlines are fetched
7635        outline_panel.update_in(cx, |panel, window, cx| {
7636            panel.update_non_fs_items(window, cx);
7637            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7638        });
7639        cx.executor()
7640            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7641        cx.run_until_parked();
7642
7643        outline_panel.update(cx, |outline_panel, cx| {
7644            assert_eq!(
7645                display_entries(
7646                    &project,
7647                    &snapshot(outline_panel, cx),
7648                    &outline_panel.cached_entries,
7649                    outline_panel.selected_entry(),
7650                    cx,
7651                ),
7652                indoc!(
7653                    "
7654outline: mod outer  <==== selected
7655  outline: pub struct OuterStruct
7656    outline: field
7657  outline: impl OuterStruct
7658    outline: pub fn new
7659    outline: pub fn method
7660  outline: mod inner
7661    outline: pub fn inner_function
7662    outline: pub struct InnerStruct
7663      outline: value
7664outline: fn main"
7665                )
7666            );
7667        });
7668
7669        let _parent_outline = outline_panel
7670            .read_with(cx, |panel, _cx| {
7671                panel
7672                    .cached_entries
7673                    .iter()
7674                    .find_map(|entry| match &entry.entry {
7675                        PanelEntry::Outline(OutlineEntry::Outline(outline))
7676                            if panel
7677                                .outline_children_cache
7678                                .get(&outline.range.start.buffer_id)
7679                                .and_then(|children_map| {
7680                                    let key = (outline.range.clone(), outline.depth);
7681                                    children_map.get(&key)
7682                                })
7683                                .copied()
7684                                .unwrap_or(false) =>
7685                        {
7686                            Some(entry.entry.clone())
7687                        }
7688                        _ => None,
7689                    })
7690            })
7691            .expect("Should find an outline with children");
7692
7693        // Collapse all entries
7694        outline_panel.update_in(cx, |panel, window, cx| {
7695            panel.collapse_all_entries(&CollapseAllEntries, window, cx);
7696        });
7697        cx.executor()
7698            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7699        cx.run_until_parked();
7700
7701        let expected_collapsed_output = indoc!(
7702            "
7703        outline: mod outer  <==== selected
7704        outline: fn main"
7705        );
7706
7707        outline_panel.update(cx, |panel, cx| {
7708            assert_eq! {
7709                display_entries(
7710                    &project,
7711                    &snapshot(panel, cx),
7712                    &panel.cached_entries,
7713                    panel.selected_entry(),
7714                    cx,
7715                ),
7716                expected_collapsed_output
7717            };
7718        });
7719
7720        // Expand all entries
7721        outline_panel.update_in(cx, |panel, window, cx| {
7722            panel.expand_all_entries(&ExpandAllEntries, window, cx);
7723        });
7724        cx.executor()
7725            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7726        cx.run_until_parked();
7727
7728        let expected_expanded_output = indoc!(
7729            "
7730        outline: mod outer  <==== selected
7731          outline: pub struct OuterStruct
7732            outline: field
7733          outline: impl OuterStruct
7734            outline: pub fn new
7735            outline: pub fn method
7736          outline: mod inner
7737            outline: pub fn inner_function
7738            outline: pub struct InnerStruct
7739              outline: value
7740        outline: fn main"
7741        );
7742
7743        outline_panel.update(cx, |panel, cx| {
7744            assert_eq! {
7745                display_entries(
7746                    &project,
7747                    &snapshot(panel, cx),
7748                    &panel.cached_entries,
7749                    panel.selected_entry(),
7750                    cx,
7751                ),
7752                expected_expanded_output
7753            };
7754        });
7755    }
7756
7757    #[gpui::test]
7758    async fn test_buffer_search(cx: &mut TestAppContext) {
7759        init_test(cx);
7760
7761        let fs = FakeFs::new(cx.background_executor.clone());
7762        fs.insert_tree(
7763            "/test",
7764            json!({
7765                "foo.txt": r#"<_constitution>
7766
7767</_constitution>
7768
7769
7770
7771## 📊 Output
7772
7773| Field          | Meaning                |
7774"#
7775            }),
7776        )
7777        .await;
7778
7779        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7780        let (window, workspace) = add_outline_panel(&project, cx).await;
7781        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7782
7783        let editor = workspace
7784            .update_in(cx, |workspace, window, cx| {
7785                workspace.open_abs_path(
7786                    PathBuf::from("/test/foo.txt"),
7787                    OpenOptions {
7788                        visible: Some(OpenVisible::All),
7789                        ..OpenOptions::default()
7790                    },
7791                    window,
7792                    cx,
7793                )
7794            })
7795            .await
7796            .unwrap()
7797            .downcast::<Editor>()
7798            .unwrap();
7799
7800        let search_bar = workspace.update_in(cx, |_, window, cx| {
7801            cx.new(|cx| {
7802                let mut search_bar = BufferSearchBar::new(None, window, cx);
7803                search_bar.set_active_pane_item(Some(&editor), window, cx);
7804                search_bar.show(window, cx);
7805                search_bar
7806            })
7807        });
7808
7809        let outline_panel = outline_panel(&workspace, cx);
7810
7811        outline_panel.update_in(cx, |outline_panel, window, cx| {
7812            outline_panel.set_active(true, window, cx)
7813        });
7814
7815        search_bar
7816            .update_in(cx, |search_bar, window, cx| {
7817                search_bar.search("  ", None, true, window, cx)
7818            })
7819            .await
7820            .unwrap();
7821
7822        cx.executor()
7823            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7824        cx.run_until_parked();
7825
7826        outline_panel.update(cx, |outline_panel, cx| {
7827            assert_eq!(
7828                display_entries(
7829                    &project,
7830                    &snapshot(outline_panel, cx),
7831                    &outline_panel.cached_entries,
7832                    outline_panel.selected_entry(),
7833                    cx,
7834                ),
7835                "search: | Field«  »        | Meaning                |  <==== selected
7836search: | Field  «  »      | Meaning                |
7837search: | Field    «  »    | Meaning                |
7838search: | Field      «  »  | Meaning                |
7839search: | Field        «  »| Meaning                |
7840search: | Field          | Meaning«  »              |
7841search: | Field          | Meaning  «  »            |
7842search: | Field          | Meaning    «  »          |
7843search: | Field          | Meaning      «  »        |
7844search: | Field          | Meaning        «  »      |
7845search: | Field          | Meaning          «  »    |
7846search: | Field          | Meaning            «  »  |
7847search: | Field          | Meaning              «  »|"
7848            );
7849        });
7850    }
7851
7852    #[gpui::test]
7853    async fn test_outline_panel_lsp_document_symbols(cx: &mut TestAppContext) {
7854        init_test(cx);
7855
7856        let root = path!("/root");
7857        let fs = FakeFs::new(cx.background_executor.clone());
7858        fs.insert_tree(
7859            root,
7860            json!({
7861                "src": {
7862                    "lib.rs": "struct Foo {\n    bar: u32,\n    baz: String,\n}\n",
7863                }
7864            }),
7865        )
7866        .await;
7867
7868        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
7869        let language_registry = project.read_with(cx, |project, _| {
7870            project.languages().add(rust_lang());
7871            project.languages().clone()
7872        });
7873
7874        let mut fake_language_servers = language_registry.register_fake_lsp(
7875            "Rust",
7876            FakeLspAdapter {
7877                capabilities: lsp::ServerCapabilities {
7878                    document_symbol_provider: Some(lsp::OneOf::Left(true)),
7879                    ..lsp::ServerCapabilities::default()
7880                },
7881                initializer: Some(Box::new(|fake_language_server| {
7882                    fake_language_server
7883                        .set_request_handler::<lsp::request::DocumentSymbolRequest, _, _>(
7884                            move |_, _| async move {
7885                                #[allow(deprecated)]
7886                                Ok(Some(lsp::DocumentSymbolResponse::Nested(vec![
7887                                    lsp::DocumentSymbol {
7888                                        name: "Foo".to_string(),
7889                                        detail: None,
7890                                        kind: lsp::SymbolKind::STRUCT,
7891                                        tags: None,
7892                                        deprecated: None,
7893                                        range: lsp::Range::new(
7894                                            lsp::Position::new(0, 0),
7895                                            lsp::Position::new(3, 1),
7896                                        ),
7897                                        selection_range: lsp::Range::new(
7898                                            lsp::Position::new(0, 7),
7899                                            lsp::Position::new(0, 10),
7900                                        ),
7901                                        children: Some(vec![
7902                                            lsp::DocumentSymbol {
7903                                                name: "bar".to_string(),
7904                                                detail: None,
7905                                                kind: lsp::SymbolKind::FIELD,
7906                                                tags: None,
7907                                                deprecated: None,
7908                                                range: lsp::Range::new(
7909                                                    lsp::Position::new(1, 4),
7910                                                    lsp::Position::new(1, 13),
7911                                                ),
7912                                                selection_range: lsp::Range::new(
7913                                                    lsp::Position::new(1, 4),
7914                                                    lsp::Position::new(1, 7),
7915                                                ),
7916                                                children: None,
7917                                            },
7918                                            lsp::DocumentSymbol {
7919                                                name: "lsp_only_field".to_string(),
7920                                                detail: None,
7921                                                kind: lsp::SymbolKind::FIELD,
7922                                                tags: None,
7923                                                deprecated: None,
7924                                                range: lsp::Range::new(
7925                                                    lsp::Position::new(2, 4),
7926                                                    lsp::Position::new(2, 15),
7927                                                ),
7928                                                selection_range: lsp::Range::new(
7929                                                    lsp::Position::new(2, 4),
7930                                                    lsp::Position::new(2, 7),
7931                                                ),
7932                                                children: None,
7933                                            },
7934                                        ]),
7935                                    },
7936                                ])))
7937                            },
7938                        );
7939                })),
7940                ..FakeLspAdapter::default()
7941            },
7942        );
7943
7944        let (window, workspace) = add_outline_panel(&project, cx).await;
7945        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7946        let outline_panel = outline_panel(&workspace, cx);
7947        cx.update(|window, cx| {
7948            outline_panel.update(cx, |outline_panel, cx| {
7949                outline_panel.set_active(true, window, cx)
7950            });
7951        });
7952
7953        let _editor = workspace
7954            .update_in(cx, |workspace, window, cx| {
7955                workspace.open_abs_path(
7956                    PathBuf::from(path!("/root/src/lib.rs")),
7957                    OpenOptions {
7958                        visible: Some(OpenVisible::All),
7959                        ..OpenOptions::default()
7960                    },
7961                    window,
7962                    cx,
7963                )
7964            })
7965            .await
7966            .expect("Failed to open Rust source file")
7967            .downcast::<Editor>()
7968            .expect("Should open an editor for Rust source file");
7969        let _fake_language_server = fake_language_servers.next().await.unwrap();
7970        cx.executor()
7971            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7972        cx.run_until_parked();
7973
7974        // Step 1: tree-sitter outlines by default
7975        outline_panel.update(cx, |outline_panel, cx| {
7976            assert_eq!(
7977                display_entries(
7978                    &project,
7979                    &snapshot(outline_panel, cx),
7980                    &outline_panel.cached_entries,
7981                    outline_panel.selected_entry(),
7982                    cx,
7983                ),
7984                indoc!(
7985                    "
7986outline: struct Foo  <==== selected
7987  outline: bar
7988  outline: baz"
7989                ),
7990                "Step 1: tree-sitter outlines should be displayed by default"
7991            );
7992        });
7993
7994        // Step 2: Switch to LSP document symbols
7995        cx.update(|_, cx| {
7996            settings::SettingsStore::update_global(
7997                cx,
7998                |store: &mut settings::SettingsStore, cx| {
7999                    store.update_user_settings(cx, |settings| {
8000                        settings.project.all_languages.defaults.document_symbols =
8001                            Some(settings::DocumentSymbols::On);
8002                    });
8003                },
8004            );
8005        });
8006        cx.executor()
8007            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
8008        cx.run_until_parked();
8009
8010        outline_panel.update(cx, |outline_panel, cx| {
8011            assert_eq!(
8012                display_entries(
8013                    &project,
8014                    &snapshot(outline_panel, cx),
8015                    &outline_panel.cached_entries,
8016                    outline_panel.selected_entry(),
8017                    cx,
8018                ),
8019                indoc!(
8020                    "
8021outline: struct Foo  <==== selected
8022  outline: bar
8023  outline: lsp_only_field"
8024                ),
8025                "Step 2: After switching to LSP, should see LSP-provided symbols"
8026            );
8027        });
8028
8029        // Step 3: Switch back to tree-sitter
8030        cx.update(|_, cx| {
8031            settings::SettingsStore::update_global(
8032                cx,
8033                |store: &mut settings::SettingsStore, cx| {
8034                    store.update_user_settings(cx, |settings| {
8035                        settings.project.all_languages.defaults.document_symbols =
8036                            Some(settings::DocumentSymbols::Off);
8037                    });
8038                },
8039            );
8040        });
8041        cx.executor()
8042            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
8043        cx.run_until_parked();
8044
8045        outline_panel.update(cx, |outline_panel, cx| {
8046            assert_eq!(
8047                display_entries(
8048                    &project,
8049                    &snapshot(outline_panel, cx),
8050                    &outline_panel.cached_entries,
8051                    outline_panel.selected_entry(),
8052                    cx,
8053                ),
8054                indoc!(
8055                    "
8056outline: struct Foo  <==== selected
8057  outline: bar
8058  outline: baz"
8059                ),
8060                "Step 3: tree-sitter outlines should be restored"
8061            );
8062        });
8063    }
8064
8065    #[gpui::test]
8066    async fn test_markdown_outline_selection_at_heading_boundaries(cx: &mut TestAppContext) {
8067        init_test(cx);
8068
8069        let fs = FakeFs::new(cx.background_executor.clone());
8070        fs.insert_tree(
8071            "/test",
8072            json!({
8073                "doc.md": indoc!("
8074                    # Section A
8075
8076                    ## Sub Section A
8077
8078                    ## Sub Section B
8079
8080                    # Section B
8081
8082                ")
8083            }),
8084        )
8085        .await;
8086
8087        let project = Project::test(fs.clone(), [Path::new("/test")], cx).await;
8088        project.read_with(cx, |project, _| project.languages().add(markdown_lang()));
8089        let (window, workspace) = add_outline_panel(&project, cx).await;
8090        let cx = &mut VisualTestContext::from_window(window.into(), cx);
8091        let outline_panel = outline_panel(&workspace, cx);
8092        outline_panel.update_in(cx, |outline_panel, window, cx| {
8093            outline_panel.set_active(true, window, cx)
8094        });
8095
8096        let editor = workspace
8097            .update_in(cx, |workspace, window, cx| {
8098                workspace.open_abs_path(
8099                    PathBuf::from("/test/doc.md"),
8100                    OpenOptions {
8101                        visible: Some(OpenVisible::All),
8102                        ..Default::default()
8103                    },
8104                    window,
8105                    cx,
8106                )
8107            })
8108            .await
8109            .unwrap()
8110            .downcast::<Editor>()
8111            .unwrap();
8112
8113        cx.run_until_parked();
8114
8115        outline_panel.update_in(cx, |panel, window, cx| {
8116            panel.update_non_fs_items(window, cx);
8117            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
8118        });
8119
8120        // Helper function to move the cursor to the first column of a given row
8121        // and return the selected outline entry's text.
8122        let move_cursor_and_get_selection =
8123            |row: u32, cx: &mut VisualTestContext| -> Option<SharedString> {
8124                cx.update(|window, cx| {
8125                    editor.update(cx, |editor, cx| {
8126                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8127                            s.select_ranges(Some(
8128                                language::Point::new(row, 0)..language::Point::new(row, 0),
8129                            ))
8130                        });
8131                    });
8132                });
8133
8134                cx.run_until_parked();
8135
8136                outline_panel.read_with(cx, |panel, _cx| {
8137                    panel.selected_entry().and_then(|entry| match entry {
8138                        PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
8139                            Some(outline.text.clone())
8140                        }
8141                        _ => None,
8142                    })
8143                })
8144            };
8145
8146        assert_eq!(
8147            move_cursor_and_get_selection(0, cx).as_deref(),
8148            Some("# Section A"),
8149            "Cursor at row 0 should select '# Section A'"
8150        );
8151
8152        assert_eq!(
8153            move_cursor_and_get_selection(2, cx).as_deref(),
8154            Some("## Sub Section A"),
8155            "Cursor at row 2 should select '## Sub Section A'"
8156        );
8157
8158        assert_eq!(
8159            move_cursor_and_get_selection(4, cx).as_deref(),
8160            Some("## Sub Section B"),
8161            "Cursor at row 4 should select '## Sub Section B'"
8162        );
8163
8164        assert_eq!(
8165            move_cursor_and_get_selection(6, cx).as_deref(),
8166            Some("# Section B"),
8167            "Cursor at row 6 should select '# Section B'"
8168        );
8169    }
8170}
8171
Served at tenant.openagents/omega Member data and write actions are omitted.