outline_panel.rs

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