outline_panel.rs

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