outline_panel.rs

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