outline_panel.rs

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