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