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