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