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