outline_panel.rs

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